Spring MVC 加入 HTTP Cache 機制

15 七月 2018

  1. 先建立 Java Config 檔案

    @Configuration
    @EnableWebMvc
    public class MvcConfig extends WebMvcConfigurerAdapter {
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
           registry.addResourceHandler("/resources/**")
              .addResourceLocations("/resources/")
              .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
              .resourceChain(false)
              .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
        }
    
        ...
    }
    

    1.1 若使用了 @EnableWebMvc,就不能在 XML 中使用 <annotation-driven /> (參考)
    1.2 WebMvcConfigurerAdapter 只能在 @EnableWebMvc 中使用 (參考同上)

  2. 在 JSP 中改用以下方式來指定靜態檔案路徑

    <script type="text/javascript" src="<c:url value="/resources/js/cache.js" />"></script>
    

    2.1 其中 /resources/js/cache.js 必須以 / 作為開頭,才能正確轉換成 /${contextPath}/resources/js/cache-d347bec25f38177ca4e18844b11f1d42.js
    2.2 推測這麼做的可能原因是同一個 .jsp 檔案有可能被不同的 @RequestMapping 所使用,因此 requestURI() 也會有所不同,故需以 / 確定其參考到的靜態檔案的路徑是正確的

  3. 在 web.xml 加入以下設定:

    <filter>
        <filter-name>resourceUrlEncodingFilter</filter-name>
        <filter-class>
           org.springframework.web.servlet.resource.ResourceUrlEncodingFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>resourceUrlEncodingFilter</filter-name>
        <servlet-name>appServlet</servlet-name>
    </filter-mapping>
    

    3.1 其中 <servlet-name> 應設為 org.springframework.web.servlet.DispatcherServlet 所使用的 servlet name (參考)

  4. 在 Spring 的 xml 設定檔中加入:

    <beans xmlns="http://www.springframework.org/schema/beans" ... >
        ...
        <context:component-scan base-package="idv.shunyi.config" /> <!-- MvcConfig.java 所在的 package -->
        ...
    </beans>
    
  5. 若要在 CSS 中 import 其他 CSS

    @import "cache.css";
    

    5.1 則需改寫 addResourceHandler()

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        ...
    
        registry.addResourceHandler("/resources/css/**")
               .addResourceLocations("/resources/css/")
               .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
               .resourceChain(false)
               .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
               .addTransformer(new CssLinkResourceTransformer());
    }
    

參考資料

變異測試工具 - Pitest Quick Start

17 六月 2018

  1. 在 pom.xml 中加入 pitest (目前最新版本是 v1.4.0)

    <plugins>
        <plugin>
           <groupId>org.pitest</groupId>
           <artifactId>pitest-maven</artifactId>
           <version>1.4.0</version>
        </plugin>
    </plugins>
    

    1.1 若出現以下錯誤訊息:

    上午 11:31:24 PIT >> SEVERE : Error generating coverage. Please check that your classpath contains JUnit 4.6 or above.
    

    1.2 請在 pom.xml 中加入以下設定:(pitest 需要使用到 JUnit v4.6+)

    <dependencies>
        <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
           <scope>test</scope>
        </dependency>
    </dependencies>
    
  2. 執行 mvn org.pitest:pitest-maven:mutationCoverage

  3. 測試的結果會產生到 target/pit-reports/ 目錄中

參考資料

Spring MVC 取得 JSON Object

25 二月 2018

  1. 系統環境

    • Spring Framework v4.3.14.RELEASE
    • Tomcat 8.0.48
  2. 在 pom.xml 加入以下設定

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.8.11</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.8.11</version>
    </dependency>
    
  3. 在 web-context.xml 加入以下設定

    <bean id="messageAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
            </list>
        </property>
    </bean>
    
  4. 加入一個 JSON Object

    package idv.shunyi.entity;
    
    public class JarFile {
    
        private long id;
        private String fileName;
        private String fileType;
        private String mainClass;
    
        public long getId() {
           return id;
        }
        public void setId(long id) {
           this.id = id;
        }
        public String getFileName() {
           return fileName;
        }
        public void setFileName(String fileName) {
           this.fileName = fileName;
        }
        public String getFileType() {
           return fileType;
        }
        public void setFileType(String fileType) {
           this.fileType = fileType;
        }
        public String getMainClass() {
           return mainClass;
        }
        public void setMainClass(String mainClass) {
           this.mainClass = mainClass;
        }
    }
    
  5. Controller 寫法如下:

    @RequestMapping(value = "/testJsonObject.do", produces = "text/html; charset=UTF-8")
    @ResponseBody
    public String param(@RequestBody JarFile jarFile) {
        String value = jarFile.getFileName();
        System.out.println(value);
        return value;
    }
    
  6. Client 端寫法如下 (e.g. JQuery ajax):

    function test() {
        $.ajax({
           method : 'post',
           url: 'testJsonObject.do',
           data: JSON.stringify({ id: '0', fileName: 'test-中文檔名.jar', fileType: 'jar', mainClass: 'idv.shunyi.entity.JarFile'}),
           contentType:'application/json; charset=UTF-8',
           success: function(data) {
             alert(data);
           }
        });
    };
    
  7. Reference
    Passing JSON Map into Spring MVC Controller

Spring 與 Quartz 版本對應

24 十二月 2017

  1. 若要在 Spring 中使用 Quartz v1.x 則使用以下設定 (JobDetailBean & SimpleTriggerBean)

    <bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass" value="idv.shunyi.mvc.QuartzTest" />
    </bean>
    
    <bean id="simpleTriggerBean" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
        <property name="jobDetail" ref="jobDetailBean" />
        <property name="repeatInterval" value="5000" />
        <property name="startDelay" value="1000" />
    </bean>
    
  2. 若要在 Spring (v3.1+) 中使用 Quartz v2.x 則使用以下設定 (JobDetail**Factory**Bean & SimpleTrigger**Factory**Bean)

    <bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="jobClass" value="idv.shunyi.mvc.QuartzTest" />
    </bean>
    
    <bean id="simpleTriggerBean" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
        <property name="jobDetail" ref="jobDetailBean" />
        <property name="repeatInterval" value="5000" />
        <property name="startDelay" value="1000" />
    </bean>
    
  3. 若設定錯誤可能會看到以下 Exception (quartz-2.0.0.jar,卻使用 項目 1. 的設定)

    Caused by: java.lang.IncompatibleClassChangeError: class org.springframework.scheduling.quartz.JobDetailBean has interface org.quartz.JobDetail as super class
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2474)
        at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:855)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1304)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1163)
        at org.springframework.util.ClassUtils.forName(ClassUtils.java:258)
        at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClass(AbstractBeanDefinition.java:417)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doResolveBeanClass(AbstractBeanFactory.java:1283)
        at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1254)
        ... 18 more
    
  4. 參考資料
    Spring should support Quartz 2.0 CronTrigger interface
    IncompatibleClassChangeError : JobDetailBean has interface org.quartz.JobDetail as super class
    Spring 4 + Quartz Scheduler Integration Example
    JobDetailFactoryBean
    SimpleTriggerFactoryBean

關於 volatile 關鍵字

10 十二月 2017

使用 volatile 的變數


簡單的解釋: 在多執行緒的環境下,volatile 用於讓變數的讀和寫都在記憶體中執行。

因為每個執行緒都需將變數放在 CPU cache 才能做運算,所以要從記憶體中把所有可存取的變數複製一份副本到 CPU cache 以供運算時讀寫操作使用,此時就有可能因為某些運算造成 CPU cache 存的變數值和記憶體存的變數值不一致。volatile 可以保證變數的讀寫操作都在記憶體中執行,以確保在多個執行緒都可以取到最新的變數值。

換個說法,每個執行緒都有自己的堆疊,所以它可以存取自己的變數副本。當執行緒被建立時,它將所有可存取的變數複製到自己的記憶體中。volatile 關鍵字用於告訴 JVM 說:「注意,這個變數可以在其他執行緒中修改」。若沒有這個關鍵字,JVM 可以自由地進行一些最佳化,像是在某些執行緒中從不更新這些本地副本。volatile 會強制執行緒更新每個變數的原始值。volatile 關鍵字可以用在任何類型的變數,可以是 primitive 或 objects!

未使用 volatile 的變數


對於非 volatile 變數而言,JVM 並不能保證什麼時候把記憶體的變數值讀取到 CPU cache 中,以及何時把 CPU cache 的變數值寫入到記憶體中。

因此在多執行緒的環境下,有可能造成 Thread 1 將 counter 變數設為 7,但沒有馬上寫入到記憶體中,造成 Thread 2 取到 counter 之值仍為 0。
java-volatile-2.png
圖片來源

效能


由於 volatile 變數的讀寫都是在記憶體中執行,所以比直接在 CPU cache 做變數讀寫來得慢。
不過比 synchronized 快,因為 synchronized 要等到寫資料的執行緒做完才能夠讀到資料。

為何不使用 synchronized 關鍵字


除了效能比較慢之外,用 synchronized 有一個風險是,若正在讀取變數的執行緒因為某些原因臨時被暫停 (suspended) 的話,其他正在等待寫入同一個變數的執行緒會 陷入無限的等待直到讀取變數的執行緒終於做完

參考資料


Java Volatile Keyword Explained by Example
Java Volatile Keyword
[译] Java Volatile 关键字详解

Open NTP Daemon

09 十二月 2017

  1. 安裝環境
    Debain Jessie

  2. 先安裝 openntpd

    apt-get install openntpd
    
  3. 若要使用國內的 NTP 伺服器,可修改 openntpd 的設定檔,並加入以下設定

    #將預設的國外 NTP 伺服器註解掉
    #servers 0.debian.pool.ntp.org
    #servers 1.debian.pool.ntp.org
    #servers 2.debian.pool.ntp.org
    #servers 3.debian.pool.ntp.org
    
    servers tock.stdtime.gov.tw
    servers time.stdtime.gov.tw
    servers clock.stdtime.gov.tw 
    servers tick.stdtime.gov.tw
    servers watch.stdtime.gov.tw
    
  4. 安裝完後 openntpd 就會自動於 daemon 執行了,若要確認 openntpd 有正確運作,可使用以下指令

    systemctl status openntpd
    
    #執行結果如下,Active 為 running 代表有正確執行
    ● openntpd.service - OpenNTPd Network Time Protocol
       Loaded: loaded (/lib/systemd/system/openntpd.service; enabled)
       Active: active (running) since Sat 2017-12-09 12:05:17 CST; 2h 41min ago
      Process: 3715 ExecStart=/usr/sbin/ntpd $DAEMON_OPTS (code=exited, status=0/SUCCESS)
      Process: 3712 ExecStartPre=/usr/sbin/ntpd -n $DAEMON_OPTS (code=exited, status=0/SUCCESS)
     Main PID: 3717 (ntpd)
       CGroup: /system.slice/openntpd.service
               ├─3717 /usr/sbin/ntpd -f /etc/openntpd/ntpd.conf
               ├─3718 ntpd: ntp engine
               └─3719 ntpd: dns engine
    
  5. 若要手動執行 openntpd,可使用以下指令

    ntpd -dsv
    
  6. 若是出現錯誤訊息:debian ntpd[29354]: adjtime failed: Invalid argument,則要在 /etc/default/openntpd 中加入 DAEMON_OPTS="-s"

參考資料
  NTP 的網路資源
  NTP 新增頻寬
  NTP 伺服器
openntpd: "adjtime failed: Invalid argument"

Apache Tika 範例

19 十一月 2017

可利用 Tika 來取得檔案的內文、Metadata 以及 MIME type

  1. 先準備 pom.xml,在 <dependency> 中加入:

    <dependencies>
        <dependency>
           <groupId>org.apache.tika</groupId>
           <artifactId>tika-core</artifactId>
           <version>1.16</version>
        </dependency>
        <dependency>
           <groupId>org.apache.tika</groupId>
           <artifactId>tika-parsers</artifactId>
           <version>1.16</version>
           <!-- 不知為何 tika-parsers 會依賴於 cxf-rt-rs-client 和 quartz,在這個範例中我先把它排除掉 -->
           <exclusions>
             <exclusion>
              <groupId>org.apache.cxf</groupId>
              <artifactId>cxf-rt-rs-client</artifactId>
             </exclusion>
             <exclusion>
              <groupId>org.quartz-scheduler</groupId>
              <artifactId>quartz</artifactId>
             </exclusion>
           </exclusions>
        </dependency>
        <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
           <scope>test</scope>
        </dependency>
    </dependencies>
    
  2. 執行 mvn eclipse:eclipse

  3. 打開 Eclipse,並將剛剛建立的 project 匯入

  4. 建立 TikaTest.java

    package idv.shunyi.tika;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    import java.io.Reader;
    
    import org.apache.tika.Tika;
    import org.apache.tika.metadata.Metadata;
    import org.junit.Before;
    import org.junit.Test;
    
    public class TikaTest {
        private Tika tika;
        private File file;
    
        @Before
        public void setUp() throws Exception {
           tika = new Tika();
           file = new File(TikaTest.class.getResource("/test.html").toURI());
        }
    
        @Test
        public void testParse() throws Exception {
           Metadata metadata = new Metadata();
    
           try (InputStream in = new FileInputStream(file)) {
             Reader reader = tika.parse(in, metadata);
    
             try (BufferedReader br = new BufferedReader(reader)) {
    
              // Print extracted text content
              System.out.println("### extracted text content");
              for (String text; (text = br.readLine()) != null;) {
                  System.out.println(text);
              }
              System.out.println();
    
              // Print meta data
              System.out.println("### metadata");
              for (String name : metadata.names()) {
                  System.out.println(name + " = " + metadata.get(name));
              }
              System.out.println();
             }
           }
        }
    
        @Test
        public void testParseToString() throws Exception {
           System.out.println("### parse to string");
           try (InputStream stream = new FileInputStream(file)) {
             String text =  tika.parseToString(stream);
             System.out.println(text);
           }
           System.out.println();
        }
    
        @Test
        public void testDetect() throws Exception {
           System.out.println("### detect media type");
           String mimeType = tika.detect(file);
           System.out.println(mimeType);
        }
    }
    
  5. 執行結果如下:

    ### extracted text content
    
        這是內文
    
    ### metadata
    X-Parsed-By = org.apache.tika.parser.DefaultParser
    dc:title = 這是標題
    Content-Encoding = UTF-8
    title = 這是標題
    Content-Type = text/html; charset=UTF-8
    
    ### parse to string
    
        這是內文
    
    
    ### detect media type
    text/html
    

Apache Tika artifacts

04 十一月 2017

tika-core/target/tika-core-*.jar

  Tika core library. Contains the core interfaces and classes of Tika, but none of the parser implementations. Depends only on Java 6.

  Tika 的核心函式庫,不包含任何 Parser

tika-parsers/target/tika-parsers-*.jar

  Tika parsers. Collection of classes that implement the Tika Parser interface based on various external parser libraries.

  Tika Parser 函式庫的彙集

tika-app/target/tika-app-*.jar

  Tika application. Combines the above components and all the external parser libraries into a single runnable jar with a GUI and a command line interface.

  命令列模式的 Tika,相關說明請參考連結

tika-server/target/tika-server-*.jar

  Tika JAX-RS REST application. This is a Jetty web server running Tika REST services.

  在 Jetty 上提供 Tika REST 服務,相關說明請參考連結

tika-bundle/target/tika-bundle-*.jar

  Tika bundle. An OSGi bundle that combines tika-parsers with non-OSGified parser libraries to make them easy to deploy in an OSGi environment.

  Tika 的 OSGi bundle

tika-eval-*.jar

  沒有包含任何 Parser 的版本

參考文件

  Build artifacts
  Download Apache Tika

Firefox for Debain Jessie

29 十月 2017

想把 Debian Jessie 的 Iceweasel 升級成 Firefox ESR

按照 Debian Mozilla Team 網頁上提供的訊息,應該是有 Firefox ESR 52 可以安裝

但是照網頁上的指示,更新 /etc/apt/sources.list 並執行 apt-get 後,卻顯示錯誤訊息:

  Package firefox-esr is not available, but is referred to by another package.
  This may mean that the package is missing, has been obsoleted, or
  is only available from another source

  E: Package 'firefox-esr' has no installation candidate

寫信去 pkg-mozilla-maintainers@lists.alioth.debian.org 問也沒有回應 ...

所以 Firefox ESR 應該已經不支援 Jessie 了...

JBake v2.5.1 安裝

14 十月 2017

  1. 下載 JBake, 並解壓縮

    unzip jbake-2.5.1-bin.zip 
    
  2. 建立 project 目錄,並初始化

    mkdir project
    cd project/
    ../bin/jbake -i
    
  3. 產生 output 目錄

    ../bin/jbake -b 
    
  4. 執行預覽 (若 content 目錄 的資料有改動會自動 update)

    ../bin/jbake -s
    
  5. 若 blog 已從 content 目錄 刪除,已存在於 output 目錄 的資料還是會被保留

Web Service

23 二月 2014

  1. 先建立要透過 Web Service 呼叫的 method

    package tw.shunyi.ws.server;
    
    import javax.jws.WebService;
    
    @WebService
    public class CircleFunctions {
    
        public double getArea(double r) {
            return java.lang.Math.PI * (r * r);
        }
    
        public double getCircumference(double r) {
            return 2 * java.lang.Math.PI * r;
        }
    }
    
  2. 建立 server 端程式

    package tw.shunyi.ws.server;
    
    import javax.xml.ws.Endpoint;
    
    public class Server {
    
        public static void main(String[] args) {
    
            Endpoint.publish(
                    "http://localhost:8080/SimpleWebService/circlefunctions",
                    new CircleFunctions());
    
        }
    }
    
  3. 啟動 web server

    3.1. 程式會動態產生 wrapper class

    2014/2/23 上午 09:11:19 com.sun.xml.internal.ws.model.RuntimeModeler getRequestWrapperClass
    資訊: Dynamically creating request wrapper Class tw.shunyi.ws.server.jaxws.GetArea
    2014/2/23 上午 09:11:19 com.sun.xml.internal.ws.model.RuntimeModeler getResponseWrapperClass
    資訊: Dynamically creating response wrapper bean Class tw.shunyi.ws.server.jaxws.GetAreaResponse
    2014/2/23 上午 09:11:19 com.sun.xml.internal.ws.model.RuntimeModeler getRequestWrapperClass
    資訊: Dynamically creating request wrapper Class tw.shunyi.ws.server.jaxws.GetCircumference
    2014/2/23 上午 09:11:19 com.sun.xml.internal.ws.model.RuntimeModeler getResponseWrapperClass
    資訊: Dynamically creating response wrapper bean Class tw.shunyi.ws.server.jaxws.GetCircumferenceResponse
    

    3.2. 可以執行以下指令來產生 wrapper class, 避免每次重啟 server 都要動態產生

    wsgen -verbose -cp . tw.shunyi.ws.server.CircleFunctions
    
  4. 執行以下指令產生給 client site 用的檔案

    wsimport -p tw.shunyi.ws.client -s src -d bin -verbose http://localhost:8080/SimpleWebService/circlefunctions?WSDL
    
  5. 建立 client 端程式

    package tw.shunyi.ws.client;
    
    public class Client {
        private static CircleFunctionsService service;
    
        public static void main(String[] args) {
            service = new CircleFunctionsService();
            CircleFunctions port = service.getCircleFunctionsPort();
            System.out.println(port.getArea(1.0));
        }
    }
    
  6. 執行結果

    3.141592653589793
    


Older posts are available in the archive.