Java平台模块系统:遗留应用的迁移策略

内容目录

Java平台模块系统(JPMS)随Java 9引入,代表了该平台最重大的变革之一。对于运行遗留单体应用的组织而言,JPMS通过显式的模块边界和强封装,提供了一条通往更好可维护性、安全性和可扩展性的道路。然而,迁移需要周密的规划和策略。本文为成功将遗留Java应用迁移至模块化架构提供了实用指南。

理解JPMS解决的问题

遗留Java单体应用通常存在紧耦合(组件深度交织,导致更改风险高)和弱封装(所有内容全局可访问,增加错误)的问题。其他挑战还包括构建速度慢(微小更改需要完全重建)以及有限的可扩展性(难以独立部署部分功能)。

JPMS通过引入显式的模块边界、强封装和更好的依赖管理来解决这些问题。该模块系统允许您分组包,并严格控制哪些属于公共API,模块会显式声明其依赖项,而不是仅仅依赖类路径。

什么是模块?

模块用于分组包,并严格控制哪些包属于公共API。与Jar文件不同,模块显式声明它们依赖哪些模块,以及它们导出哪些包。模块声明存在于源层次结构根目录下名为 module-info.java 的文件中。

以下是一个基本的模块声明:

module com.company.data {
    requires java.sql;
    requires java.logging;

    exports com.company.data.api;
    // 内部包不会被导出
}

导出包的公共成员将被依赖模块访问,而私有成员则无法访问(即使通过反射,具体取决于Java版本设置)。

迁移准备

在着手模块化之前,您需要了解应用程序的结构和依赖关系。

步骤1:分析依赖关系

JDeps(Java依赖分析工具)是一个命令行工具,用于处理Java字节码并分析类之间静态声明的依赖关系。该工具对于迁移规划至关重要。

在您的应用程序上运行 jdeps 以获取依赖关系概览:

jdeps -s myapp.jar

这将提供一个摘要,显示您的应用程序依赖哪些JDK模块。要进行更详细的分析:

jdeps --class-path 'libs/*' -summary -recursive myapp.jar

步骤2:识别JDK内部API使用情况

最关键的迁移挑战之一是识别对JDK内部API的依赖,这些API在Java 9+中不再可访问。使用 jdeps 查找这些有问题的依赖:

jdeps -jdkinternals myapp.jar

如果您使用了内部API,jdeps 可能会建议替换方案以帮助您更新代码。例如,如果您的代码使用了 sun.misc.BASE64Encoderjdeps 将建议改用 java.util.Base64

步骤3:创建依赖关系图

理解应用程序的一个有效方法是使用分层图,以分层方式表示依赖关系。这有助于识别哪些组件可以迁移以及迁移顺序。

您可以生成DOT文件用于可视化:

jdeps --dot-output . myapp.jar

然后使用像Graphviz这样的工具或像 webgraphviz.com 这样的在线服务来可视化生成的 .dot 文件。

步骤4:识别循环依赖

JPMS禁止模块之间存在循环依赖。解决此问题的常用方法是创建一个包含共享代码的中间模块,从而打破循环。

例如,如果模块A依赖于B,而模块B又依赖于A,您需要将共享功能提取到一个新的模块C中,让A和B都依赖于C。

迁移策略:自顶向下 vs 自底向上

JPMS支持两种主要的迁移策略,每种策略适用于不同的场景。

自底向上迁移

在此策略中,最初所有JAR文件都位于类路径上。然后,通过一个逐步的过程,所有JAR都可以逐个迁移到模块路径上。

过程:

  1. 选择最低层级的模块:从对您的其他JAR没有依赖关系的模块开始。
  2. 添加 module-info.java:创建一个模块描述符,定义其依赖项和导出项。
  3. 移动到模块路径:此JAR成为命名模块。
  4. 向上重复:继续处理下一层级的模块。

一个三层应用程序(Utils → Services → Application)的示例进展:

# 步骤1:模块化Utils(无内部依赖)
jdeps --generate-module-info . utils.jar

# 步骤2:模块化Services(依赖于Utils)
jdeps --generate-module-info . services.jar

# 步骤3:模块化Application(依赖于两者)
jdeps --generate-module-info . application.jar

自底向上迁移允许底层模块强制执行强封装,而上层模块在过渡期间仍可以访问模块化和非模块化的依赖项。

自顶向下迁移

在此策略中,最初所有JAR文件都位于模块路径上,因此所有未迁移的项目都被视为自动模块。然后,您选择依赖层次结构中尚未迁移的最高层级的项目。

过程:

  1. 将所有JAR放在模块路径上:非模块化JAR成为自动模块。
  2. 从顶部开始:首先模块化您的主应用程序模块。
  3. 添加 module-info.java:使用自动模块名称定义依赖项。
  4. 向下工作:逐渐用适当的命名模块替换自动模块。

自顶向下迁移鼓励在更高层级进行仔细的API设计,并允许您的主应用程序代码立即从模块化中受益。

理解模块类型

在迁移过程中,您将处理三种类型的模块:

命名模块(显式模块)

显式模块遵循其模块声明(module-info.java)中定义的依赖项和API规则。这些是具有完整模块描述符的完全模块化JAR。

自动模块

自动模块是位于模块路径上的普通JAR(无模块描述符)。模块名称源自JAR文件名或 MANIFEST.MF 文件中的 Automatic-Module-Name 条目。

自动模块在迁移期间提供了一个桥梁:

  • 它们可以读取所有其他模块
  • 它们导出其所有包
  • 其他命名模块可以依赖它们

未命名模块

类路径上的所有内容都成为未命名模块的一部分。未命名模块可以读取所有其他模块,但不能被命名模块所需。这提供了向后兼容性。

使用 jdeps 生成模块描述符

jdeps 命令可以为指定的JAR文件生成 module-info.java 文件:

# 生成模块描述符
jdeps --generate-module-info output-dir myapp.jar dependency.jar

# 生成开放模块(用于反射密集型代码)
jdeps --generate-open-module output-dir myapp.jar

生成的 module-info.java 文件提供了一个起点,但您应该审查并完善它们,以正确封装内部包。

处理常见的迁移挑战

拆分包

一个包不能存在于多个模块中。如果您有拆分包,您需要进行重构或合并,以避免同一个包分布在多个模块中。

反射密集型框架

如果框架使用反射(例如 Spring, Hibernate),请声明 opens 以允许访问:

module com.company.data {
    requires java.sql;

    exports com.company.data.api;
    opens com.company.data.entities to org.hibernate.core;
}

opens 指令允许对包进行反射访问,而无需将其导出到公共API。

第三方库

某些第三方库可能尚未模块化;在这些情况下,请使用自动模块或模块修补。随着生态系统的成熟,越来越多的库正在添加模块支持。

对于没有模块的库,请确保它们的 MANIFEST.MF 中有一个 Automatic-Module-Name,或者向上游贡献一个。

更新构建工具

Maven 配置
使用 maven-compiler-plugin 并配置 --module-path--patch-module 选项:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <release>17</release>
    </configuration>
</plugin>

Gradle 配置
使用支持模块化的 java-library 插件:

plugins {
    id 'java-library'
}

java {
    modularity.inferModulePath = true
}

实践示例:模块化一个三层应用程序

让我们逐步完成一个典型的分层应用程序(包含数据层、服务层和API层)的模块化过程。

步骤1:数据层模块

// 数据层的 module-info.java
module com.company.app.data {
    requires java.sql;
    requires java.logging;

    exports com.company.app.data.api;
    // 内部包不导出
}

编译和打包:

javac -d mods/com.company.app.data src/com/company/app/data/**/*.java
jar --create --file mods/com.company.app.data.jar -C mods/com.company.app.data .

步骤2:服务层模块

// 服务层的 module-info.java
module com.company.app.service {
    requires com.company.app.data;
    requires java.logging;

    exports com.company.app.service.api;
}

使用模块路径编译:

javac --module-path mods -d mods/com.company.app.service \
    src/com/company/app/service/**/*.java

步骤3:应用程序模块

// 应用程序的 module-info.java
module com.company.app {
    requires com.company.app.service;
    requires com.company.app.data;
    requires java.logging;
}

运行模块化应用程序:

java --module-path mods -m com.company.app/com.company.app.Main

测试您的模块化应用程序

运行完整的单元测试和集成测试,以验证模块边界。测试确保:

  • 所有必需的模块都已正确声明
  • 导出的包对于依赖模块是足够的
  • 没有发生非法访问尝试
  • 基于反射的框架能通过 opens 指令正常工作

用于兼容性的命令行选项

在迁移期间,您可能需要临时的变通方案。Java提供了几个命令行选项:

  • --add-exports:将一个模块的包导出给另一个模块
  • --add-opens:为一个包开放深度反射权限
  • --add-reads:使一个模块读取另一个模块
  • --patch-module:覆盖或扩充模块的内容

这些选项应作为临时解决方案,同时您需要适当地重构代码。

迁移后的好处

成功迁移后,您将获得:强封装(模块显式控制API)、更好的依赖管理(具有清晰的模块边界)、更高的安全性(通过减少攻击面)以及更快的启动时间(通过优化的模块图)。

模块化的JDK本身展示了这些好处——您可以使用 jlink 创建仅包含应用程序所需模块的自定义运行时映像,从而显著减小部署大小。

最佳实践

成功迁移到JPMS需要遵循成熟的模式并避免常见的陷阱。以下最佳实践源自现实世界的迁移经验,代表了已成功模块化大型Java应用程序的团队的集体智慧。遵循这些指南将帮助您避免代价高昂的错误,并确保顺利过渡到模块化架构。

最佳实践 描述 重要性
从 Java 9+ 兼容性开始 在尝试模块化之前,确保您的应用程序可以在 Java 9+ 上运行 将运行时问题与模块相关问题分开,使调试更容易
增量迁移 不要试图一次性模块化所有内容;选择自底向上或自顶向下策略并系统地进行 降低风险,允许从早期模块中学习,并保持应用程序稳定性
策略性使用自动模块 将自动模块视为迁移期间的临时桥梁,而不是最终目标 自动模块缺乏适当的封装,应替换为显式模块
审查生成的描述符 始终审查和完善由 jdeps 生成的 module-info.java 文件 生成的描述符是起点,可能会过度导出或错过重要的封装机会
积极封装 仅导出真正属于公共API的包;隐藏内部包 强封装防止紧耦合,使重构更安全
文档化模块依赖关系 维护关于模块边界、职责和依赖关系的清晰文档 帮助团队理解模块图并在开发过程中做出明智决策
彻底测试 在每次模块迁移后运行全面的单元测试和集成测试 模块化改变了可见性和访问模式,可能会破坏基于反射的代码
监控非法访问 在测试期间使用 --illegal-access=warn--illegal-access=deny 在内部API导致生产问题之前识别依赖它们的代码
规划循环依赖 通过将共享功能提取到单独的模块中,及早识别并解决循环依赖 JPMS禁止模块间的循环依赖,因此必须对它们进行重构
一致的模块版本管理 在模块生态系统中使用一致的版本控制方案 简化依赖管理并使故障排除更容易

结论

将遗留Java单体应用迁移到使用JPMS的模块化Java是一项重大但有益的工作。通过周密的规划、重构和测试,您可以获得更好的可维护性、性能和安全性等好处。

关键是为您的应用程序选择正确的策略——对于依赖关系较少的库和组件使用自底向上,对于希望在应用程序级别立即受益的应用使用自顶向下。广泛使用 jdeps 来了解您当前的状态并指导您的迁移决策。

虽然模块化可以说是Java有史以来最令人望而生畏的特性之一,并且适当的模块在Java世界中仍然很少见,但对于大规模应用程序而言,强封装、显式依赖关系和更高的安全性所带来的好处使得迁移工作是值得的。


【注】本文译自:Java Platform Module System: Migration Strategies for Legacy Applications

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注