程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 在異構UNIX系統間可靠的遷移Java應用

在異構UNIX系統間可靠的遷移Java應用

編輯:關於JAVA

引言

使用 Java Native Interface (JNI) 編寫 Java™ 應用程序可能充滿挑戰。C/C++ 代碼比較復雜,並且維護其構建系統也是一項非常煩瑣的任務。當基礎平台的數量增加時, 如果不經過精心設計,整個構建系統可能會變得一團糟。有一種選擇是,為每種平台構造一 種單獨的構建系統,盡管從軟件工程的角度來說,這樣做不是很合適並且可能帶來很大的麻 煩。

要確保能夠移植到許多異類 UNIX® 平台,那麼構建系統必須是可插入的。通過集成 Apache Ant、GNU Compiler Collection (GCC)、Make 和 Subversion,您可以創建一個功能 強大的構建系統。應該將構建系統組件化,這樣一來,為新的組件添加或刪除新的平台支持 就非常簡單。它還應該使用各種技術以便自動地檢測當前的平台,然後調用相應平台特定的 構建組件。本文介紹了如何構造這樣的系統,您將學習到下面的內容:

集成 Ant、GCC、Make 和 Subversion

設計可插入的和可移植的構建系統及其源代碼布局

在設計本地構建組件時,描述值得注意的 GCC 標志

自動檢測當前構建環境,並設計任務依賴關系

集成 Ant、GCC、Make 和 Subversion

本文中的構建環境涉及到許多開放源代碼工具(請參見參考資料部分以進行下載):

Ant

Java Development Kit (JDK)

GCC

Make

Subversion

圖 1 中顯示了這個構建環境。Ant 主要負責:

檢索代碼到本地工作位置

調用 JDK 編譯 Java 代碼

調用 Make 構建本地代碼

圖 1. 構建環境

獲得源代碼並控制構建版本

可以使用 Ant 和 Subversion 的組合來檢索源代碼,並同時控制構建版本。在開發周期 中,構建的工作將重復很多次。如果每次都需要手動地指定一個構建版本,那將是非常麻煩 的。清單 1 中的構建腳本用於提取 SVN 版本編號,並使用它作為構建編號。

清單 1. Ant 任務:管理構建版本

1  <target name="svn-prop">
2   <exec executable="svn">
3    <arg value="--non-interactive"/>
4    <arg value="info"/>
5    <redirector outputproperty="svn.revision">
6     <outputfilterchain>
7      <linecontains>
8       <contains value="Revision:"/>
9      </linecontains>
10      <tokenfilter>
11       <replacestring from="Revision:" to=""/>
12      </tokenfilter>
13     </outputfilterchain>
14    </redirector>
15   </exec>
16   <!--<echo>${svn.revision}</echo>-->
17  </target>

在執行這項任務的時候,會將 SVN 版本編號提取到 Ant 的 svn.revision 屬性中。稍後 可以使用這個屬性作為構建編號。這個任務只能夠處理英文版本 SVN,因為 linecontains 篩選器僅檢查該行內容中是否包含 Revision:。

另一項任務是從 SVN 存儲庫中檢索源代碼。下面的清單 2、3 和 4 顯示了檢索源代碼的 任務。

清單 2. Ant 任務:檢查該存儲庫是否已經簽出

1  <target name="svn-check">
2   <available type="dir" property="svn.check" file="${local.url}">
3   <echo>${svn.check}</echo>
4  </target>

清單 3. Ant 任務:檢索源代碼

1  <target name="svn-checkout" unless="svn.check" depends="svn-check">
2   <exec executable="svn">
3    <arg value="co" />
4    <arg value="${p.root}/${local.url}" />
5   </exec>
6  </target>

清單 4. Ant 任務:更新源代碼

1  <target name="svn-update" depends="svn- checkout">
2   <exec executable="svn">
3    <arg value="up" />
4    <arg value="${local.url}" />
5   </exec>
6  </target>

您可以使用這三種協作的目標來檢索源代碼。svn-check 目標用於測試該存儲庫是否已經 簽出。如果這個目錄主干存在,那麼將屬性 svn.check 設置為 true。下一個目標是 svn- checkout,僅在沒有簽出存儲庫的情況下才執行它,可以使用 unless="svn.check。在 簽出之後,每次調用該腳本的時候都會執行 svn-update 目標。請注意,測試條件 unless 僅檢查是否設置了某個屬性。而該屬性的取值不會產生任何效果。

構建 Java 類

使用 Ant 可以很容易地構建 Java 組件。這個部分提供了構建 Hello 程序的一個簡單示 例。

清單 5 和清單 6 顯示了輸出 Hello 的 Java 文件和 Ant 構建腳本。

清單 5. 輸出 Hello 的 Java 文件

1  package hello;
2  public class Hello {
3   public static void main(String[] args) {
4    System.out.println("Greeting from build system - java component! ");
5   }
6  }

清單 6. 用於 Java 源文件的 Ant 構建文件

1  <?xml version="1.0"?>
2  <project default="deploy" basedir=".">
3   <echo message="pulling in property files" />
4   <import file="properties.xml" />
5   <echo message="compile java greeting ${path.dist.classes}" />
  
6   <target name="compile">
7    <mkdir dir="${path.dist.classes}"/>
8    <javac destdir="${path.dist.classes}" srcdir="${path.src}"/>
9   </target>
10  <target name="deploy" depends="compile">
11   <jar destfile="${path.dist}/HelloWorld.jar" basedir="${path.dist.classes}">
12   <manifest>
13    <attribute name="Main-Class" value="hello.Hello"/>
14   </manifest>
15   </jar>
16   </target>
17   <target name="clean">
18   <delete dir="build"/>
19   </target>
20  </project>

從 property.xml 文件中提取了一些通用屬性的定義,如清單 7 所示。稍後還將反復地 使用這個文件。

清單 7. 構建屬性文件

1 <project name="Top-Level property definitions" default="echo" basedir=".">
2  <description>
3  Ant file of common properties to be imported by other ant files
4  </description>
5  <property name="path.dist" value="build" />
6  <property name="path.dist.classes" value="build/classes" />
7  <property name="path.src" value="src" />
8  <target name="echo">
9   <echo>build properties</echo>
10  </target>
11 </project>

這個構建組件中使用了一種常用的技術,即將變量(屬性定義)與內置的變量(構建步驟 )分離。

構建 JNI 組件

可以使用 Ant 和 JDK 的組合對 Java 代碼進行編譯和部署,同樣地,可以使用 Ant、 Make、GCC 和 JDK 的組合對 JNI 組件進行編譯和部署。這個過程比較復雜,因為它需要下 列內容:

三種編譯器:javah、javac 和 GCC

兩種構建工具:Ant 和 Make

下面的清單 8、9、10、11 和 12 顯示了包括 Java 代碼、本地代碼、一個 Ant 腳本和 一個 Makefile 的 JNI 構建組件。這個 JNI 組件還實現了輸出 Hello 的任務。

清單 8. 在本地方法中輸出 Hello

1  package hello;
2  public class Test {
3   public static native void hello();
4   static {
5    System.loadLibrary("libhello");
6   }
7   /**
8    * @param args
9    */
10   public static void main(String[] args) {
11     hello();
12   }
13  }

清單 9. 從 Hello.clas 編譯得到的 Header 文件

1  /* DO NOT EDIT THIS FILE - it is machine generated */
2  #include <jni.h>
3  /* Header for class Test */
4  #ifndef _Included_Test
5  #define _Included_Test
6  #ifdef __cplusplus
7  extern "C" {
8  #endif
9  /*
10   * Class:   Hello
11   * Method:  hello
12   * Signature: ()V
13   */
14  JNIEXPORT void JNICALL Java_Test_hello
15   (JNIEnv *, jclass);
16  #ifdef __cplusplus
17  }
18  #endif
19  #endif

清單 10. 本地代碼

1  #include "hello_Hello.h"
2  #include <stdio.h>
3  JNIEXPORT void JNICALL Java_Test_hello
4   (JNIEnv * env, jclass jobj) {
5    printf("Greeting from build system – jni component! \n");
6  }

清單 11. 構建 JNI 組件的 Ant 腳本

1  <?xml version="1.0"?>
2  <project default="compile" basedir=".">
3   <echo message="pulling in property files" />
4   <import file="properties.xml" />
5   <echo message="compile jni greeting" />
6   <target name="compile" depends="compile- native">
7    <mkdir dir="${path.dist.classes}" />
8    <javac destdir="${path.dist.classes}" srcdir="${path.src}" />
9   </target>
10   <target name="compile-java">
11    <mkdir dir="${path.dist.classes}" />
12    <javac destdir="${path.dist.classes}" srcdir="${path.src}" />
13   </target>
14   <!-- native tasks start here -->
15   <target name="compile-header" depends="compile- java">
16    <javah destdir="${path.dist.classes}" classpath="${path.dist.classes}"
         class="${class.jni}" />
17   </target>
18   <target name="copy-native-includes" depends="compile- header">
19    <mkdir dir="${path.src.native.include}" />
20    <copy todir="${path.src.native.include}">
21     <fileset dir="${path.dist.classes}">
22      <include name="**/*.h" />
23     </fileset>
24    </copy>
25   </target>
26   <target name="compile-native" depends="copy-native- includes">
27    <exec executable="nmake" dir="${path.src.native} " failonerror="true" />
28    <copy todir="${path.dist.lib}">
29     <fileset dir="${path.src.native}">
30      <include name="*.so" />
31     </fileset>
32    </copy>
33   </target>
34   <target name="clean">
35    <delete dir="${path.dist}" />
36    <delete dir="${path.src.native.include}" />
37    <delete>
38     <fileset dir="${path.src.native}">
39      <include name="**/*.so" />
40     </fileset>
41    </delete>
42   </target>
43  </project>

清單 12. Red Hat (linux.x86.mk) 上的 Makefile

1  CC= gcc
2  PROGNAME= libhellolib.so
3  INCLUDES= -I/home/spark/jdk1.5.0_07/include
4  INCLUDES+= -I/home/spark/jdk1.5.0_07/include/linux
5  CFLAGS=  -o $(PROGNAME) -shared -Wl,-soname,libhello.so
6  CFLAGS+= $(INCLUDES)
7  CFLAGS+= -static -lc
8  SRCS = hello.c
9  all:
10      $(CC) $(CFLAGS) $(SRCS)

如果將上述所有的任務集中到一起,那麼您就可以構造一個原型構建系統。下面的圖 2 顯示了該系統的規劃依賴關系圖。可以使用這個原型作為一個起點。本文剩下的部分將從原 型到具有實際規模的工作系統對該系統進行詳細闡述。

圖 2. 任務依賴關系

設計一個可插入的構建系統

要移植到不同的目標平台,對於開發人員來說是一項挑戰,因為不同平台的系統調用各不 相同。這些平台上的第三方庫也不相同。在編寫構建腳本時,存在同樣的情況。假設目標平 台包括下列平台:

Freebsd + x86

Linux® + ia64

Linux + ppc32

Linux + ppc64

Linux + s390

Linux + s390x

Linux + x86

Linux + x86_64

使得平台特定的構建腳本成為可插入的和可移植的

這些平台上的 GCC 可以識別不同的標志,make 命令甚至具有不同的名稱。表 1 對這些 目標平台上 GCC 標志的子集進行了比較。

表 1. 不同平台上標志的比較

Platform OPT OSLIBS ASFLAGS LDFLAGS XLIBS freebsd/x86 -march=pentium3 -lc_r -lm N/A N/A N/A Linux/ia64 N/A N/A N/A N/A N/A Linux/ppc32 -m32 N/A -m32 -m32 N/A Linux/ppc64 -m64 N/A -a64 -m64 -L/usr/X11R6/lib64 -lX11 -lXft Linux/s390 -fpic -m31 N/A -m31 -m31 N/A Linux/s390x -fpic -m64 N/A -m64 -m64 N/A Linux/x86 -march=pentium3 N/A N/A N/A N/A Linux/x86_64 -fpic N/A N/A N/A -L/usr/X11R6/lib64 -lX11 -lXft

GCC 標志(本地 makefile 的平台特定部分)有所不同。同時,在每個平台上構建本地組 件的步驟大致上是相同的。為了避免重復,每個模塊的構建步驟只需要一個 makefile。圖 3 顯示了這個結構,它從常量(構建步驟)中提取變量(GCC 標志)。它還使得新的平台特定 的本地代碼成為可插入的。

圖 3. 將構建步驟和構建標志分開

在這個解決方案中,每個模塊的 makefile 放在 <module-name>/native/ 目錄中 ,而 GCC 標志 (<platform>.mk) 位於每個平台的 make/platform 目錄中。在構建過 程中,每個模塊的 makefile 將在運行時自動查找 <platform>.mk 文件(稍後將介紹 其中使用的技術),並包含它們以組成一個完整的本地構建腳本。腳本還可以在特定的目錄 (例如,AIX.s390)中選擇平台特定的代碼進行編譯。

要使得前面的示例兼容於這種結構,需要進一步對 makefile 進行劃分。下面的清單 13 、14 和 15 顯示了這種劃分。

清單 13 顯示了用於前面示例的模塊特定的 makefile(在 makefile 文件中)。

清單 13. 用於 Hello 程序的模塊特定的構建步驟

1  include ../../make/defines.mk
2  include ../../make/platform/$(HY_PLATFORM).mk
3  CFLAGS+= $(SRCS)
4  CFLAGS+= $(INCLUDES) $(OPT)
5  CFLAGS+= -static -lc
6  PROGNAME= libhellolib.so
7  include ../../make/rules.mk

清單 14 顯示了常用的宏(在 defines.mk 文件中),這些宏定義了使用哪個 GCC、使用 哪個 ld 以及使用哪些平台特定的本地代碼。

清單 14. defines.mk 中常用的宏

1  CC= gcc
2  ifneq ($(HY_OS),aix)
3  DLL_LD = $(CC)
4  else
5  DLL_LD = $(LD)
6  endif
7  INCLUDES= -I/home/robert/jdk1.5.0_07/include
8  INCLUDES+= -I/home/robert/jdk1.5.0_07/include/linux
9  SRCS=$(HY_PLATFORM)/hello.c

清單 15 顯示了平台特定的 GCC 標志(在 <platform.mk> 文件中)。

清單 15. <platform>.mk 中平台特定的 GCC 標志

1  CC= gcc
2  ifneq ($(HY_OS),aix)
3  DLL_LD = $(CC)
4  else
5  DLL_LD = $(LD)
6  endif
7  INCLUDES= -I/home/robert/jdk1.5.0_07/include
8  INCLUDES+= -I/home/robert/jdk1.5.0_07/include/linux
9  SRCS=$(HY_PLATFORM)/hello.c

清單 16 顯示了一般的構建步驟(在 rules.mk 文件中)。

清單 16. rules.mk 中一般的構建步驟

1 all: $(PROGNAME)
2 $(PROGNAME):
3 $(DLL_LD) -o $(PROGNAME) -shared \
4 -Wl,-soname,libhello.so $(CFLAGS)

在需要一個新的平台支持時,如 Linux on x86,開發人員可以將 linux.x86.mk 文件添 加到 make/platform 目錄,並將平台特定的本地代碼放到 <module>/native/linux.x86 目錄中。在構建的過程中,構建系統使用相應的技術( 稍後將作介紹)動態地查找正確的組件。

設計源代碼的布局

這個部分描述了源代碼的布局。通常一個企業項目中包含許多組件。假設您有四個組件:

一個純 Java 組件,用於與用戶進行交互

三個 JNI 組件:thread,用來處理實時線程;archive,用來進行壓縮和解壓縮;net, 用來處理套接字函數

在 JNI 組件中,Java 代碼和對應的本地代碼之間是高度相關的,所以應該將它們放在一 起。用於該模塊的 makefile 和 Ant 構建腳本也應該放在相同的文件夾中。圖 4 顯示了整 個源代碼布局。

圖 4. 代碼布局和構建腳本

在這個布局中,make 目錄擁有一個 platform 子目錄和兩個 .mk 文件:rules.mk 和 defines.mk。在 platform 子目錄中,每個目標平台都有一個 <platform>.mk 文件。

與 make 目錄一樣,build.xml 也是一個頂級元素。在調用它時,它將檢測基礎構建環境 ,並使用宏調用模塊調用每個模塊中每個構建文件的 compile 任務。然後,component.xml 調用本地構建腳本,其中包括對應平台特定的 GCC 標志,並按照 build.xml 文件中的指示 ,編譯相關的本地代碼。通過這種方式,可以很容易地添加新的模塊,並且還可以根據您的 需要為現有的模塊自定義新的平台支持。同時,不會對其他組件產生影響。

Ant 不具有內置的 make 任務。您需要使用 <exec> 任務來調用 make 命令。因為 在不同的體系結構中 make 命令基本類似,所以采用這種方式調用它們將會出現重復的工作 。Ant 提供了一種稱為宏 的機制來解決這個問題。清單 17 顯示了名為“make” 的宏(在 property.xml 中定義)。

清單 17. make 宏

1 <macrodef name="make">
2  <attribute name="dir" />
3  <sequential>
4   <exec failonerror="true" executable="${make.command} " dir="@{dir}">
5    <env key="HY_ARCH" value="${hy.arch}" />
6    <env key="HY_OS" value="${hy.os}" />
7   </exec>
8  </sequential>
9 </macrodef>

清單 18 顯示了如何調用 make 宏。

清單 18. 調用 make 宏

<make dir="${path.src.native}">

隨著該項目的發展,可以添加更多的組件和平台。開發人員需要修改構建系統以適應這種 發展。要在一個中央 Ant 構建文件中為新的組件和平台添加 Ant 任務,您需要修改已經穩 定的構建腳本,而這可能會引入潛在的錯誤。在這個解決方案中,每個組件都擁有自己的 build.xml 文件。中央構建腳本依次調用每個模塊的編譯任務。每個組件都成為可插入的。 清單 19 顯示了中央 build.xml 文件。

清單 19. 中央 build.xml 文件中的 Java 任務

1  <target name="compile-java">
2   <mkdir dir="${path.dist.classes}" />
3   <call-modules target="compile-java" />
4  </target>
5  <target name="compile-native">
6   <call-modules target="compile-native" />
7  </target>
8  <target name="clean">
9   <delete dir="${path.dist}" />
10   <call-modules target="clean" />
11  </target>

清單 20 顯示了一個模塊特定的 build.xml 文件。

清單 20. 用於 thread 模塊的 build.xml 文件

1 <?xml version="1.0"?>
2 <project basedir="." default="compile-java">
3  <echo message="compile thread greeting" />
4  <import file="../../properties.xml" />
5  <property name="p.root" value="${basedir}/../../" />
6  <target name="compile" depends="compile-java, compile- native" />
7  <target name="compile-java">
8   <javac destdir="${p.root}/${path.dist.classes}"
      srcdir="${p.root}/${path.src}/thread/java" />
9  </target>
10  <!-- native tasks start here -->
11  <target name="compile-header" depends="compile- java">
12  <javah destdir="${p.root}/${path.dist.classes}" classpath=\
        "${p.root}/${path.dist.classes}"
        class="${class.jni}" />
13  </target>
14  <target name="copy-native-includes" depends="compile- header">
15   <mkdir dir="${p.root}/${path.src}/thread/native/include" />
16   <copy todir="${p.root}/ ${path.src}/thread/native/include">
17    <fileset dir="${p.root}/${path.dist.classes}">
18    <include name="**/*.h" />
19    </fileset>
20   </copy>
21  </target>
22  <target name="compile-native" depends="copy-native- includes">
23   <echo>${make.command} ${path.src.native}</echo>
24   <make dir="${p.root}/${path.src}/thread/native" />
25   <copy todir="${p.root}/${path.dist.lib}">
26    <fileset dir="${p.root}/${path.src}/thread/native">
27    <include name="*.so" />
28   </fileset>
29   </copy>
30  </target>
31  <target name="clean">
32   <delete dir="${p.root}/${path.src}/thread/native/include" />
33   <delete>
34   <fileset dir="${p.root}/${path.src}/thread/native">
35    <include name="**/*.so" />
36   </fileset>
37   </delete>
38  </target>
39 </project>

清單 21 定義了一個稱為“calls-modules”的宏(在 property.xml 中定義 ),以便調用模塊特定的 build.xml 文件。

清單 21. call-module 宏

1  <property name="build.module" value="*" />
2  <property name="exclude.module" value="nothing" />

3  <macrodef name="call-modules">
4   <attribute name="target" />
5   <sequential>
6    <subant target="@{target}">
7     <dirset dir="modules" includes="${build.module}" excludes=\
       "${exclude.module}" />
8    </subant>
9   </sequential>
10 </macrodef>

檢測構建環境

該應用程序由 Java 類文件和本地庫共同組成。在構建的過程中,系統需要通過人工輸入 或自動檢測的方式了解基礎環境,以便能夠正確地構建本地代碼。這個解決方案引入了一種 自動檢測技術來確定構建環境。在構建系統檢測出基礎系統類型後,它應該包含相應的屬性 文件,並生成庫。圖 5 顯示了包含正確的屬性文件、生成正確的本地庫和 JAR 包裝器的過 程。下面將對圖中的數字進行解釋。

圖 5. 包含正確的屬性文件

build.xml 文件包含 property.xml。在 property.xml 中,Ant 使用了一個內置的測試 條件和屬性來識別基礎平台。清單 22 顯示了使用這種技術的腳本。在 property.xml 文件 中對這些屬性進行了定義。 清單 22. 與確定的環境相結合的條件和內置屬性

1  <!-- built-in test conditions -->
2  <condition property="hy.os" value="linux">
3   <os name="linux" />
4  </condition>
5  <condition property="hy.os" value="freebsd">
6   <os name="freebsd" />
7  </condition>
8  <condition property="hy.os" value="aix">
9   <os name="aix" />
10  </condition>
11  <!-- built-in property os.name -->
12  <property name="hy.os" value="${os.name}" />

hy.os 屬性將傳遞給 make 宏,如清單 17 所示。從而將其作為名為 HY_OS 的環境變量 傳遞到 makefile 中。這種技術也適用於環境變量 HY_ARCH。

對應的模塊中的 makefile,根據 HY_ARCH 的值來包含相應的編譯標志。清單 23 顯示了 一個示例。 清單 23. makefile 包含平台特定的標志 ( $(HY_PLATFORM).mk )

...
1 include $(HY_HDK)/build/make/platform/$(HY_PLATFORM).mk
2 ifneq ($(HY_OS),freebsd)
3 OSLIBS += -ldl
4 Endif
...

剩下的步驟是構建和打包。

設計任務依賴關系

最後一項任務是設計任務依賴關系。這可能是一個迭代的過程。如圖 2 中的原型構建系 統所示,該系統中有三項主要的任務。它們分別是與 SVN 相關的、用於構建 Java 和構建本 地代碼的。隨著項目的推進,可以進一步改進這些任務。下面的圖 6 顯示了一個更精細的系 統。

圖 6. 經過改進的任務依賴關系

圖 7 說明了如何進一步改進構建本地代碼的任務。

圖 7. 進一步細化構建本地代碼的任務

您可以在下載部分中下載 autobuild.zip,其中包括一個 JNI 模塊、Java 構建腳本和本 地構建腳本。該文件中包含了本文中介紹的所有任務。

總結

本文介紹了一種自動地將帶有本地擴展的 Java 項目移植到異類 UNIX 平台的系統的方法 。您了解了如何將項目劃分為 Ant 構建腳本中公共部分和模塊特定的部分,以及如何使得模 塊開發成為可插入的。您還了解了如何將平台劃分為 make 腳本中公共部分和平台特定的部 分,以及如何使得平台相關的代碼成為可插入的。最後,您掌握了如何使用 Ant 內置的測試 條件和屬性,動態地自動檢測基礎體系結構,包括平台特定的代碼。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved