2017/02/20

Spark tutorial

要使用 Spark 之前,一般會先遇到 scala 這個語言的熟悉度的問題,當有了一定的語言程度後,再來就是 scala IDE 的選擇,目前的狀況,還是IDEA 會比 scala IDE for Eclipse 好用。接下來就是下載跟安裝 spark,然後進行 WordCount 的範例練習,以下記錄怎麼安裝與設定 stand alone 的 spark 開發環境。

要下載哪一個 spark 套件

當我們連結到 Download Apache Spark 時,首先遇到的問題,就是要下載哪一個 spark release 套件。

基本的原則如下:

如果要直接下載已經編譯好的 binary 套件,我們可以根據 Hadoop 的版本決定要下載哪一個,但如果像我們一樣,不打算安裝 Hadoop 就直接測試,就直接選最新版的 spark-1.6.1-bin-hadoop2.6.tgz 就好了,下載後解壓縮,馬上就可以使用 spark-shell,或直接取得 all-in-one 的 spark-assembly-1.6.1-hadoop2.6.0.jar 套件。

如果我們要編譯 source code,就下載預設的 1.6.1(Mar 09 2016) spark release,Package type 選擇 Source Code:spark-1.6.1.tgz

由於目前 spark 預設是使用 scala 2.10 版,使用預先編譯的 spark 就必須要使用 scala 2.10 版,如果像要改成 2.11,就一定要自己重新編譯 spark,目前 spark 的 JDBC component 還不支援 scala 2.11。

Building for Scala 2.11 有兩行指令說明如何將 spark 由 2.10 調整為 2.11,我們同時把 hadoop 版本改為 2.6。

./dev/change-scala-version.sh 2.11
mvn -Pyarn -Phadoop-2.6 -Dscala-2.11 -DskipTests clean package

編譯 spark 要花的時間很久,以我現在的環境花了 40 分鐘。

[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 39:33 min
[INFO] Finished at: 2016-04-29T09:23:03+08:00
[INFO] Final Memory: 452M/2703M

也可以使用 sbt 來編譯 spark,編譯後會得到 spark-assembly 的 jar。

sbt/sbt assembly

如果要修改 spark souce code,可以啟用增量編譯模式,避免每一次修改都要花很久的時間重新編譯。

export SPARK_PREPEND_CLASSES=true
sbt/sbt compile

unset SPARK_PREPEND_CLASSES

在 compile 前面加上 ~ 可以避免每一次都重開一次新的 sbt console

sbt/sbt ~ compile

可以用 sbt 或是 mvn 指令查閱 dependency map

sbt/sbt dependency-tree

mvn -DskipTests install
mvb dependency:tree

如果要設定 spark source 的開發環境,可以用以下的指令產生 IDEA project file

git clone https://github.com/apache/spark
sbt/sbt gen-idea

Spark 開發環境 in IDEA

  1. 在 IDEA 建立新的 scala project: sparktest

  2. 在 project 中建立一個 lib 目錄,把 spark-assembly-1.6.1-hadoop2.6.0.jar 放在那個目錄中

  3. 在 File -> Project Structure -> Libraries 點 "+",然後把 lib 目錄加入 project 中

  4. 取得一個文字檔的測試資料 pg5000.txt ,將檔案放在新建立的 data 目錄中

  5. 將 RunWordCount.scala 放在 src 目錄中,程式會計算 pg5000.txt 裡面每一個字出現的數量

    import org.apache.log4j.Logger
    import org.apache.log4j.Level
    import org.apache.spark.{ SparkConf, SparkContext }
    import org.apache.spark.rdd.RDD
    
    object RunWordCount {
      def main(args: Array[String]): Unit = {
    
        // 以這兩行設定不顯示 spark 內部的訊息
        Logger.getLogger("org").setLevel(Level.OFF)
        System.setProperty("spark.ui.showConsoleProgress", "false")
    
        // 清除 output folder
        FileUtils.deleteDirectory(new File("data/output"))
    
        println("執行RunWordCount")
    
        // 設定 application 提交到 MASTER 指向的 cluster 或是 local 執行的模式
        // local[4] 代表是在本地以 四核心的 CPU 執行
        val sc = new SparkContext(new SparkConf().setAppName("wordCount").setMaster("local[4]"))
    
        println("讀取文字檔...")
        val textFile = sc.textFile("data/pg5000.txt") 
    
        println("開始建立RDD...")
        // flapMap 是取出文字檔的每一行資料,並以 " " 進行 split,分成一個一個的 word
        // map 是將每一個 word 轉換成 (word, 1) 的 tuple
        // reduceByKey 會根據 word 這個 key,將後面的 1 加總起來,就會得到 (word, 數量) 的結果
        val countsRDD = textFile.flatMap(line => line.split(" "))
          .map(word => (word, 1))
          .reduceByKey(_ + _) 
    
        println("儲存結果至文字檔...")
        try {
          countsRDD.saveAsTextFile("data/output") 
          println("存檔成功")
        } catch {
          case e: Exception => println("輸出目錄已經存在,請先刪除原有目錄");
        }
    
      }
    }
  6. 我們可以直接在 IDEA 就執行這個測試程式

    執行RunWordCount
    Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
    16/04/29 16:28:50 INFO Slf4jLogger: Slf4jLogger started
    16/04/29 16:28:50 INFO Remoting: Starting remoting
    16/04/29 16:28:50 INFO Remoting: Remoting started; listening on addresses :[akka.tcp://sparkDriverActorSystem@192.168.1.151:56205]
    讀取文字檔...
    開始建立RDD...
    儲存結果至文字檔...
    存檔成功
    
    Process finished with exit code 0
  7. 最後產生的結果有三個檔案,其中 part-00000 及 part-00001 裡面存了每一個 word 的發生次數

    _SUCCESS
    part-00000
    part-00001

會產生兩個檔案的原因是因為,spark 本身是平行運算的工具,所以會自動產生多個 partitions。

如果需要將結果整合成一個檔案,就必須使用 coalesce,在程式的最後面,用 countsRDD.coalesce(1).saveAsTextFile 將結果輸出到新目錄,也會得到一個檔案的結果。

try {
      countsRDD.coalesce(1).saveAsTextFile("data/output2")
      println("存檔成功")
    } catch {
      case e: Exception => println("輸出目錄已經存在,請先刪除原有目錄");
    }

匯出程式

  1. 在 IDEA 選擇 "File" -> "Project Structure" -> "Artifact"

  2. 點擊 "+" -> "JAR" -> "From modules with dependencies"

  3. Main Class 填成 "RunWordCount",輸出目錄的最後面改為 "out"

  4. 選擇 "Build" -> "Build Artifacts",就能在 out 目錄取得 sparktest.jar 檔

  5. 這樣就能在另一台機器執行 sparktest

java -jar sparktest.jar

References

HADOOP+SPARK大數據巨量分析與機器學習整合開發實戰

Spark大資料分析實戰

2017/02/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/02/13

scopt: command line parsing library in Scala

在 Java 要製作一個 command line 工具可以使用 Apache Commons cli,不過在 scala,有另一個更簡潔的 library: scopt,可以幫助我們製作 cli 程式。

libraryDependencies

根據 scopt github 的說明,我們應該在 build.sbt 中加上這樣的 libraryDependencies 宣告設定

libraryDependencies += "com.github.scopt" %% "scopt" % "3.5.0"

但我們使用起來覺得有點問題,搜尋了 maven Group: com.github.scopt,看起來這個 library 有針對 scala 的版本提供不同的 library,因為我們是使用 scala 2.11.8,所以就將 libraryDependencies 改成以下這樣

"com.github.scopt" % "scopt_2.11" % "3.5.0",

Config

使用 scopt 之前,要先定義一個用來存放 cli parsing 結果的 case class: Config,我們是要做一個 License File 的產生工具,所以 Config 裡面存的都是 license 需要的資料。

  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

Parser

接下來是使用 Config 產生 OptionParser,Parser 中是以第一個參數 "mode" 作為不同指令的判斷,我們提供了四個指令:key, lic, dec, --getmid, --ver,另外還有一個基本的 --help,每一個指令都有一個縮寫。

我們可以先看 help 列印出來的結果,最前面的 Usage 是這個程式的使用方式,然後有兩個基本的 --ver 及 --getmid 方法。

接下來是 key, lic, dec 這三個獨立指令的說明,每一個指令都有相關的參數,最後一行是 --help 列印 help 頁面的部分。

[info] Running license.LicenseBuilder -h
[info] License Builder 0.1
[info] Usage: license.LicenseBuilder [key|lic|dec] [options] <args>...
[info]
[info]   -v, --ver                Prints the version number.
[info]   -i, --getmid             Prints the machine id.
[info] Command: key keyfile
[info]   generate RSA key file
[info]   keyfile                  gen key files with key filename prefix
[info] Command: lic [options]
[info]   generate license file
[info]   -k, --prikeyfile <value>
[info]                            private key file prefix
[info]   -l, --lictype <value>    Evaluation/Standard/Enterprise
[info]   -p, --product <value>    product name, ex: kokome
[info]   -e, --version <value>    product version number, ex: 3.0.0
[info]   -n, --name <value>       licensed name, ex: kokome
[info]   -o, --company <value>    licensed company name, ex: maxkit
[info]   -c, --copies <value>     licensed number of users, ex: 5
[info]   -m, --mid <value>        machine id
[info]   -v, --validfrom <value>  licensed valid from date ex: 2016/01/01
[info]   -g, --goodthru <value>   licensed good thru date ex: 2016/12/31
[info] Command: dec keyfile
[info]   decode maxkit.lic
[info]   keyfile                  decode maxkit.lic with key filename prefix
[info]   -h, --help               prints this usage text

看了 help 的說明後,再去看 OptionParser 的寫法,就比較能清楚地分辨不同指令區塊的部分。

val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }

完整的程式

package license

import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.FileUtils
import play.api.Logger
import utils.StringUtil

object LicenseBuilder extends App {
  val ver = "0.1"

  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

  def key(keyfile: String) = {
    println(s"generate key pairs with filename prefix ${keyfile}")
  }

  def getmid() = {
    val mid = LicenseId.getLicenseId
    println(s"mid = ${mid}")
  }

  def dec(keyfile:String) = {
    println(s"decode license maxkit.lic with ${keyfile}.prikey.dat")

  }

  def lic(keyfile:String, lictype:String,
          product:String, version:String,
          name:String, company:String,
          copies:Int, mid:String,
          validfrom:String, goodthru:String) = {

    println(s"gen license with ${keyfile}.prikey.dat, lictype=${lictype}," +
      s"product=${product}, version=${version}, name=${name}, company=${company}, " +
      s"copies=${copies}, mid=${mid}, validfrom=${validfrom}, goodthru=${goodthru}")
      
  }

  val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }
}

Reference

scala 命令行解析

2017/02/06

OpenJDK

Oracle JDK 長久以來並沒有被追討授權費用的問題,但因為 JDK 本來就是以 BCL 授權,並不是整個 JDK 都是免費使用的,再加上Oracle 開始追討 Java 授權費,企業客戶頭痛,所以要開始注意這個問題。Oracle 取締未經適當授權的 Java 用戶 提供了如何安全地使用 Oracle JDK 的一些 hint,不過最根本的方法就是換成 OpenJDK。

OpenJDK 是以 GPL with Classpath Exception 授權,classpath exception 就是可以在 proprietary 軟體中使用 OpenJDK 的意思。

OpenJDK 8 已經跟 Oracle JDK 沒有什麼差異,在 Linux Server 中,都已經可以很快速就將 JDK 轉換到 OpenJDK 上面,不過 windows 跟 MacOS 就麻煩了一些,但基本上後面這兩個 OS 都是開發環境,只是下載使用,沒有散佈,繼續用 Oracle JDK 應該也可以。

CentOS

ref: CentOS7 使用yum命令安装Java SDK

$ yum search java | grep -i --color JDK

java-1.8.0-openjdk.x86_64 : OpenJDK Runtime Environment
java-1.8.0-openjdk-debug.x86_64 : OpenJDK Runtime Environment with full debug on
java-1.8.0-openjdk-demo.x86_64 : OpenJDK Demos
java-1.8.0-openjdk-demo-debug.x86_64 : OpenJDK Demos with full debug on
java-1.8.0-openjdk-devel.x86_64 : OpenJDK Development Environment
java-1.8.0-openjdk-devel-debug.x86_64 : OpenJDK Development Environment with
java-1.8.0-openjdk-headless.x86_64 : OpenJDK Runtime Environment
java-1.8.0-openjdk-headless-debug.x86_64 : OpenJDK Runtime Environment with full
java-1.8.0-openjdk-javadoc.noarch : OpenJDK API Documentation
java-1.8.0-openjdk-javadoc-debug.noarch : OpenJDK API Documentation for packages
java-1.8.0-openjdk-src.x86_64 : OpenJDK Source Bundle
java-1.8.0-openjdk-src-debug.x86_64 : OpenJDK Source Bundle for packages with

openjdk 的安裝路徑 /usr/lib/jvm/

yum install java-1.8.0-openjdk  java-1.8.0-openjdk-devel

以 alternatives 調整執行檔的目標

alternatives --config java
alternatives --config javac
alternatives --config javadoc
alternatives --config javah
alternatives --config javap

設定環境變數

vi /etc/profile

export JAVA_HOME=/usr/lib/jvm/java-openjdk
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

Debian

ref: How to download and install prebuilt OpenJDK packages

apt-get update
apt-get install openjdk-8-jdk

openjdk8 的路徑是 /usr/lib/jvm/java-8-openjdk-amd64

update-alternatives --display java
update-alternatives --display javac
update-alternatives --display javadoc
update-alternatives --display javah
update-alternatives --display javap

java -version

設定環境變數

vi /etc/profile

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

openjdk for windows

Redhat Develper 提供了 windows 的 openjdk binary installer OpenJDK now available for Windows

openjdk for macos

build OpenJDKs at home on Linux and OSX

How to build and package OpenJDK 8 on OSX

https://www.zhihu.com/question/40816585 安装Homebrew然后在终端sudo brew install openjdk

2017/02/04

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;