49、跨平台开发与原生代码部署指南

跨平台开发与原生代码部署指南

1. 跨平台开发准备

在开始跨平台开发时,我们首先要对编译器进行处理。具体操作步骤如下:
1. 注释掉所有汇编代码。
2. 创建一个存根实现,每次调用该实现时都返回零。
完成上述步骤后,当构建和测试过程正常工作时,我们就可以进行汇编代码的移植了。

2. 扩展构建文件

<cc> 任务允许我们为多个编译器声明编译设置,通过不同的条件 <compiler> <linker> 元素实现。不过,我们没有选择扩展现有的 <cc> 任务声明来支持 Linux,原因有两点:
1. 需要将条件包含扩展到源文件和 <sysincludepath> 引用,这会使目标变得非常复杂。
2. 不想破坏在一个平台上已经正常工作的目标。

因此,我们将现有的 cc-windows 目标的内容复制到一个新的目标 cc-linux 中,并对其进行配置以支持 Linux 上的 GNU 工具。核心定制步骤如下:
1. 选择 GNU 编译器和链接器。
2. 将 Linux 版本的 JNI 头文件引入编译。
3. 通过设置属性使目标依赖于操作系统为 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 目标,这样它们就会为各自的平台运行适当的编译器目标。

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 系统几乎相同。我们认为,Linux 版本的 C++ 代码应该可以在任何支持 gcc 的 x86 平台上运行,从 Windows 到 Solaris Intel Edition。如果一开始就只使用 gcc,代码迁移会更容易,因为只需要一个版本的 C++ 源文件和一个 <cc> 任务。只需要使 <sysincludespath> includes/windows includes/linux includes/solaris 的引用具有条件性,就可以在支持的每个平台上重建 JNI 库。

5. 深入了解 <cc> 任务

5.1 定义预处理器宏

在构建原生代码时,可能需要定义预处理器宏。可以使用 <defineset> 数据类型来实现。最简单的方法是在编译器任务中声明定义,但为了在不同任务之间共享定义,我们在 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,可以在后续引用。它的两个定义依赖于 build.debug 属性:如果该属性为 true ,则定义 DEBUG ;如果为 false ,则定义 RELEASE 。由于 <define> 标签中的条件基于属性是否定义,并且在进行发布构建时将 build.debug 设置为 false ,所以需要创建一个新属性,仅在 build.debug true 时定义。然后,在编译器目标中引用这些预处理器定义:

<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> 任务或链接器元素中声明 <libset> 。例如:

<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> 中声明库以控制大型原生应用程序的构建外,还可以将常见的 <libset> 声明保存在 XML 文件片段中,将不同的库分组到不同的集合中(如 corba com mozilla ),并在需要时在项目中重用它们。

5.3 配置编译器和链接器

在原生语言项目中,最后一个定制领域是更改编译器和链接器的设置。 <cc> 目标的 <compiler> <linker> 元素不仅可以实现这一点,它们还是独立的数据类型,允许为整个项目声明通用的链接器和编译器,并在适当的地方使用。随着项目规模和复杂性的增加,这种方式的价值会更加明显。

5.3.1 配置编译器

在编译器元素内部(无论是在 <cc> 任务中还是作为独立的数据类型声明),可以嵌套之前介绍的 <defineset> 元素。以下是一个从 msvc 派生的编译器配置示例,包含额外的选项(如警告、为 Pentium Pro 生成代码)以及在调试构建中条件定义预处理器宏:

<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> 任务中使用配置好的编译器时,任务中添加的所有编译器设置也会生效。 <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" />

如果需要配置链接器,说明正在处理一些复杂的原生代码。有一个示例构建文件可以用于构建 Xerces 的 C++ 版本,这表明可以在单个构建文件中构建非常复杂的 C++ 项目。

6. 分发原生库

6.1 基本要求

在运行 JVM 之前,需要将 java.library.path 属性设置为包含 JNI 库的目录。在分发代码时,在任何运行 JNI 程序的 <java> 调用或启动程序的 shell 脚本中都需要进行相同的设置。当尝试将原生库与 Web 应用程序集成时,需要修改应用服务器的启动属性或将库放入执行路径。可以使用基于 Ant 的安装脚本将库复制到相应位置,就像 deploy 目标所做的那样。但在操作之前,应先关闭应用服务器,以确保 Ant 可以覆盖任何现有的库版本,并且应用服务器会重新加载新库。

6.2 客户端代码限制

由于安全原因,小程序不能下载原生库。而 Java Web Start 允许最终用户下载和运行原生库,并且能够智能地为客户端平台下载适当的库。

6.3 使用 Java Web Start 分发

Java Web Start 是一种很好的分发 Java 应用程序和原生库的方式,因为它会下载签名的 Web Start 应用程序声明所需的任何原生库。Ant 本身没有对 Java Web Start 的内置支持,但 Venus Application Publisher Vamp 产品系列(http://www.vamphq.com)可以解决这个问题。该产品系列提供了几个用于 Web Start 代码交付的 Ant 任务,其中一个任务 <vampwar> 可以构建一个 WAR 文件,该文件包含要分发的应用程序和一个 servlet,使 Web Start 客户端能够使用 JNLP 协议下载应用程序。如果提供了正确的信息,该任务甚至可以对可下载的 JAR 文件进行签名。

6.4 生产部署与开发部署的区别

将企业或 Web 应用程序部署到生产应用服务器与部署到本地开发环境有以下区别:
| 区别项 | 生产部署 | 开发部署 |
| ---- | ---- | ---- |
| 管理团队 | 由运维团队管理 | 由开发人员自己管理 |
| 应用服务器 | 可能使用不同的应用服务器 | 通常使用固定的开发环境服务器 |
| 部署难度 | 由于安全系统,服务器可能是远程的,部署更困难 | 本地部署,相对简单 |
| 部署过程 | 需要更健壮的部署过程,具备回滚机制 | 对部署过程的健壮性要求相对较低 |
| 部署内容 | 部署内容更复杂,包括静态内容和 Web/EJB 应用程序 | 部署内容相对简单 |
| 部署方式 | 可能需要部署到多个服务器集群,并进行滚动更新以保持系统运行 | 一般部署到单个开发服务器 |

总之,使用 <cc> 任务可以在 Ant 中编译原生代码,它可以替代一个或多个 makefile,将 C++ 代码与 Java 构建、测试和部署过程集成。随着任务的成熟,更多人可能会将 Ant 用作纯 C++ 程序的构建工具。现在就可以使用 <cc> 任务进行 JNI 代码生成,并且构建文件可以随着新代码的添加而扩展。只需要编写 Java 存根、C++ 实现和 JUnit 测试,并将存根类添加到 <javah> 类列表中即可。Ant 构建文件在处理 C++ 代码时与处理 Java 项目一样有效。

graph LR
    A[开始跨平台开发] --> B[注释汇编代码并创建存根实现]
    B --> C[扩展构建文件]
    C --> D[测试迁移]
    D --> E[代码移植]
    E --> F[深入了解<cc>任务]
    F --> G[定义预处理器宏]
    F --> H[链接库]
    F --> I[配置编译器和链接器]
    F --> J[定制链接器]
    G --> K[分发原生库]
    H --> K
    I --> K
    J --> K
    K --> L[使用Java Web Start分发]
    L --> M[生产部署]

7. 应对不同应用服务器的挑战

7.1 应用服务器的多样性问题

在企业级应用开发中,不同的应用服务器具有各自独特的配置和部署要求。例如,Tomcat、WebLogic、WebSphere 等,它们在性能、功能、安全等方面各有特点。这就给应用的部署带来了很大的挑战,需要针对不同的服务器进行专门的配置和调整。

7.2 与运维团队协作

运维团队负责生产系统的稳定运行,在部署过程中与他们密切合作至关重要。具体协作步骤如下:
1. 沟通需求:开发人员与运维团队明确应用的部署需求,包括服务器配置、资源要求等。
2. 制定计划:共同制定部署计划,确定部署时间、步骤和回滚策略。
3. 测试环境:在运维团队提供的测试环境中进行充分测试,确保应用在生产环境中的稳定性。
4. 监控与反馈:部署后,与运维团队一起监控应用的运行情况,及时反馈问题并进行调整。

8. 使用 Ant 解决部署挑战

8.1 Ant 的部署能力

Ant 提供了一系列强大的部署任务,可以帮助我们简化部署过程。通过编写 Ant 构建文件,可以将部署步骤自动化,提高部署效率和准确性。

8.2 构建生产部署流程

以下是一个使用 Ant 构建生产部署流程的示例:

<project name="MyAppDeployment" default="deploy">
    <target name="clean">
        <delete dir="dist"/>
    </target>
    <target name="compile">
        <javac srcdir="src" destdir="classes"/>
    </target>
    <target name="package">
        <jar destfile="dist/MyApp.jar" basedir="classes"/>
    </target>
    <target name="deploy" depends="clean, compile, package">
        <scp file="dist/MyApp.jar" todir="user@server:/path/to/deploy"/>
    </target>
</project>

上述代码展示了一个简单的 Ant 构建文件,包含了清理、编译、打包和部署的步骤。通过 scp 任务将打包好的 JAR 文件上传到远程服务器。

8.3 Ant 的部署工具

Ant 还提供了一些高级的部署工具,如 <war> 任务用于创建 WAR 文件, <ear> 任务用于创建 EAR 文件等。这些工具可以帮助我们更好地组织和部署应用。

9. 构建生产部署过程

9.1 部署流程设计

一个完整的生产部署过程通常包括以下步骤:
1. 代码检查:确保代码经过充分测试,没有明显的 bug。
2. 环境准备:准备好生产服务器的环境,包括安装必要的软件和配置系统参数。
3. 打包应用:将应用代码打包成可部署的格式,如 WAR、EAR 或 JAR 文件。
4. 上传文件:将打包好的文件上传到生产服务器。
5. 部署应用:在生产服务器上部署应用,并进行必要的配置。
6. 测试验证:对部署后的应用进行测试,确保其正常运行。
7. 监控与维护:部署后持续监控应用的运行情况,及时处理出现的问题。

9.2 部署到特定应用服务器

不同的应用服务器有不同的部署方式,以下是一些常见应用服务器的部署示例:

9.2.1 Tomcat
  1. 将 WAR 文件复制到 Tomcat 的 webapps 目录下。
  2. 启动或重启 Tomcat 服务器,Tomcat 会自动解压并部署 WAR 文件。
9.2.2 WebLogic
  1. 使用 WebLogic 的控制台或 WLST(WebLogic Scripting Tool)工具进行部署。
  2. 配置数据源、安全策略等相关参数。
9.2.3 WebSphere
  1. 使用 WebSphere 的管理控制台或 wsadmin 脚本进行部署。
  2. 配置应用的资源和环境变量。

9.3 验证部署

部署完成后,需要对应用进行验证,确保其正常运行。验证步骤如下:
1. 访问应用的 URL,检查页面是否正常显示。
2. 进行功能测试,确保应用的各项功能正常。
3. 检查日志文件,查看是否有错误信息。

10. 最佳实践

10.1 版本控制

使用版本控制系统(如 Git)管理应用代码和部署脚本,确保代码的可追溯性和一致性。

10.2 自动化测试

在部署前进行充分的自动化测试,包括单元测试、集成测试和端到端测试,减少部署风险。

10.3 回滚策略

制定详细的回滚策略,当部署出现问题时能够快速恢复到上一个稳定版本。

10.4 监控与日志

建立完善的监控和日志系统,实时监控应用的运行状态,及时发现和解决问题。

10.5 文档记录

记录部署过程和相关配置信息,方便后续维护和问题排查。

11. 总结

通过使用 <cc> 任务和 Ant 的部署功能,我们可以实现跨平台开发和原生代码的高效部署。在生产部署过程中,要充分考虑与运维团队的协作、不同应用服务器的特点以及部署的稳定性和可维护性。遵循最佳实践,能够提高部署效率,降低部署风险,确保应用在生产环境中的稳定运行。随着技术的不断发展,Ant 作为一种强大的构建和部署工具,将在更多的项目中发挥重要作用。

graph LR
    A[开始生产部署] --> B[代码检查]
    B --> C[环境准备]
    C --> D[打包应用]
    D --> E[上传文件]
    E --> F[部署应用]
    F --> G[测试验证]
    G --> H[监控与维护]
    I[版本控制] --> A
    J[自动化测试] --> A
    K[回滚策略] --> A
    L[监控与日志] --> H
    M[文档记录] --> A

综上所述,跨平台开发和生产部署是一个复杂而又关键的过程,需要我们综合运用各种技术和工具,遵循最佳实践,才能确保项目的成功。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值