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.