当前位置:Java -> Maven故事

Maven故事

回归到 Maven 的新博客 - 并没有传奇故事,只是对我旧项目的简单更新。就像与一个老朋友见面一样,分享一些新鲜事。在这篇博客中,我们将深入了解 Maven 的世界,探索我对一个旧项目所做的调整和改进。无论你是 Maven 老手还是刚开始接触,让我们一起共同探索我的 Maven 之旅吧!

TL;DR

  • 挑战: 如何在一个具有许多部分的项目中使用 Ant 升级应用服务器。
  • 解决方案: 使用 Maven 的多模块计划和版本控制的 BOM(Bill Of Materials),使升级和部件管理变得更加简单。
  • 旅程: 面对诸如循环依赖和复制项目等问题,Maven 的工具和多模块系统提供了很大的帮助。
  • 关键收获: 分享在移动过程中学到的实用经验教训,展示了 Maven 是如何使项目变得更好并为未来的改进做好准备的。

从前,在工程的岁月

在一个技术探险中,我被要求升级用于 Glassfish 或 WebLogic 等服务器上的产品的应用服务器。目标是提升安全性并为未来的 JDK 升级做准备。该产品包括 20 多个 EJB 项目和 20 多个 war 项目被打包成一个 EAR。

这些 EJB(*-ejb.jar) 和 WEB (*.war 项目) 是基于 Ant 的 Java 项目。

Ant 的挑战

在这个 Ant 项目的世界中,出现了一个大问题——确定和升级应用服务器相关的依赖关系。目前的设置使用了两个项目来持有各种依赖项,这使得难以知道哪个项目使用了什么。在服务器升级期间进行更新变成了一个手动且容易出错的任务。

Maven 之旅

为了解决这些问题,我旨在简化未来的升级并集中依赖关系控制。虽然 Ant IVY 看起来很有帮助,但我担心管理依赖项和混乱的项目结构。Maven 和 Gradle 都是选择,但因为 Maven 拥有强大的工具和插件供应用服务器使用,Maven 占据上风。

借鉴开源项目和 Spring 项目的结构,Maven 的多模块与 Bill Of Materials (BOM) 成为一个引人注目的选择。

选择道路

为了更容易理解,可以将下面的图片视为我们组织项目的旧方式。

Ant 中的 Module_A 项目被编译成四个二进制文件。然而,这与 Maven 要求每个 Ant 项目都要维护一个 pom.xml 的政策相矛盾。

在这里出现了一个关键问题:我能够将 Maven 无缝集成到现有的 Ant 项目结构中吗?

铺设新道路

答案在于创建一个父项目,其 POM 作为 BOM。这种方法提供了一个精炼的项目结构,符合 Maven 的目录布局标准。

最终的设置变成了一个 Maven 多模块项目,/Application/pom.xml 控制着所有模块的版本。application_ear 项目将 EJB 和 WAR 文件打包成可部署的 EAR,而 BuildModules 则创建了一个可执行的应用服务器 zip 包。

在这种情况下,/Module_A 本身是一个 Maven 模块,其父项目为 /Application。此外,在旧项目结构中为 Module_A 创建的每个二进制文件,我建立了一个以上级为父项目的 Maven 模块。这种设计反映了我们项目的理想 Maven 多模块项目结构。

  • *_ejb 项目将 EJB 类编译成 module_a_ejb.jar。
  • *_web 项目将 WAR 类组装成 module_a_web.war。
  • 此外,*_interface 项目将接口和 DTOs 捆绑在一起形成 module_a_interface.jar 以解决模块间的依赖关系。

我将在本博客的后面部分深入探讨这种 *_interface 打包背后有趣的原因。

深入了解

如前所述,我建立了一个名为 /Application 的主要 Maven 项目,将所有子域项目列在其下方。

关于版本控制,/Application/pom.xml 充当一个 BOM,覆盖了中心化的依赖和插件管理。下面的 XML 片段展示了 log4j-1.2-api 和 Gson 的版本声明。

<properties>
   <version.log4j.1.2.api>2.19.0</version.log4j.1.2.api>
   <version.gson>2.8.6</version.gson>
</properties>
<dependencyManagement>
  <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-1.2-api</artifactId>
      <version>${version.log4j.1.2.api}</version>
  </dependency>

  <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>${version.gson}</version>
  </dependency>
</dependencyManagement>


因此,所有继承或使用这些依赖项的子模块都无需指定版本信息,正如后续的 XML 片段所示:

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-1.2-api</artifactId>
	<scope>compile</scope>
</dependency>
<dependency>
	<groupId>com.google.code.gson</groupId>
	<artifactId>gson</artifactId>
	<scope>compile</scope>
</dependency>


此外,继承的概念在 Maven POMs 中扮演着一个角色,在这里,Maven POMs 从父 POM 中继承值。 这包括在父级定义的 Maven 插件,为子模块插件提供默认实现,如下所示:
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-jar-plugin</artifactId>
	<version>${version.maven.jar.plugin}</version>
	<configuration>
		<archive>
			<index>false</index>
			<manifestEntries>
				<Built-By>${user.name}</Built-By>
				<Build-Date>${maven.build.timestamp}</Build-Date>
				<Build-Jdk>${java.vendor}</Build-Jdk>
			</manifestEntries>
			<manifest>
				<addClasspath>false</addClasspath>
			</manifest>
		</archive>
  </configuration>
</plugin>
处理内部或内部依赖,我们在内部网络中建立了一个类似于 JFrog Artifactory 的内部 Maven 仓库。 为我们的产品依赖项制定了一个专门的仓库,并且已经集成了这个仓库以进行有效的依赖解析。
<repositories>
	<repository>
      	<name>Internal Dev</name>
      	<id>maven-dev</id>
      	<url>${maven.dev.repo.url}</url>
      	<layout>default</layout>
      	<releases>
        	<updatePolicy>never</updatePolicy>
      	</releases>
      	<snapshots>
       		<updatePolicy>always</updatePolicy>
      	</snapshots>
	</repository>
</repositories>
为了确保在编译期间的 Java 发行和版本的一致性,我们遇到了在 Ant 构建模型中使用不同 JDK版本的意外编译。 解决了这个问题,我使用了maven enforcer plugin,这个插件允许我们对 JDK 版本和发行版本 强制执行规则,就像下面的示例中说明的那样:
 <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>${version.maven.enforcer.plugin}</version>
        <executions>
          <execution>
            <id>enforce-java</id>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <requireJavaVendor>
                  <excludes>
                    <exclude>${oracle.jdk}</exclude>
                  </excludes>
                  <includes>
                    <include>${eclipse.temurin.jdk}</include>
                  </includes>
                </requireJavaVendor>
              </rules>    
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
面临的挑战  在我们顺利迁移过程中,我们遇到了一些障碍: 1. 创建 160+ 个项目: 超过 40 个主模块,总计 160 多个项目和子模块,用默认配置和文件在所有项目中复制项目结构都非常耗时。 为了简化这个流程,我使用了 Maven 项目模板工具 Archetype。 这个工具可以创建带有资源文件和子模块默认类的项目模板结构。 2. 循环依赖: 由于旧项目基于 Ant,并且缺少严格的组件依赖模型,因此在多个领域的项目编译过程中出现了循环依赖。 Maven 不允许循环依赖。 为了解决这个问题,我在子模块下引入了 *-ejb、*-web 和 *-interfaces 项目,按关注点对 Java 类进行了分离。 INTERFACE 项目作为救世主,通过仅持有 DTO 和 EJB 接口而没有项目间依赖来解决循环依赖。 其他模块的 EJB/WEB 项目现在将自己模块或其他模块的 *-interfaces 作为依赖项。 3. 分发打包: 创建一个将 *.ear 与应用服务器捆绑成一个 zip 文件的产品分发是必要的。 由于完整的产品构建脚本基于 Ant,因此我决定使用 maven assembly plugin进行打包。 最初,我考虑将组件脚本放在父项目 /Application 中,假设构建父项目将覆盖所有子模块。 然而,这种方法导致了所有子模块上继承的组件行为,导致了有关分发依赖和配置的混乱。 尽管尝试将继承限制在子模块中,但这种解决方案被证明是无效的。 在参考 assembly plugin guide后,我创建了一个名为 BuildModules 的分发项目。 该项目能够在 EAR 和应用服务器之外生成 zip 文件,并具有对 EAR 模块的依赖,而 EAR 模块又具有对子模块二进制文件的传递依赖(*-ejb.jar 和 *.war)。 压轴之作 尽管超出了预期的时间表,项目的最终结果是值得称赞的。 随后对应用服务器进行的次要升级非常容易地完成,仅通过简单的 Maven 属性更改。 目前,OWASPdependency-check-maven 对已知发行漏洞的依赖进行了更有效的扫描。 将项目的依赖项升级以与兼容的 JDK 版本或发行版本对齐,证明是一个简单而不复杂的过程。 最终,这是一个完美的成功故事。 展望未来,我们将深入研究 Maven 改进,并在下一阶段为产品开发定制插件。 感谢你加入我们的旅程。 我期待在即将到来的博客中分享更多见解。

推荐阅读: 剑指offer 24.反转链表

本文链接: Maven故事