門外漢的 Java 輕量開發環境


在 Raspberry Pi 2 上設置虛擬環境實在方便,最近要是有什麼小東西想測一下,只要插上 USB,SSH 進去就可以開始測了,於是我才會想,那麼在上面裝個 Java 開發環境,即使許多人認為這不是 Docker 的用途,不過,既然 Docker 可以用來虛擬一個環境,而開發環境也是環境,那麼用 Docker 建開發環境,也是可以的吧!

實際上,在〈門外漢的 Raspberry Pi 2 運行 Docker〉的最後,我安裝了 Java 與 Gradle,就是為了這個目的而做的初步實驗。

使用 Docker 設置開發環境的考量

關於在 Docker 中設置開發環境,其實我找過很多種做法,最大的問題是 IDE,做法之一是在虛擬環境中安裝桌面環境,然後以遠端桌面連進去之後安裝 IDE,另一個做法是在 HOST 安裝桌面環境與 IDE,在 HOST 以 IDE 撰寫程式,然後透過 VOLUME 掛載到容器中執行。

不過,這對 Raspberry Pi 2 來說太重了,雖然 Raspberry Pi 2 效能比較好,跑跑桌面環境還可以,但是加上 IDE 就會略顯遲頓了,因此最後我不採用這類方案。

倒是在找資料的過程中找到篇〈Running GUI apps with Docker〉很有意思,作者不是在 Docker 中直接運行桌面環境與 GUI,而是與容器共享 X11 socket,直接執行容器中的 GUI 並顯示在 HOST 之中,作者也試出了 NetBeans 的做法,我是沒有試,有興趣的話你可以試著做做看。

附帶一提的是,NetBeans 現在也可以將專案透過 SSH 送到遠端,執行之後傳回給過至 HOST,這也許是個可行的方案。

後來我仔細想想,我最想要的其實是 IDE 裏頭的 auto complete 功能,用來對付 Java 撰寫程式時,那些又臭又長的名稱,因此,我開始轉而尋求 vim 中可以有 auto complete 方案。

最常被推薦的是 Eclim,因為它直接利用了 Eclipse 作為後盾,不過,使用 apt-get 可以安裝的 Eclipse 不是最新的,而 Raspberry Pi 2 運行的 Docker 映像檔中,我還沒找到方法可以運行最新版的 Eclipse。

再來可以考量使用的是 YouCompleteMejavacomplete,YouCompleteMe 看起來不錯,有機會建置其他語言輕量環境時,我會想使用它。

不過最後我選擇使用 javacomplete,一來設置比較方便,二來是因為,javacomplete 是用一隻 Java 程式來做 Reflection 以取得 auto complete 時需要的資訊,因此如果你有其他的程式庫來源,只要設置 CLASSPATH,就可以讓 javacomplete 用來進行 auto complete。

建構 Java 程式時就採用 Gradle,它可以替代原本 IDE 中的一些需求,像是程式庫管理、建構、測試、部署等,至於開新專案的話,我選擇使用 gradle-templates,而版本控制系統則使用 Git。

因此,最後我的輕量 Java 開發環境就是:

  • Raspberry Pi 2
  • HypriotOS
  • Docker
  • Git
  • Oracle JDK8
  • Gradle
  • Vim
  • javacomplete
  • gradle-templates

說到要怎麼完整配置這樣的環境,在 Raspberry Pi 2 安裝 HypriotOS 的安裝方式,可以直接參考〈在 Raspberry Pi 2 運行 Docker〉,裝完就有 Docker 可以用了,至於其他部份,其實只要看這篇提供的 Dockerfile 就可以明瞭了,因此,接下來的說明,只記錄一些比較要注意的地方。

更新 Docker

在 Raspberry Pi 2 運行 Docker〉中設置的 Docker 版本是 1.5,如果想要更新至新版本,可以至 HypriotOS 的下載區 下載最新的 package 進行更新,以更新至 Docker 1.8.1 來說,可以在 HypriotOS 中執行以下指令:

$ wget http://downloads.hypriot.com/docker-hypriot_1.8.1-1_armhf.deb
$ dpkg -i docker-hypriot_1.8.1-1_armhf.deb
$ rm docker-hypriot_1.8.1-1_armhf.deb

這樣你的 Docker 就會是 1.8.1 的版本了:

更新至 Docker 1.8.1

使用 apt-get 安裝 Oracle JDK 8

在 Raspberry Pi 2 運行 Docker〉中安裝 Oracle JDK 8 時,是自行下載 jdk-8u60-linux-arm32-vfp-hflt.gz,實際上,若要在 resin/rpi-raspbian 映像檔中利用 apt-get 來安裝 Oracle JDK 8,也可以使用以下的指令(參考自〈Installing Java 8.x and Tomcat 8.x on Debian Jessie (and RedHat too)〉):

$ echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" > /etc/apt/sources.list.d/webupd8team-java.list
$ echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" >> /etc/apt/sources.list.d/webupd8team-java.list
$ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys EEA14886
$ apt-get update
$ apt-get install oracle-java8-installer

不過,這一次我參考了 Wei Lin 的做法,改使用 Ubuntu 14.04 映像檔,來源是 armv7/armhf-ubuntu,安裝的方式列出在稍後的 Dockerfile 中,因為之前用了一陣子 Ubuntu,比較習慣這個版本。

設置 javacomplete

要設置 javacompete,你要先裝好 JDK 與 vim,然後下載 javacomplete,將 zip 中的 doc、autoload 兩個目錄,解壓縮至 /root/.vim 之中,然後在 /root/.vimrc 中編輯以下內容:

setlocal omnifunc=javacomplete#Complete
inoremap <buffer> . .<C-X><C-O><C-P>

其實第一行設置好之後,在鍵入 . 之後,就可以按下 Ctrl + X 後加上 Ctrl + O 來進行 auto complete,加上第二行的話,就可以鍵入 . 就直接進行 auto complete 了,這樣比較符合在 IDE 中的習慣,不過缺點就是遇到 . 就會試圖 auto complete,如果你不需要每一次都自動提示,就將第二行拿掉。

javacomplete

第一次編輯 Java 程式碼要進行 auto complete 時,會自動編譯 /root/.vim/autoload/Reflection.java,編譯好之後會將 Reflection.class 放到 /root 中。

設置 gradle-template

gradle-template 是可以使用 Gradle 來建立指定類型的專案目錄,基本的設置方式,就是在 build.gradle 中撰寫以下內容:

buildscript {
    repositories {
        maven {
            url 'http://dl.bintray.com/cjstehno/public'
        }
    }
    dependencies {
        classpath 'gradle-templates:gradle-templates:1.5'
    }
}

apply plugin:'templates'

如果要建立一個基本的 Java 專案,就只要下 gradle createJavaProject 就可以了,想要知道它支援哪些專案類型,可以執行 gradle tasks

gradle-template

完整的 Dockefile

安裝的細節,就直接看 Dockerfile 吧!我建立了一個 data 目錄並設置為 VOLUME,稍後會將專案建在這裏頭,如果你懶得自己建置的話,也可以直接 pull 我的 caterpillar/rpi-dev-java

FROM armv7/armhf-ubuntu:14.04
MAINTAINER Justin Lin <caterpillar@openhome.cc>

ENV TERM linux

# Basic tools
RUN apt-get -qq update && \
    apt-get -qqy install wget && \
    apt-get -qqy install vim && \
    apt-get -qqy install unzip && \
    apt-get -qqy install git

# Oracle Java 8
RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
    apt-get update && \
    apt-get install -y software-properties-common && \
    add-apt-repository -y ppa:webupd8team/java && \
    apt-get update && \
    apt-get install -y oracle-java8-installer

# Upgrade and clean up
RUN apt-get dist-upgrade -y && \
    apt-get remove -y software-properties-common && \
    apt-get autoremove -y && \
    apt-get autoclean -y && \
    apt-get clean -y && \
    rm -rf /var/lib/apt/lists/* && \
    rm -rf /var/cache/oracle-jdk8-installer

ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

# Gradle
RUN wget https://downloads.gradle.org/distributions/gradle-2.6-bin.zip && \
    unzip gradle-2.6-bin.zip -d /opt && \
    rm gradle-2.6-bin.zip

ENV GRADLE_HOME /opt/gradle-2.6

# PATH
ENV PATH $PATH:$JAVA_HOME/bin:$GRADLE_HOME/bin

# javacomplete
RUN mkdir /root/.vim && \
    wget http://www.vim.org/scripts/download_script.php?src_id=14914 -O javacomplete.zip && \
    unzip javacomplete.zip -d /root/.vim && \
    rm javacomplete.zip && \
    echo "setlocal omnifunc=javacomplete#Complete" >> /root/.vimrc && \
    echo "inoremap <buffer> . .<C-X><C-O><C-P>" >> /root/.vimrc

# gradle-templates

RUN mkdir /data && \
    echo "buildscript {" >> /data/build.gradle && \
    echo "    repositories {" >> /data/build.gradle && \
    echo "        maven {" >> /data/build.gradle && \
    echo "            url 'http://dl.bintray.com/cjstehno/public'" >> /data/build.gradle && \
    echo "        }" >> /data/build.gradle && \
    echo "    }" >> /data/build.gradle && \
    echo "    dependencies {" >> /data/build.gradle && \
    echo "        classpath 'gradle-templates:gradle-templates:1.5'" >> /data/build.gradle && \
    echo "    }" >> /data/build.gradle && \
    echo "}" >> /data/build.gradle && \
    echo "apply plugin:'templates'" >> /data/build.gradle

VOLUME /data

WORKDIR /data

# Define default command.
CMD ["bash"]

Hello, World

該怎麼使用這個設置好的環境呢?直接來看動畫吧(Refresh 一下網頁就可以重頭看了…XD)!

Hello, World

若要從容器中取得專案,可以執行 docker ps -a 查看容器 id,然後使用 docker inspect 檢視容器資訊,看看 VOLUME 掛載到哪邊去:

docker inspect

在上圖中可以看到 Mounts,/data 被掛載到 /var/lib/docker/volumes/ac7129ddc73ec6a5c1c842595d4c9aa2887340ca2126339c761a8dcdebf16355/_data 中,你可以在其中找到建立的 JavaPrj:

VOLUME

當然,如果願意,你可以再自行安裝更多的 vim 外掛,像是 vim-autoclosenerdtreectrlp 等,讓這個環境更加適合開發時的需求。

最後再提一下 javacomplete,想要它能提示的類別來源,可以設置在 CLASSPATH 之中,如果你的 JAR 等是放置在同一個資料夾中,這比較沒有問題,Java SE 6 之後,可以用 lib* 這樣的語法來設置 CLASSPATH,這樣 lib 目錄中的 JAR 就都會被設置。

不過,像 Gradle 這個就比較麻煩,例如在容器中,它會將 JAR 下載到 /root/.gradle/caches 之中,然後依不同的程式庫與版本,給予不同的目錄來存放 JAR,手動設置這些 JAR 來源不方便,你可以寫個 script 來解決這個問題,例如:

CLASSPATH=.; export CLASSPATH=$CLASSPATH$(find "JAR 的目錄" -name '*.jar' -type f -printf ':%p\n' | sort -u | tr -d '\n'); echo $CLASSPATH

這樣,它就會在「JAR 的目錄」中遞迴地找出 JAR,並設置為 CLASSPATH 的一部份。