49、跨平台开发与原生代码部署全解析

跨平台开发与原生代码部署全解析

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. 文档记录 :详细记录开发和部署过程中的关键信息,方便后续的维护和升级。

通过遵循这些最佳实践,可以有效降低开发和部署过程中的风险,提高项目的成功率。

总之,跨平台开发和原生代码部署是一个复杂的过程,需要我们综合运用各种技术和工具,充分考虑生产环境的特点和需求。通过合理的规划和实践,我们可以实现高效、稳定的开发和部署,为企业的发展提供有力支持。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值