跨平台开发与原生代码部署全解析
1. 跨平台开发起步
在跨平台开发中,我们首先要对汇编代码进行处理。具体操作步骤如下:
1. 注释掉所有汇编代码。
2. 创建一个存根实现,使每次调用都返回零。待构建和测试流程正常后,再移植汇编代码。
2. 扩展构建文件
<cc>
任务可用于为多个编译器指定编译设置,包含不同的
<compiler>
和
<linker>
元素。我们不选择扩展现有
<cc>
任务声明来支持 Linux,原因有二:
- 需将条件包含扩展到源文件和
<sysincludepath>
引用,会使目标变得复杂。
- 不想破坏在单一平台上正常工作的目标。
因此,我们将现有的
cc-windows
目标内容复制到新的
cc-linux
目标,并进行如下配置:
<condition property="is-linux">
<os name="linux" arch="x86" />
</condition>
<condition property="is-windows">
<os family="windows"/>
</condition>
<target name="cc-linux" depends="headers" if="is-linux">
<cc debug="${build.debug}"
link="shared"
outfile="${dist.filename.nosuffix}"
objdir="${obj.dir}"
multithreaded="true"
exceptions="true" >
<compiler name="gcc"/>
<fileset dir="src/cpp/linux" includes="**/*.cpp" />
<includepath location="${generated.dir}" />
<sysincludepath location="${env.JAVA_HOME}/include" />
<sysincludepath location="${env.JAVA_HOME}/include/linux"/>
<linker name="gcc" />
</cc>
</target>
<target name="cc" depends="cc-windows,cc-linux"/>
同时,我们还需添加一个高级目标
cc
,它依赖于两个条件编译目标,在单一平台构建时仅会运行其中一个。之后,将
cc-windows
的现有依赖目标(如
test
)修改为依赖于
cc
目标,以便在各自平台上运行合适的编译器目标。
以下是这个过程的 mermaid 流程图:
graph LR
A[开始] --> B[创建 cc-linux 目标]
B --> C[添加 cc 高级目标]
C --> D[修改依赖目标为 cc]
D --> E[结束]
3. 测试迁移
由于尚未迁移汇编语言,并非所有测试都能通过,但首次调用方法返回的测试应能正常工作。运行构建后,结果如下:
cc-linux:
[cc] Starting dependency analysis for 1 files.
[cc] Parsing build/generated/org_example_antbook_cpu_CpuInfo.h
[cc] 0 files are up to date.
[cc] 1 files to be recompiled from dependency analysis.
[cc] 1 total files to be compiled.test:
[junit] Running org.example.antbook.cpu.CpuInfoTest
[junit] Tests run: 3, Failures: 2, Errors: 0, Time elapsed: 0.129 sec
[junit] Testcase: testClockCallReturns took 0.023 sec
[junit] Testcase: testClockCodeWorks took 0.017 sec
[junit] FAILED
这表明
cc-linux
目标已被调用以构建文件,首个测试通过,但后两个失败。至此,我们已拥有一个能从 Java 文件创建 JNI 头文件、在两个不同平台编译 C++ 类以实现方法并进行测试的构建流程,接下来只需在第二个平台实现原生代码。
4. 代码移植
最后一步是移植计时器代码,具体做法是通过 Google 查找合适的代码片段并进行定制:
JNIEXPORT jlong JNICALL
Java_org_example_antbook_cpu_CpuInfo_getCpuClock
(JNIEnv *,
jobject) {
long long int timestamp;
asm volatile (".byte 0x0f, 0x31" : "=A" (timestamp));
return timestamp;
}
再次运行构建,结果如下:
test:
[junit] Running org.example.antbook.cpu.CpuInfoTest
[junit] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 0.091 sec
[junit] Testsuite: org.example.antbook.cpu.CpuInfoTest
[junit] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 0.091 sec
[junit] ------------- Standard Output ---------------
[junit] Invocation time=594 cycles
[junit] Total time=1469967 cycles
[junit] Invocation time=146 cycles
[junit] ------------- ---------------- ---------------
[junit]
[junit] Testcase: testClockCallReturns took 0.023 sec
[junit] Testcase: testClockCodeWorks took 0.003 sec
[junit] Testcase: testJitOptimization took 0.023 sec
BUILD SUCCESSFUL
在与 Windows 系统相同配置的系统上(Java 1.4,Redhat 7.1 Linux),优化后的往返时间与 Windows 系统几乎相同。我们认为,该 C++ 代码的 Linux 版本应能在任何支持 gcc 的 x86 平台上运行,从 Windows 到 Solaris Intel Edition 均可。使用 gcc 进行 JNI 开发是一个极具吸引力的选择,即便开发者拥有 Microsoft 工具和调试器。
5. 深入了解
<cc>
任务
<cc>
任务虽不稳定,但包含一些有趣的特性,以下是对其未使用特性的详细介绍。
5.1 定义预处理器宏
使用
<defineset>
数据类型可定义预处理器宏,有两种方式:
- 简单方式:在编译器任务内声明定义。
<target name="cc-linux" depends="headers" if="is-linux">
<cc debug="${build.debug}"
link="shared"
outfile="${dist.filename.nosuffix}"
objdir="${obj.dir}"
multithreaded="true"
exceptions="true" >
<compiler name="gcc"/>
<fileset dir="src/cpp/linux" includes="**/*.cpp"/>
<defineset defines="DEBUG"/>
<includepath location="${generated.dir}" />
<sysincludepath location="${env.JAVA_HOME}/include" />
<sysincludepath location="${env.JAVA_HOME}/include/linux"/>
<linker name="gcc" />
</cc>
</target>
-
共享定义方式:在
init目标中创建<defineset>。
<condition property="build.debug.istrue">
<istrue value="${build.debug}" />
</condition>
<defineset id="build.defines">
<define name="DEBUG" if="build.debug.istrue" />
<define name="RELEASE" unless="build.debug.istrue" />
</defineset>
之后,在编译器目标中通过引用 ID 来使用这些预处理器定义:
<target name="cc-linux" depends="headers" if="is-linux">
<cc debug="${build.debug}"
link="shared"
outfile="${dist.filename.nosuffix}"
objdir="${obj.dir}"
multithreaded="true"
exceptions="true" >
<compiler name="gcc"/>
<fileset dir="src/cpp/linux"/>
<defineset refid="build.defines"/>
<includepath location="${generated.dir}" />
<sysincludepath location="${env.JAVA_HOME}/include" />
<sysincludepath location="${env.JAVA_HOME}/include/linux"/>
<linker name="gcc" />
</cc>
</target>
5.2 使用
<libset>
链接库
若要链接编译器默认集以外的库,可使用
<libset>
数据类型,可在
<cc>
任务或
<linker>
元素内声明。
<cc
outfile="build/app"
multithreaded="true"
exceptions="true" >
<compiler name="gcc"/>
<fileset dir="src"/>
<linker name="gcc" />
<libset libs="cclib/tools,cclib/services">
</cc>
无需为库指定特定平台的扩展名,
<libset>
会自动使用合适的扩展名(如 Unix 的
.a
和
.so
,Windows 的
.lib
)。不同实现访问系统库的方式不同,Microsoft 链接器依赖
LIB
环境变量,gcc 链接器会搜索知名库位置(如
/usr/lib
)。若要在目标间共享库,可将其声明为带 ID 的数据类型:
<libset id="common.libset" libs="cclib/tools,cclib/services" />
在编译时通过 ID 引用该
<libset>
:
<cc
outfile="build/app"
multithreaded="true"
exceptions="true"
>
<compiler name="gcc"/>
<fileset dir="src"/>
<linker name="gcc" />
<libset refid="common.libset">
</cc>
为便于管理大型原生应用程序的构建,可将常见的
<libset>
声明放在 XML 文件片段中,将不同库分组(如
corba
、
com
、
mozilla
),并在项目中按需复用。
5.3 配置编译器和链接器
在原生语言项目中,还可对编译器和链接器的设置进行定制,
<compiler>
和
<linker>
元素不仅可用于此,还是独立的数据类型,能为整个项目声明通用的链接器和编译器并在适当位置使用。
5.3.1 配置编译器
在
<compiler>
元素内(无论是在
<cc>
任务中还是独立的数据类型声明),可嵌套
<defineset>
元素。以下是一个基于
msvc
的编译器配置示例:
<compiler id="studio" name="msvc">
<compilerarg value="/G6"/>
<compilerarg value="/W3"/>
<compilerarg value="/Ze"/>
<compilerarg value="/Zc:forScope"
if="msvc.version.is.devenv"/>
<defineset>
<define name="_CRTDBG_MAP_ALLOC"
if="build.debug.istrue"/>
</defineset>
</compiler>
在
<cc>
任务中引用该声明:
<compiler refid="studio" />
还可扩展现有配置:
<compiler id="studio2" extends="studio">
<compilerarg value="/Gm"/>
<compilerarg value="/ZI"/>
</compiler>
扩展后的定义会保留之前的定制并添加更多参数,常用于定义单独的调试和发布编译器,各自具有不同的优化和编译标志。使用配置好的编译器时,
<cc>
任务中添加的所有编译器设置也会生效,且该任务会缓存构建输出文件、每个目标文件的编译器参数和链接文件的链接器参数,更改参数时会自动重建受影响的文件,但仍建议运行
clean
目标。
5.3.2 定制链接器
链接器及
<cc>
任务的其他处理器可类似编译器进行配置,嵌套参数和标签可更改行为,还可为链接器指定 ID 以便后续引用,也可扩展现有链接器定义。以下是一个配置链接器的示例,它将 Windows DLL 限制为最低 Windows 版本并设置库的基地址:
<linker id="nt4linker" name="msvc"
base="201333515">
<linkerarg value="/version:4.0" />
<syslibset libs="kernel32,user32"/>
</linker>
在
<cc>
任务中引用该链接器:
<linker refid="nt4linker" />
若需配置链接器,说明正在处理复杂的原生代码,通过
<cc>
任务可构建非常复杂的 C++ 项目。
6. 分发原生库
在运行 JVM 前,需将
java.library.path
属性设置为包含 JNI 库的目录。分发代码时,在任何运行 JNI 程序的
<java>
调用或启动程序的 shell 脚本中都要进行相同设置。将原生库与 Web 应用集成时,可修改应用服务器的启动属性或将库放入执行路径,Ant 安装脚本可将库复制到指定位置,但需先关闭应用服务器,以确保 Ant 能覆盖现有库版本并使应用服务器重新加载新库。
对于客户端代码,由于安全原因,小程序无法下载原生库,而 Java Web Start 允许最终用户下载并运行原生库,且能根据客户端平台下载合适的库。
使用 Java Web Start 重新分发原生库是个不错的选择,虽然 Ant 本身不支持 Java Web Start,但 Venus Application Publisher Vamp 产品系列(http://www.vamphq.com)可解决此问题。该系列提供了几个用于 Web Start 代码交付的 Ant 任务,其中
<vampwar>
任务可构建一个 WAR 文件,包含要分发的应用和使 Web Start 客户端能使用 JNLP 协议下载应用所需的 servlet,若提供正确信息,该任务还能对可下载的 JAR 文件进行签名。这意味着该任务可处理代码和其他资源,生成可直接交付给最终用户的 Web 应用,开发者只需将该 Web 应用部署到服务器,不过仍需从 SDK 文档中学习 Java Web Start 和 JNLP 相关知识,并花时间确保 JNLP 描述符正确,而修改 Web 应用以支持 JNLP 及设置打包应用的构建流程步骤则由 Vamp 任务完成。
7. 生产部署挑战与特性
生产系统部署与开发环境部署存在诸多差异,生产系统具有以下特点:
| 特点 | 描述 |
| ---- | ---- |
| 管理团队 | 由运维团队管理,而非开发者自身 |
| 应用服务器 | 可能使用不同的应用服务器 |
| 远程部署 | 服务器可能位于远程,受安全系统影响部署难度增加 |
| 部署流程 | 需要更健壮的部署流程,具备回滚机制 |
| 部署内容 | 部署内容更复杂,包含静态内容和 Web/EJB 应用 |
| 集群部署 | 可能需要部署到多个服务器集群,采用滚动更新以保持系统运行 |
在进行企业或 Web 应用的生产部署时,需应对这些挑战,确保部署过程的顺利进行。
以下是生产部署过程的 mermaid 流程图:
graph LR
A[开始] --> B[分析生产系统特点]
B --> C[选择合适部署方式]
C --> D[准备部署内容]
D --> E[执行部署]
E --> F[验证部署结果]
F --> G{是否成功}
G -- 是 --> H[结束]
G -- 否 --> I[回滚操作]
I --> B
综上所述,跨平台开发和原生代码部署是一个复杂但有章可循的过程,通过合理运用
<cc>
任务和相关工具,能有效解决开发和部署中的各种问题,提高开发效率和代码的可移植性。
跨平台开发与原生代码部署全解析
8. 应对不同应用服务器的挑战
在生产部署中,不同的应用服务器会带来诸多挑战。不同的应用服务器有各自独特的配置要求和部署方式,这就需要我们针对具体的服务器进行相应的调整。
例如,某些应用服务器可能需要特定的环境变量设置,或者对部署文件的格式和目录结构有严格要求。为了应对这些挑战,我们需要深入了解每个应用服务器的特性,然后制定相应的部署策略。
以下是一些常见应用服务器在部署时可能遇到的问题及解决思路:
| 应用服务器 | 常见问题 | 解决思路 |
| ---- | ---- | ---- |
| Tomcat | 部署文件路径配置复杂,可能出现类加载冲突 | 仔细检查
server.xml
等配置文件,确保部署路径正确;使用独立的类加载器解决冲突 |
| WebLogic | 安全策略严格,部署过程中可能需要额外的权限 | 配置合适的安全策略,确保部署用户具有足够的权限 |
| JBoss | 对数据源和事务管理的配置要求较高 | 详细配置
standalone.xml
等文件,确保数据源和事务管理正常工作 |
9. 与运维团队协作
在生产部署过程中,与运维团队的协作至关重要。运维团队负责生产系统的日常维护和监控,他们对系统的稳定性和安全性有着深刻的理解。
与运维团队协作的具体步骤如下:
1.
沟通需求
:在部署前,与运维团队充分沟通部署的需求和目标,包括部署的时间、范围、影响等。
2.
制定计划
:共同制定部署计划,明确双方的职责和任务,确保部署过程的顺利进行。
3.
测试环境搭建
:在测试环境中进行部署测试,邀请运维团队参与,及时发现并解决潜在的问题。
4.
生产部署
:在生产环境中进行部署时,与运维团队密切配合,确保部署过程的安全和稳定。
5.
监控和反馈
:部署完成后,与运维团队一起监控系统的运行情况,及时反馈问题并进行处理。
10. 使用 Ant 解决部署挑战
Ant 提供了一系列强大的部署任务,可以帮助我们解决生产部署中的各种挑战。以下是一些使用 Ant 进行部署的关键步骤:
10.1 配置部署目标
在
build.xml
文件中,定义不同的部署目标,每个目标对应一个具体的部署任务。例如:
<target name="deploy-tomcat" description="Deploy to Tomcat">
<copy file="dist/myapp.war" todir="${tomcat.deploy.dir}" />
</target>
<target name="deploy-weblogic" description="Deploy to WebLogic">
<exec executable="${weblogic.deploy.script}">
<arg value="${weblogic.server.url}" />
<arg value="${weblogic.username}" />
<arg value="${weblogic.password}" />
<arg value="dist/myapp.war" />
</exec>
</target>
10.2 集成部署流程
将部署任务集成到整个开发流程中,确保每次代码更新后都能自动进行部署。可以使用 Ant 的依赖关系来实现这一点,例如:
<target name="deploy" depends="compile, test, package">
<antcall target="deploy-tomcat" />
</target>
10.3 处理错误和回滚
在部署过程中,可能会出现各种错误。Ant 可以通过异常处理和回滚机制来确保部署的安全性。例如:
<target name="deploy" depends="compile, test, package">
<trycatch>
<try>
<antcall target="deploy-tomcat" />
</try>
<catch>
<antcall target="rollback-tomcat" />
</catch>
</trycatch>
</target>
<target name="rollback-tomcat" description="Rollback Tomcat deployment">
<delete file="${tomcat.deploy.dir}/myapp.war" />
</target>
11. Ant 部署工具介绍
Ant 提供了一些强大的部署工具,以下是其中一些常用工具的介绍:
11.1
<copy>
任务
<copy>
任务用于将文件或目录从一个位置复制到另一个位置。例如:
<copy file="src/main/resources/config.properties" todir="dist/config" />
11.2
<exec>
任务
<exec>
任务用于执行外部命令。例如,执行一个 Shell 脚本:
<exec executable="sh">
<arg value="deploy.sh" />
</exec>
11.3
<war>
任务
<war>
任务用于创建 WAR 文件。例如:
<war destfile="dist/myapp.war" webxml="src/main/webapp/WEB-INF/web.xml">
<fileset dir="src/main/webapp" />
</war>
12. 构建生产部署流程
构建一个完整的生产部署流程需要考虑多个方面,以下是一个典型的生产部署流程:
1.
代码检查
:在部署前,对代码进行静态检查,确保代码质量。
2.
编译和打包
:使用 Ant 对代码进行编译和打包,生成可部署的文件。
3.
测试
:进行单元测试、集成测试和系统测试,确保代码的正确性。
4.
部署到测试环境
:将打包好的文件部署到测试环境,进行进一步的测试。
5.
审批
:经过测试后,由相关人员进行审批,确保部署的安全性和稳定性。
6.
部署到生产环境
:将审批通过的文件部署到生产环境。
7.
监控和反馈
:部署完成后,对系统进行监控,及时反馈问题并进行处理。
以下是该生产部署流程的 mermaid 流程图:
graph LR
A[代码检查] --> B[编译和打包]
B --> C[测试]
C --> D[部署到测试环境]
D --> E[审批]
E --> F[部署到生产环境]
F --> G[监控和反馈]
G -- 有问题 --> C
G -- 无问题 --> H[结束]
13. 部署到特定应用服务器
针对不同的应用服务器,需要采用不同的部署方法。以下是一些常见应用服务器的部署示例:
13.1 部署到 Tomcat
<target name="deploy-tomcat" description="Deploy to Tomcat">
<copy file="dist/myapp.war" todir="${tomcat.deploy.dir}" />
</target>
13.2 部署到 WebLogic
<target name="deploy-weblogic" description="Deploy to WebLogic">
<exec executable="${weblogic.deploy.script}">
<arg value="${weblogic.server.url}" />
<arg value="${weblogic.username}" />
<arg value="${weblogic.password}" />
<arg value="dist/myapp.war" />
</exec>
</target>
14. 验证部署结果
部署完成后,需要对部署结果进行验证,确保系统正常运行。验证的方法包括:
1.
功能验证
:检查系统的各项功能是否正常工作。
2.
性能验证
:测试系统的性能指标,如响应时间、吞吐量等。
3.
安全验证
:检查系统的安全漏洞,确保数据的安全性。
可以使用自动化测试工具来进行验证,例如 JUnit 进行单元测试,Selenium 进行 Web 应用的功能测试。
15. 最佳实践总结
在跨平台开发和原生代码部署过程中,遵循以下最佳实践可以提高开发效率和系统的稳定性:
1.
使用版本控制
:使用 Git 等版本控制系统来管理代码,方便团队协作和代码的追溯。
2.
自动化测试
:编写自动化测试用例,确保代码的正确性和稳定性。
3.
配置管理
:使用配置管理工具,如 Ansible 或 Puppet,来管理服务器的配置。
4.
持续集成和持续部署(CI/CD)
:建立 CI/CD 流程,实现代码的快速迭代和部署。
5.
文档记录
:详细记录开发和部署过程中的关键信息,方便后续的维护和升级。
通过遵循这些最佳实践,可以有效降低开发和部署过程中的风险,提高项目的成功率。
总之,跨平台开发和原生代码部署是一个复杂的过程,需要我们综合运用各种技术和工具,充分考虑生产环境的特点和需求。通过合理的规划和实践,我们可以实现高效、稳定的开发和部署,为企业的发展提供有力支持。
超级会员免费看
1313

被折叠的 条评论
为什么被折叠?



