2017年2月15日 星期三

Dockerfile 初步閱讀與學習紀錄

最近花了點時間閱讀 Docker 官網的上 Dockerfile reference 這篇教學文件。閱讀後在加上一些自己實作測試,稍微有點心得,因此就想說再寫篇文章當作學習筆記吧。

Dockerfile 簡介

在 docker 要自己建置 image,你可以使用比較麻煩點的方法,就是自己先把自己想要的 base image 從 docker hub 拉下來,然後用 container 執行它,接著在 container 內執行你想做的命令,然後在 commit container 讓他變成 image,這方式就會變成是一直在重複用 container run,然後 commit 成 image 的動作。

docker 提供了另一種方式來建置 image,也就是本篇要提到的 Dockerfile。docker 可以透過 Dockerfile 讀指令的方式自動建立 image,也就是說你可以把你要 build image 的步驟透過 Dockerfile 的指令一步一步的寫出來,這樣就能使用 docker build 指令來幫你自動建置 image。至於中間過程需要用 container 執行你的命令、再將 container commit 回 image 這些瑣碎的細節,你可以不需要太在意他。

Context:

在開始寫 Dockerfile 之前,要先知道 build context 的概念,context 可以是本地端的目錄或是某個 URL(Git repository),context 不論是本地端目錄或是 URL,其底下的所有子目錄或是檔案都是屬於 context 內。

當你在 command line 下 docker build 指令要透過 Dockerfile 來 build image 時,docker 並不會在 command line 直接幫你 build,而是要透過 docker daemon 來執行。它會把 build context 傳送給 docker daemon,之後 docker daemon 在幫你 build。

所以 build 的一切過程,是在 docker daemon 上執行的,你在 build 過程中如果要複製檔案到 image 內,檔案也必須要先放在 context 內,這樣 docker daemon 才能在 build 過程中找到檔案。

另外你也可以把 build image 相關的說明文件放在 context 目錄內,然後在搭配 .dockerignore 檔案讓說明文件在 docker build 時不會被傳送到 docker daemon 中。

這邊的 context 有點像是在寫 project 時,一個 project 會放在某個目錄底下,該目錄可以視為一個 context,然後該 project 要執行所需的原始碼、lib、以及相關的說明文件都會放在此目錄底下的。

Dockerfile command:

Dockerfile command 說多不多,以下列出並加上簡短說明。

  • FROM:base image 設定,你在 build image 時,一定需要指定一個 image 來源。
  • RUN:build 時使用。會執行你的命令,然後 commit 結果,被 commit 的結果會被 Dockerfile 下一步驟所使用。
  • CMD:run container 時要執行的命令,要注意這個跟 RUN 的區別,他是指當你使用 docker run 時,container 建立起來的時候要跑什麼樣的命令。另外一個 Dockerfile 內要有一個 CMD 或是等等會提到的 ENTRYPOINT。
  • LABEL:標籤,會是以 key=value 的形式來定義。你可以用 LABEL 記錄一些 image 資訊,如版號、作者等等的訊息。官方建議用一個 LABEL 指令來定義多組值會比較有效率。
  • EXPOSE:告訴 container 在執行時需要監聽哪個 port。注意,EXPOSE 並不是直接開放 port 給外界訪問,要讓外界訪問,你要在下 docker run 指令時加上 -p flag 才能讓外界訪問。
  • ENV:環境變數設定。
  • ADD:將檔案加到 images 內,檔案來源可以是 URL 或是 context 內的檔案。
  • COPY:將檔案加到 images 內,檔案來源是 context 內的檔案,注意這個跟 ADD 的區別,上網查有人是說這個語意清楚,行為單純,就是複製檔案的概念,建議用這個。
  • ENTRYPOINT:run container 時要執行的命令,可以和 CMD 混搭用,此時 CMD 會被視為初始參數來使用。
  • VOLUME:掛載目錄用。
  • USER:可以指令使用者為何。在此指令過後的 Dockerfile 指令,都會用這個使用者來執行。
  • WORKDIR:工作目錄設定,在執行此指令後,在 Dockerfile 內後續的 RUN、CMD、ENTRYPOINT、COPY
  • ARG:buid 時可以設定參數,讓 docker build 指令可以下參數來處理。
  • ONBUILD:提供一個 trigger 機制,當別人要使用你 build 好的 image 當成 base 時,如果你當初在 build 時有寫 ONBUILD 指令,則別人在用這個 image 當成 base image 時,當跑完他 Dockerfile 的 FROM 指令之後會先跑你寫的 ONBUILD,接著才會開始跑它寫的其他指令。
  • STOPSIGNAL:要不要在結束時收到系統發的信號。
  • HEALTHCHECK:提供一個檢查的機制,你可以透過 HEALTHCHECK 來讓 docker 知道什麼樣的狀況對你的 container 來說才算是正常的。
  • SHELL:改變預設的 shell。Dockerfile command 會有 shell 格式的命令,預設是用 /bin/sh -c 來執行命令,你可以透過 SHELL 強制修改掉。

一個簡單的 Dockerfile 範例

接下來示範怎麼寫一個簡單的 Dockerfile:

  1. 首先先在本機上決定你的 context 要在哪,比如說我用以下路徑的 foobar 目錄,foobar 底下的所有檔案都會視為 context。

    /Users/mayer/Documents/dockerspace/foobar
  2. 由於我是使用 Mac 系統,所以我要用 docker 啟動時一併開啟的終端機來 build,先將終端機切換到該目錄:

    cd /Users/mayer/Documents/dockerspace/foobar
  3. 之後,透過 vi 新增 Dockerfile

    vi Dockerfile
  4. 然後開始撰寫 Dockerfile,這邊 FROM image 我們使用 alpine linux,這是個容量只有 3.9 MB 左右的超輕量 Linux,然後預設他並不會執行任何的命令,所以我們會讓他預設起來就執行 top 指令,Dockerfile 內容如下:

    FROM alpine:3.5
    CMD ["top"]
  5. 之後使用 :wq 離開 vi,這樣 Dockerfile 檔案就準備好了。

  6. 接著要使用此 Dockerfile 來 build 我們自己的 image,我們可以使用 docker build 指令來處理,以下是 build 的資訊:

    $ docker build -t mayer/foo:1.0.0 .
    Sending build context to Docker daemon 2.048 kB
    Step 1 : FROM alpine:3.5
    3.5: Pulling from library/alpine
    0a8490d0dfd3: Pull complete 
    Digest: sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8
    Status: Downloaded newer image for alpine:3.5
     ---> 88e169ea8f46
    Step 2 : CMD top
     ---> Running in 5f41dcb5519c
     ---> 7265d7f11cd1
    Removing intermediate container 5f41dcb5519c
    Successfully built 7265d7f11cd1

    上述的 docker build 指令有用上 -t flag,用來指定 image 名稱與 tag,另外後面的 . 代表 context 的路徑為當下目錄。

    下完指令後的第一個訊息,可以看到 docker 先把 build context 傳送給 Docker daemon,然後才開始 build 步驟,首先 docker daemon 在執行 FROM 指令時發現到本地端沒有 alpine:3.5 這個 image,所以他先幫你 pull 下來,然後結束第一步驟。接著執行第二步驟 CMD top,指定這個 image 在透過 container 啟動時預設要執行的命令為 top,這樣就完成了 build image 步驟。

  7. 可以透過 docker images 查看是否 build 成功:

    $ docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    mayer/foo           1.0.0               7265d7f11cd1        8 minutes ago       3.984 MB
  8. 接著使用 container 來執行這個 image,這邊用 -d flag 讓他在背景執行:

    docker run -d mayer/foo:1.0.0
  9. 你可以透過 docker ps 來查詢 container 運行狀態:

    $ docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    8332ee2f38a6        mayer/foo:1.0.0     "top"               30 seconds ago      Up 29 seconds                           nauseous_albattani      
  10. 另外除了 docker ps 指令之外,你也可以 Kilematic 工具來直接觀看 container 運作的內容,看畫面在跑感受會比較深一點。

透過以上步驟,我們已經完成了透過 Dockerfile 來 build image,然後使用 container 來執行 image 的流程了。

接著下面要講記錄一些讓人有些困惑的指令。

ENTRYPOINT 與 CMD

假設有個 Dockerfile 長這樣:

FROM centos:7.2.1511
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

我們透過此 Dockerfile 來 build images:

docker build -t mayer/foo .

接著執行兩個 container:

docker run -d mayer/foo
docker run -d mayer/foo -H

使用 docker ps -a 查詢:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
83b6433d168b        mayer/foo           "top -b -H"         20 minutes ago      Up 4 minutes                            modest_hodgkin
20ba3187d8e3        mayer/foo           "top -b -c"         21 minutes ago      Up 9 seconds                            angry_bardeen

可以看到 Docker 會執行 ENTRYPOINT,若有 CMD,他會將它當成預設參數,你可以在 run 時改變它。

另外可以下指令查 images 相關資訊:

docker inspect mayer/foo:latest

他會印出此 image 預設的 Entrypoint 與 Cmd 為何:

    ...
    "Cmd": [
        "-c"
    ],
    "Image": "sha256:97aa54a9a5ebfeff5a2b1992a1e0cb0721162fc15061da2c4192205ba93f8554",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": [
        "top",
        "-b"
    ],
    ...

WORKDIR:

image 的 workdir 可以用 docker inspect 來觀察,比如說 tomcat:8.5.11-jre8 的:

$ docker inspect tomcat:8.5.11-jre8

...
"WorkingDir": "/usr/local/tomcat",
...

可以看到此 image,他把 WorkingDir 設定在 /usr/local/tomcat。然後我們想從這個 image 加上自己的 webapp,我們該如何做?

首先,在本機上建立一個目錄,把這個目錄視為 context,這邊先用以下路徑的 sample 目錄作為 context:

/Users/mayer/Documents/dockerspace/sample

然後用終端機切換到那個目錄內:

cd /Users/mayer/Documents/dockerspace/sample

然後我們要將 Web Application 放到這目錄,

mv /somepath/sample.war ./

接著使用 vi 來編輯檔案

vi Dockerfile

Dockerfile,內容如下:

FROM tomcat:8.5.11-jre8
COPY sample.war webapps/

可以注意到,這邊 COPY 指令的 dest,是直接寫 webapps/,這邊能這樣寫,是因為他有設定了 WORKDIR 的緣故,其實真正的 dest 會是:

/usr/local/tomcat/webapps

所以上面的 Dockerfile 是說,我要 build 一個 images,來源是 tomcat:8.5.11-jre8,然後幫我把檔案 sample.war 複製到 /usr/local/tomcat/webapps 目錄底下,這樣一來我就可以直接使用 docker build 來建立一個裡面包含我 webapp 的 tomcat image 了:

docker build -t mayer/sample .

注意 docker build 最後那個 . 代表 context 為當下目錄,不能省略。

CMD 與 EXPOSE

接著要如何執行呢?在執行之前,讓我們再看一下 tomcat:8.5.11-jre8 的詳細資訊

$ docker inspect tomcat:8.5.11-jre8

...
"ExposedPorts": {
            "8080/tcp": {}
        },
...
"Cmd": [
    "catalina.sh",
    "run"
],
...
"Entrypoint": null,
...

這邊要注意的是這三個資訊,一個是預設執行的 container 會監聽 8080,一個是他的初始 Cmd 就是讓 tomcat 啟動,然後他沒有寫 Entrypoint,所以可以不用擔心會影響到 Cmd,也就是說如果我們要使用我們 build 的 image 的話,我們只需要使用 docker run 指令來啟動 container 並執行,如下:

docker run -d -p 8080:8080 mayer/foo

注意 port 要透過 -p flag 處理,冒號前面的 8080 代表本地端機器的 port,冒號後面的 8080 代表 container 的 port。

更多實際應用範例

如果你需要更多範例,請參考官網 Docker Documentation - Dockerization examples 這個網頁,裡面包含像是 MongoDB、PostgreSQL 之類的應用使用 Dockerfile 來 build image 的過程。

Reference:

Docker Documentation - Dockerfile reference

Stackoverflow - What is the difference between the COPY and ADD commands in a Dockerfile?

Stackoverflow - What is the difference between CMD and ENTRYPOINT in a Dockerfile?

segmentfault - Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?

2017年2月4日 星期六

Spring MVC with Maven

最近工作上遇到需要快速建立 web service project 來讓同仁測試的需求,想起了好久之前看過但是一直沒試的 Spring MVC。詢問了朋友的意見與上網查了相關資料之後,發覺用這個來建還真的蠻快的,而且處理 JSON 也很方便,通通幫你整合好了,是個很好的快速解決方案,因此嘗試了一下,然後在搭配 Maven 處理 jar 檔問題,摸索加建立基礎架構大概只花兩天而已吧,整個就是迅速。以下是記錄建立整個 web service project 的過程。

Maven

上 Apache Maven Project 官網 抓 maven,設 path,然後 maven repository 設定位置如下,可用預設不用改:

/Users/mayer/Develop/apache-maven-3.3.9/conf/settings.xml
Default: ${user.home}/.m2/repository

Spring 開發工具:

  1. 先抓 eclipse 4.6.2 j2ee。
  2. 在抓 spring 開發工具 Spring Tool Suite。到 https://spring.io/tools/sts/all 抓 Update Site Archives,版本要是 4.6.2
  3. 開啟 eclipse,到 Help -> Install New Software,然後點 Add,在選 Archive,指到你剛剛下載的 spring 開發工具。然後選 Core Spring IDE、Extensions Spring IDE、Integrations Spring IDE、Resources Spring IDE,裝好之後會重啟,這樣 IDE 就完成了。

建立 Maven Project

  1. 建立 Maven Project

    1. 右鍵 New -> Maven Project,選 maven-archetype-webapp 1.0,Group Id 填 tw.com.maxkit,Artifact Id 填 ivrsample,package 會長這樣 tw.com.maxkit.ivrsample,
    2. 預設 index.jsp 會出錯,把 Server Runtime 加進 project 去就好。
    3. 修改 pom 檔案,加上會用到的 spring mvc dependency

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>4.3.5.RELEASE</version>
      </dependency>
    4. 修改 pom 在 build 內加上 plugins,確保下次執行 Maven Update Project 時 eclipse 設定不會跑掉:

        <build>
          <finalName>ivrsample</finalName>
          <plugins>
              <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-compiler-plugin</artifactId>
                  <version>3.1</version>
                  <configuration>
                      <source>1.8</source>
                      <target>1.8</target>
                  </configuration>
              </plugin>
          </plugins>
        </build>
  2. Spring MVC 相關設定

    1. 在 web.xml 加上:

      <servlet>
          <servlet-name>dispatcher</servlet-name>
          <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>dispatcher</servlet-name>
          <url-pattern>/</url-pattern>
      </servlet-mapping>
    2. 增加 dispatcher-servlet.xml,把它放到路徑 ivrsample/WebContent/WEB-INF 底下。專案點右鍵 -> New -> Spring Bean Configuration File,名稱為 dispatcher-servlet.xml,xsd 選 mvc、p、context。然後加上這兩行:

      <context:component-scan base-package="tw.com.maxkit.ivrsample.controller"/>
      <mvc:annotation-driven />
    3. 這樣就可以開始寫 web service,到 tw.com.maxkit.ivrsample.controller 底下建立 FooController.java,範例如下:

      @Controller
      @RequestMapping("/foo")
      public class FooController {
      
          @ResponseBody
          @PostMapping("/test")
          public String hello(){ 
              System.out.println("into test");
              return "hello";
          }
      
          @ResponseBody
          @PostMapping(path = "/testjson", consumes = "application/json")
          public TestBean json(@RequestBody TestBean testBean) {
              System.out.println("testBean = " + testBean);
              System.out.println(testBean.getId());
              System.out.println(testBean.getName());
              System.out.println(testBean.getDate());
      
              TestBean testBean2 = new TestBean();
              testBean2.setId(2);
              testBean2.setName("james");
              testBean2.setDate(new Date());
              return testBean2;
          }
      }

      這個 class 的存取路徑會是:

      http://localhost:8080/ivrsample/foo

      有開放兩個 api:

      http://localhost:8080/ivrsample/foo/test
      http://localhost:8080/ivrsample/foo/testjson

      json 處理參考接下來的說明。

Spring MVC with Json

需要 jackson lib,在 pom 加上:

<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.13</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.3</version>
</dependency>

然後你的 controller method 直接回傳物件就好,沒有其他設定了,如下:

@ResponseBody
@PostMapping(path = "/testjson", consumes = "application/json")
public TestBean json(@RequestBody TestBean testBean) {
    System.out.println("testBean = " + testBean);
    System.out.println(testBean.getId());
    System.out.println(testBean.getName());
    System.out.println(testBean.getDate());

    TestBean testBean2 = new TestBean();
    testBean2.setId(2);
    testBean2.setName("james");
    testBean2.setDate(new Date());
    return testBean2;
}

遇到日期時,他會回傳 timestamp,若要日期轉字串則要加上:

public class TestBean {
    private int id;
    private String name;
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date date;
    // ... get and set method
}

日期相關處理參考:http://wiki.fasterxml.com/JacksonFAQDateHandling

不讓 null 回傳,在 bean 上面加上:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

@JsonInclude(Include.NON_NULL)
public class ApiOut { ... }

or

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class ApiOut { ... }

null 回傳處理參考:http://stackoverflow.com/questions/11757487/how-to-tell-jackson-to-ignore-a-field-during-serialization-if-its-value-is-null

取 ServletContext

在你的 Spring 控管 object 內寫上這個,就會自動注入了

protected ServletContext context;

@Autowired
public void setContext(ServletContext context) {
    this.context = context;
}

另一種方式,連 get、set 都省了,上網查是用 JAVA Reflection 做到的,不過細節沒去看就是了:

@Autowired
protected ServletContext context;

2014年5月13日 星期二

Android Service之IntentService

前言:

對於Android的Service,一直以來都是一知半解的狀況,因此前陣子有小段空擋時,花點時間在Android Develop Guide看看Service的說明,意外看到了一個玩意,IntentService,對它有點好奇,有Service就好了為何還會有它?因此對這個類別研究並測試了一下。

淺談Service

Android Service其實有點複雜,要談的話可能需要多點篇幅,因此這邊稍微講一下它的大概就好。
在我的認知裡,其實就把它想成一個沒有畫面的Activity就好了,因為它沒有畫面,所以它也不會因為User切換Activity而被中斷掉,因此是一個很適合做背景工作的App Component,如放音樂、下載檔案等工作,畢竟你不可能讓User只能在某個特定頁面才能正常下載,而且還跟User說,不准動手機,等我抓完你才可以動這類的話吧。
而Android提供了方便的框架,讓開發者去使用自己寫的Service,也就是透過context.startService(Intent service),就可以對Service送出request,讓它做某些工作。
只是因為要讓Service能完成更全面的工作,因此Android官方在設計此Components時將此它弄的很彈性,相對的複雜度也提高。在看官方API時可能會對於context.bindService()和context.startService()差別在哪為何要用有所困惑,又或者對為什麼service.onStartCommand()的回傳值要傳特定的參數回去,每個參數有何意義,在什麼狀況下我的參數會發揮什麼作用諸如此類的。
所以在看IntentService時,只需知道幾件和Service有關的事情:
  1. Service並不會另外開一條process出來執行你的code,除非你另外指定,否則它會和application執行在同一條process內,所以你如果在這邊執行大量運算的工作,還是會出現ANR的訊息。
  2. Service並不是另開一條新的thread來執行你的工作,它還是在main thread底下,所以不能直接透過他執行網路存取的工作。

Why IntentService

由於上面提到的Service特性以及它的複雜度的關係,因此官方提供了一個簡單使用的Service來給開發者使用,也就是今天要提到的IntentService,IntentService跟Service的差別在於,系統會給IntentService獨立的一條worker thread,讓它不會和activity共用thread,然後它預設會有個queue的機制,會保證一次只有一個request會被執行而已,也就是說不管你呼叫幾次context.startService(intent),在同一時間內只有單一個request會在service.onHandleIntent(intent)執行,而當沒有request在queue內時,該IntentService會自動銷毀,讓你不需要管理它的命週期。這邊整理了IntentService的一些特點:
  1. Easy to use,一樣透過context.startService(Intent service)就可以送出request。
  2. 不需要管Service的生命週期,只要把主要工作的code寫在onHandleIntent(Intent intent)就好了,code執行完畢如果沒有其他request則它會自己銷毀。
  3. 保證一次只有一個request會被處理,其餘的request會被block住。
  4. 由於他是另外開一條worker thread的關係,因此request被block住也不會影響main thread,簡單來說就是不會出現ANR的訊息。
  5. 也由於是另一條thread的關係,因此你可以直接在onHandleIntent()內寫網路相關的程式。

測試範例:

先在resource裡面定一個Button:
<Button 
    android:id="@+id/btn_is"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="intent service go"/>
接著在Activity內寫他的事件,只寫了這段code,發送request出去:
btn_is = (Button) findViewById(R.id.btn_is);
btn_is.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, 
                    IntentServiceImpl.class);
        startService(intent);
    }
});
而主要IntentService code,在onHandleIntent()裡面會故意將Thread sleep 2秒,用以觀察request bolcking的狀況,這邊需要注意的是,要覆寫建構子回傳一個IntentServiceImpl Service的名稱給worker thread,如下::
public class IntentServiceImpl extends IntentService {
    public static final String TAG = "mayer";
    private int i = 0;

    public IntentServiceImpl() {
        super("IntentServiceImpl");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate()");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent(), start work.");
        Log.d(TAG, "print start, init i = " + i);
        for(; i< 5; i++) {
            Log.d(TAG, "i = " + i);
        }
        try {
            TimeUnit.SECONDS.sleep(2l);
        } catch (InterruptedException e) {
            Log.e(TAG, "Error:", e);
        }
        Log.d(TAG, "onHandleIntent(), end work.");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy()");
    }
}
最後,跟一般Service一樣,需要在AndroidManifest.xml加上:
<service android:name="com.test.servicetest.IntentServiceImpl" />

範例結果:

測試時對按鈕連點三下,會輸出以下結果:
01-14 11:02:43.746: onCreate()
01-14 11:02:43.761: onHandleIntent(), start work.
01-14 11:02:43.761: print start, init i = 0
01-14 11:02:43.761: i = 0
01-14 11:02:43.761: i = 1
01-14 11:02:43.761: i = 2
01-14 11:02:43.761: i = 3
01-14 11:02:43.765: i = 4
01-14 11:02:45.765: onHandleIntent(), end work.
01-14 11:02:45.769: onHandleIntent(), start work.
01-14 11:02:45.773: print start, init i = 5
01-14 11:02:47.773: onHandleIntent(), end work.
01-14 11:02:47.777: onHandleIntent(), start work.
01-14 11:02:47.781: print start, init i = 5
01-14 11:02:49.785: onHandleIntent(), end work.
01-14 11:02:49.793: onDestroy()
由於點了按鈕三次,因此是送出了三個context.startService()的request出去。可以看到測試的Intent Service只會onCreate()一次,也就代表只會有一個Intent Service的實例被建立,接著他執行onHandleIntent()裡面的程式,由於會自己幫你block住其他的request,因此可以確保一次只有一個request會在這邊被執行,第一個request執行完之後第二個在進來執行,以此類推。當第三個request執行完時,由於沒有request了,因此這個Intent Service會自己呼叫onDestroy(),將自己銷毀。

結論:

所以這能幹麻呢?舉個例子,譬如你的Client要做圖片下載,而你又想限制一次只從Server上抓取一張圖片,一張抓完之後在抓下一張,這時你就可以用IntentService來做,當要抓圖片時把intent包一包,就直接呼叫startService(intent),然後就可以達成你的需求了!

Java 8 Lambda Expressions

Java 8從 2014/3/18 正式推出至今,也一個多月了,而Eclipse也在Java 8推出後宣布Eclipse 4.3.2將會支援Java8,因此花點時間測試了一下最有興趣的新特性,也就是本篇的標題,Lambda Expressions。

環境準備:

要用Eclipse來測試Java 8,需要先準備好Java 8相關的開發工具。
  1. Java Platform (JDK) 8u5,此為Java的開發工具,目前版本為8u5。
  2. eclipse 4.3.2(kepler SR2),Eclipse要4.3.2版才會支援Java 8,由於我是單純要測試而已,因此只抓標準版的Eclipse即可。
  3. Eclipse JDT升級,才能將專案的compiler設為1.8。安裝完Eclipse 4.3.2之後,再到:
    Help -> Eclipse Marketplace,搜尋Java 8,安裝Java 8 support for Eclipse Kepler SR2。

  4. 開一個新的project,並將project設定為 compiler 1.8。

做完以上的準備,可以開始測試Java 8了。

Lambda語法(Syntax)介紹:

Lambda語法架構為:
Argument ListArrow TokenBody
(int x, int y)->x+y
下面有幾個範例,來對Lambda有基本的認識:
輸入沒有任何參數,輸出2:
() -> 2;  
輸入兩個int參數,輸出兩個參數相加的結果:
(int x, int y) -> x+y;
輸入字串,在console顯示輸入字串:
(String s) -> System.out.println(s);

Functional Interfaces:

對於Lambda的語法有了了解之後,接著來看看Java 8如何使用它。Java 8引進的一個新的詞彙,Functional Interfaces,指的是只擁有單一抽象方法的介面。這詞彙並不是新概念,我們之前在開發時也常常會使用到類似的介面,如比較資料時會用到的Comparator介面,或是在跑執行緒時會用到的Runnable介面。
而只要是Functional Interfaces,就可以用Lambda的方式來實作!比如說Runnable介面,原本可以透過匿名函式的方式實作,現在可改用Lambda的方式來實作,如下:
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("run!");
    }
};

Runnable r2 = () -> System.out.println("run!"); 
再來看看Comparator介面,用匿名函式與用Lambda的實作方式:
Comparator<Integer> c1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1.compareTo(o2);
        }
    };

Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
透過Lambda來實作可以讓程式可以精簡很多,並增加程式的可讀性。
另外,Java 8也引進了新的標注(Annotation),來標記介面為Functional Interface,只要該介面有兩個以上的自定義方法,則編譯器會報錯,此標注讓團隊的其他開發人員不會在此介面上加上別的方法。
@FunctionalInterface
interface Ti {
    public void test(String s);
}

java.util.function:

Java的型別需要事先定義好,程式才能進行編譯。因此為了搭配Lambda語法,Java 8提供新的介面讓人使用,這些介面統一放在java.util.function底下,每個介面都代表一個方法,根據輸入參數與輸出參數,定了多個介面出來。以下介紹幾個基本類型的介面:
  • Consumer< T >:輸入參數類型為T,沒有任何輸出。
  • Function< T, R >:輸入參數類型為T,輸出參數類型為R。
  • Predicate < T >:輸入參數類型為T,輸出一個布林值。
  • Supplier< T >:沒有任何輸入,輸出參數類型為T。
  • UnaryOperator< T >:輸入參數類型為T,輸出參數類型為T。
舉例來說,今天我需要一個接受字串作為輸入而沒有任何輸出的方法,則我可以使用Consumer這個介面來操作,如下:
Consumer<String> c = (s) -> System.out.println(s);
c.accept("hello world!"); // output hello world!

ArrayList與java.util.function:

接著要談到於Java 8的Collections中,新增了幾個方法可以搭配Lambda使用。這邊會說明ArrayList的三個方法:
  • forEach(Consumer<? super E> action)
  • removeIf(Predicate<? super E> filter)
  • replaceAll(UnaryOperator operator)
首先介紹forEach方法,就跟for each語法一樣,只是這邊更直覺。傳入參數為Consumer介面,Consumer介面為單一輸入參數,沒有任何輸出參數,下面為範例:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);

// for each語法
for(Integer t : numbers) {
    System.out.println(t);
}

// Lambda語法
numbers.forEach((t) -> {
    System.out.println(t);
});
接著介紹removeIf方法,傳入參數為Predicate介面,此介面輸入參數類型為T,然後輸出一個布林值。也就是說,他會幫你繞行陣列的每個元素,元素會被丟到Predicate內執行,當執行結果為true,則該元素會被刪除,範例如下:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.removeIf((n) -> {
    if (n > 3) {
        return true;
    } else {
        return false;
    }
});
System.out.println(list); // output:[2, 3]
最後是replaceAll方法,傳入參數為UnaryOperator介面,此介面輸入參數類型和輸出參數類型是一樣的,這邊運作的方法是,回傳的參數會取代掉輸入的參數,範例如下:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.replaceAll((t) -> t + 1);
System.out.println(list); // output:[8, 7, 3, 4]

Method and Constructor References:

在Java 8,你可以將將方法的參考傳給變數,摘錄JSR335的內容:
Examples of method and constructor references:
System::getProperty
"abc"::length
String::length
super::toString
ArrayList::new
int[]::new
說明一下上面的例子:
  1. 如果你要需要靜態方法的參考,則可以使用 類別名稱::靜態方法名稱 來得到參考。
  2. 如果你是需要某個實例物件的方法他的參考,則可以用 實例變數::方法名稱 來得到參考。
  3. 如果你需要得到的是建構式,則可以使用 類別名稱::new 來得到參考。
下面為一個簡單的範例,透過Lambda實作一個介面,之後將介面的方法傳給變數,在用此變數呼叫該方法。
public static void main(String[] args) {
    TestInterface ti = (i) -> (i > 5) ? true : false;
    Predicate<Integer> p = ti::isGreaterThenFive;

    System.out.println(p.test(6));
}

@FunctionalInterface
interface TestInterface {
    public boolean isGreaterThenFive(Integer i);
}

參考:

Singleton的雙重檢查鎖與volatile

前言:

幾年前從踏進java開始,熟悉了物件導向之後,偶然從朋友那邊聽到了一個詞,Design Pattern,因此買了本歐萊禮的Headfirst Design Pattern來看,其中對於Singleton這Pattern一直有特別印象,因為這Pattern在系統設計時,放置系統設定檔案資料時很常用,也是一個好入門的Pattern,只是對於書上的雙重檢查鎖範例,一直沒有去理解它,趁最近比較有空時,花點時間去搞懂它。

Singleton雙重檢查鎖技巧:

先看一下書上的範例:
public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            synchronized(Singleton.class) {
                if(uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
這毫無疑問的是個Singleton的Pattern,建構子設為private,因此不能用new Singleton()的方式來建立instance,而其他物件要來取此物件的instance,也只能透過它對外開放的getInstance()來取得;為了講求效能,因此只有在uniqueInstance尚未初始化,也就是為null時,才會用synchronized來避免多執行緒造成new過多物件的問題發生,而如果它已經初始化過了,則直接回傳物件回去。
看起來是個很簡單很好懂的範例,但這範例裡面有個讓我困惑很久的地方就是,為什麼要在uniqueInstance這個field上加個volatile的關鍵字?

關鍵字volatile:

一開始我對valotitle的理解大概是這樣。
如果我今天宣告了一個宣告了一個long變數為volatile,如下
public volatile long value = 0l;
則其語意會相等於(注意是語意,不是底層真正的實作方式):
public long value = 0l;
public synchronized void set(long v) {
    value = v;
}

public synchronized long get() {
    return value;
}
也就是說,把一個有可能會被中斷的操作,變為不可中斷的操作,看起來似乎不加關鍵字volatile是對Singleton沒啥影響,那為何還要加呢?於是我花了一些時間尋找了幾篇相關討論的文章。

問題1,物件可能尚未初始化完成:

在這個網站The "Double-Checked Locking is Broken" Declaration提到,在不同系統下的compiles,可能會對於指令的處理有所不同,以下是網站裡提到的測試程式片段:
singletons[i].reference = new Singleton();
網站裡提到,Paul Jakubik發現把這程式碼片段程式在Symantec JIT執行過後,它會先對物件做memory allocate,然後才去執行建構子。
這會造成什麼問題呢?讓我們在回過頭來看Singleton的程式碼:
public static Singleton getInstance() {
    if(uniqueInstance == null) {
        synchronized(Singleton.class) {
            if(uniqueInstance == null) {
                uniqueInstance = new Singleton();
            }
        }
    }
    return uniqueInstance;
}
上面問題在於,一開始我們判斷物件是不是為null,null代表物件尚未初始化過,接著我們而在Symantec JIT的情況下,會先做memory allocate,在去執行建構子,因此可能有某個Thread正在對物件做初始化,做到一半被中斷了,而第二個物件呼叫了getInstance(),然後發現該物件不為null,因此馬上就回傳回去,因此它就得到了一個尚未初始化完成的物件,這有可能會造成系統錯誤。

問題2,編譯器優化:

接著另一本書提到的Effective Java (2nd Edition)裡面的 Item66 Synchronize access to shared mutable data 提到的範例,編譯器可能會對你的程式碼進行優化,如以下的程式碼:
public class HoistingTestClass {
    private static boolean stopRequested;

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!stopRequested)
                    i++;
            }
        });

        t.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            stopRequested = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
程式裡面,宣告一個Thread,並且先讓它執行,Thread裡面只做一件事,就是判斷stopRequested這個flag,如果為false,就一直執行下去,而主thread會在停止1秒之後,把stopRequested這個flag設為true。預期中,這程式應該是會跑一秒之後就停止了,不過實際卻是無窮迴圈。原因在於jvm對它做了優化處理。所以原本code是:
while(!stopRequested)
    i++;
在jvm執行時會被處理成:
if(!stopRequested)
    while(true)
        i++;
因此會和預期中的不一樣。而這問題要怎麼處理,只要將變數加上volatile則一切就正常了。
private static volatile boolean stopRequested;
為何用volatile可以解決呢?因為用了volatile,編譯器會已確保這個值的正確做為主要策略,而不會已效能為主來優化程式,因此它可以讓被設為volatile的field在任何Thread去讀取時,都會看到最新的狀態。

問題3,指令的重新排序:

在這篇文章 JSR 133 (Java Memory Model) FAQ 提到,執行程式時為了提高效率,因此compiler和processor可以對指令做重新排序的動作,比如說這段code:
class VolatileExample {
    int x = 0;
    boolean v = false;

    public void writer() {
        x = 42;
        v = true;
      }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
其中compile會認為在writer()內的這段code
public void writer() {
    x = 42;
    v = true;
}
和以下這段code,就語意上來說執行起來是一樣的,而為了效能考量,它有可能會對這段code進行重新排序然後執行,就會變成以下這段code:
public void writer() {
    v = true;
    x = 42;
}
而這如果發生在multithread的情況下,當有一條thread執行writer(),而另一條thread執行reader(),就會發生問題了,問題在於reader()內的程式:
public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
}
這邊會先判斷v是否為true,是true的話,根據原始碼的流程來看,如果這邊的v為true了,則代表writer()已經將x設為42了,但是由於編譯器對writer()做了重新排序,因此當reader()判斷v== ture之後繼續往下執行時,x的值有可能還是為0,這會造成問題。
所以這邊的code只要boolean v設為volatile,就能避免編譯器對writer ()這段code重新排序,如下:
volatile boolean v = false;

結論:

所以回到一開始的疑問,為何要在這Pattern的instance加上volatile關鍵字。
  • 為了讓Singleton instance初始化正常,不會初始化到一半就被別的thread取走了。
  • 確保編譯器不會幫你對Singleton的code進行優化,讓一切判斷如你預期般的執行。
  • 確保指令在執行時不會被重新排序,造成Singleton的值有異常。
值得注意的是,在JAVA 5以上才能用這種雙重鎖的技巧,原因volatile在JAVA 5之後變得比較完善(JSR-133實現)。。

參考:

  1. 深入理解Java内存模型(一)——基础
  2. 深入理解Java内存模型(四)——volatile
  3. The "Double-Checked Locking is Broken" Declaration
  4. Double-checked locking
  5. JSR 133 (Java Memory Model) FAQ

2014年3月16日 星期日

MQTT(七)DISCONNECT Message And Review

前言:

前篇介紹了UNSUBSCRIBE Message,此訊息個人是覺得會當某個使用者從整合了MQTT系統的服務登出時,需要對MQTT Broker取消註冊,本想說上一篇是這系列的最後一篇文章,在撰寫時卻突然想到,服務登出除了取消訂閱之外,還有個斷線訊息要送吧!因此又多了此篇,來介紹Client要對Server斷開連線所發送的訊息,DISCONNECT Message。

DISCONNECT Message

DISCONNECT Message為client發送給server,表示client將要斷掉它的TCP/IP連線。比起直接將連線斷掉的方式來斷線,使用此訊息來斷線會是種比較乾淨的做法。
如果Client當初在連線時有設置clean session flag,則該位client之前在MQTT Broker上面的資訊將會被清除掉。Server在關閉TCP/IP連線時,不會給client任何回應。

Fixed Header:

bit76543210
byte 1Message Type(14)DUP flagQoS levelRETAIN
1110xxxx
byte 2Remaining Length
00000000
上面為DISCONNECT Message裡面的fixed header,Message Type爲14,因此爲1110。其他DUP flag、QoS level以及RETAIN皆沒有使用到。
而DISCONNECT Message也沒有Variable header和Payload,因此其Remaining Length爲00000000。
DISCONNECT Message和上篇介紹的PINGREQ Message一樣,只有2 byte而已,也是非常輕量化的訊息。

Review:

最後依照架構MQTT服務與程式設計的觀點,將幾個訊息以及其需要注意的參數設定與特性,用圖片列出來。

CONNECT Message


在Client要對MQTT Broker建立連線時會用到的訊息,其中裡面需要設定:
  • Clean Session Flag:當Cleint斷線時,Broker該怎麼處理,是要幫他保留訊息,還是不管,等Client自己重新連線後在過來重新訂閱感興趣的主題。
  • Will:設定是否要使用MQTT的遺言功能(Last Will and Testament),要使用的話,則要將相關欄位與資料一併傳給Broker。
  • Username & Password:Broker會決定建立連線時需不需要帳號密碼,如果需要則Client要把帳號密碼放在CONNECT Message一併送給Broker。
  • Keep Aliver timer:代表此連線的過期時間,以秒為單位。也就是說如果Client和Broker在這段期間內雙方都沒有對發送任何訊息,則Broker要將該Client視為斷線。
  • Client Identifier:Client獨一無二的識別碼,可以帶該位使用者在這系統內所使用的帳號。
更多CONNECT Message的細節可以參考先前的文章MQTT(三)CONNECT Message。

PUBLISH Message


在Client要對Broker上的某個主題發佈訊息時會用到,其中裡面需要設定欄位與資料的有:
  • Qos level:此訊息傳送時所要用的QoS level為何,可以是0(只傳一次)、1(至少傳一次)或2(確定對方會收到一次)。
  • RETAIN:Broker上面的主題,是否要保留此訊息,當成是該主題上最新的一筆訊息。設定為RETAIN,則Broker會將此訊息保留起來,直到下一個RETAIN訊息發佈過來為止。當有新的Client對此主題訂閱,則Broker會把此主題上最新的RETAIN訊息傳給它。
  • Topic name:此次發佈訊息的主題,要注意的是這邊不能用萬用字元,主題名稱必須是完整的。
  • Payload:要發佈的訊息內容。
更多PUBLISH Message的細節可以參考先前的文章MQTT(四)PUBLISH Message。

SUBSCRIBE Message


在Client對Broker上的某個主題感興趣時,可以透過SUBSCRIBE Message來訂閱該主題,其中裡面需要設定欄位很單純,只有Topic name & Requested QoS level,也就是想要訂閱的主題,以及訂閱主題時期望的QoS level為何。
這邊可以一次傳送多個Topic name,透過SUBSCRIBE Message同時訂閱多個主題。或者是說利用萬用字元(#或+)的方式來一定訂閱多個主題。
關於SUBSCRIBE Message的細節可以參考先前的文章MQTT(五)SUBSCRIBE Message。

UNSUBSCRIBE Message


最後要談到的就是UNSUBSCRIBE Message,UNSUBSCRIBE Message也是跟SUBSCRIBE Message一樣,可以透過在一個Message裡面傳多個Topic的方式來一次取消訂閱多個主題。
另外有一點需要特別提出來,就是MQTT3.1版在規格裡面並沒有說到UNSUBSCRIBE Message可以使用萬用字元!但是在上面的圖為何要把他標出來呢,是因為我到網路上搜尋了相關資訊,發現到了這串位於MQTT官方的Google網上論壇這串討論串Topic Wildcards,提問者對此發表疑問,並且有人回答萬用字元是可以用在UNSUBSCRIBE Message的。
更多UNSUBSCRIBE Message的細節可以參考先前的文章MQTT(六)UNSUBSCRIBE Message And PINGREQ Message。

小結

本篇介紹了MQTT的DISCONNECT Message,在Client想斷開連線時,必須傳送此訊息給Server,讓Server可以清理不必要的資料,節省資源。
本篇也以系統架構和程式開發的觀點,將MQTT幾個重要的訊息和其參數列出來,以方便讓人理解在使用上需要注意些什麼。
以上MQTT的介紹系列,將會告一個段落,接下來會從實作面的部分來繼續延伸MQTT此主題。

參考:

MQTT(六)UNSUBSCRIBE Message And PINGREQ Message

前言:

上一篇提到用來訂閱主題的SUBSCRIBE Message,接著最後要介紹的兩個訊息,分別為取消訂閱用的UNSUBSCRIBE Message以及用來與Server維持連線的PINGREQ Message。

UNSUBSCRIBE Message

UNSUBSCRIBE Message為client發送給server,用來取消訂閱主題。

Fixed Header:

bit76543210
byte 1Message Type(10)DUP flagQoS levelRETAIN
1010001x
byte 2Remaining Length
上面為UNSUBSCRIBE Message裡面的fixed header,以下依依介紹它們的作用。
  • Message Type:每種類型的Message都要用到,要根據想用的訊息類型填上相對應的值,值可以參考先前貼的文章MQTT(二)Message Type and Flows。這邊由於是SUBSCRIBE Message的關係,因此會填上1010。
  • DUP flag:標記此訊息為重複(duplicate)的訊息,會用在PUBLISH、PUBREL、SUBSCRIBE、UNSUBSCRIBE上。當client或是server要重新傳送上述訊息時,則要將此flag設為1。此flag會適用於QoS大於0的訊息。
    此flag 只能當成一種提示,表示此則訊息可能在之前有收過了,不應該拿它來當成訊息重複的檢查flag。
  • QoS level:UNSUBSCRIBE Message使用QoS level 1來回應(acknowledge)不同的取消訂閱主題請求。
  • RETAIN:沒有使用到。
  • Remaining Length:此欄位記錄當前的Message,它的Variable header和Payload總共的長度為何,此欄位不一定就是1 byte,它是可變得,最多到4 byte。
接著讓我們繼續往下看,再來是第二個Herader,也就是Variable Header。

Variable Header:

由於UNSUBSCRIBE Message QoS level為1,因此其variable header會包含Message ID,用來確保訊息能夠正確傳送。
下面為SUBSCRIBE Message,variable header欄位範例:
FieldValue
Message ID10

Payload:

UNSUBSCRIBE Message的payload會包含一個主題名稱列表,代表該Client想要取消訂閱的主題。下表爲其payload範例,代表該Client想要取消訂閱兩個主題"a/b"、"c/d":
Topic name"a/b"
Topic name"c/d"

Response:

取消註冊成功,Server會回應一個UNSUBACK Message給client。

看完UNSUBACK Message的介紹之後,緊接著要介紹PINGREQ Message。

PINGREQ Message

PINGREQ Message為client發送給server,用來詢問Server是否還活著。Server收到來自Client的PINGREQ Message之後,會將該位Client的Keep Alive timer歸零重新計算,否則當Keep Alive timer時間到了,則Server會視此Client爲斷線狀態,Server將會關閉TCP socket連線(詳情可參考MQTT(三)CONNECT Message#Keep Alive timer)。因此這訊息反過來說,也算是Client用來回報自己狀況的給Server的訊息。

Fixed Header:

bit76543210
byte 1Message Type(12)DUP flagQoS levelRETAIN
1100xxxx
byte 2Remaining Length
00000000
上面為PINGREQ Message裡面的fixed header,以下依依介紹它們的作用。
  • Message Type:每種類型的Message都要用到,要根據想用的訊息類型填上相對應的值,值可以參考先前貼的文章MQTT(二)Message Type and Flows。這邊由於是PINGREQ Message的關係,因此會填上1100。
  • DUP flag:標記此訊息為重複(duplicate)的訊息,PINGREQ Message不會用到此欄位。
  • QoS level:定義該則訊息的QoS level為何,PINGREQ Message不會用到此欄位。
  • RETAIN:沒有使用到。
  • Remaining Length:此欄位記錄當前的Message,它的Variable header和Payload總共的長度為何,PINGREQ Message沒有Variable header,也沒有Payload,因此該欄位爲00000000。
由上面的Fixed header可得知,在MQTT的規範裡,PINGREQ Message是非常輕量化的,只有2 byte而已,因此PINGREQ Message非常適合用來作為機器間互相了解彼此間狀態的訊息。

Response:

Server收到PINGREQ Message,會回應一個PINGRES Message給client,告訴Client它還活著,PINGRES Message跟PINGREQ一樣,也是2 byte而已。

小結

本篇介紹了MQTT的UNSUBSCRIBE Message和PINGREQ Message。
UNSUBSCRIBE此訊息用於取消訂閱主題上,它需要注意的地方在於它可以一次同時取消多個主題。而另外介紹的PINGREQ訊息,此訊息可以讓Client運用最小的頻寬耗損,去告知Server它的狀態,Server也可以反過來利用此訊息得知Client的狀況,避免彼此間裝置上的資源造成不必要的消耗。

參考: