级别: 中级 Shen Yu, 软件工程师, IBM
2007 年 6 月 14 日 使用组件化的构建系统自动地将带有本地扩展的 Java™ 项目移植到异类的 UNIX® 平台。现在,类 UNIX 平台上运行的许多大型 Java 系统都需要第三方本地库支持,或者需要开发您自己的本地组件。这些平台上的许多实用工具和系统调用都没有相应的 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 内置的测试条件和属性,动态地自动检测基础体系结构,包括平台特定的代码。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| Automatic build system | au-autobuild.zip | 7KB | HTTP |
|---|
参考资料 学习
获得产品和技术
-
IBM 试用软件:从 developerWorks 可直接下载这些试用软件,您可以利用它们开发您的下一个项目。
- 获得下面的产品:
讨论
关于作者  | 
|  | Shen Yu 是 IBM 在中国的 Application and Integration Middleware Software 软件小组的软件工程师,是 IBM 位于上海的中国软件开发实验室的软件工程师。他已经参与了若干个解决方案集成项目。业余时间,他喜欢绘画。 |
对本文的评价
|