使用Java模块的多模块Maven应用程序

2023/05/24

1. 概述

Java平台模块系统(JPMS)为Java应用程序增加了更高的可靠性、更好的关注点分离和更强大的封装。但是,它不是构建工具,因此它缺乏自动管理项目依赖项的能力

当然,我们可能想知道是否可以在模块化应用程序中使用成熟的构建工具,如MavenGradle

其实,我们可以!在本教程中,我们将学习如何使用Java模块创建多模块Maven应用程序

2. 在Java模块中封装Maven模块

由于模块化和依赖管理在Java中并不是相互排斥的概念,我们可以将JPMS与Maven等无缝集成,从而利用两全其美的优势。

在一个标准的多模块Maven项目中,我们添加一个或多个子Maven模块,方法是将它们放在项目的根文件夹下,并在父POM的<modules>部分中声明它们。

反过来,我们编辑每个子模块的POM并通过标准<groupId>、<artifactId>和<version>坐标指定其依赖项。

Maven中的反应器机制负责处理多模块项目,负责以正确的顺序构建整个项目。

在这种情况下,我们将基本上使用相同的设计方法,但有一个微妙但基本的变体:我们将通过向其添加模块描述符文件module-info.java将每个Maven模块包装到一个Java模块中

3. 父Maven模块

为了演示模块化和依赖管理如何很好地协同工作,我们将构建一个基本的Demo多模块Maven项目,其功能将缩小到仅从持久层获取一些域对象

为了保持代码简单,我们将使用普通Map作为存储域对象的底层数据结构。当然,我们可以轻松地进一步切换到成熟的关系型数据库。

让我们从定义父Maven模块开始,为此,让我们创建一个名为multimodulemavenproject的根项目目录(但它可以是其他任何名称),并向其中添加父pom.xml文件:

<groupId>cn.tuyucheng.taketoday.multimodulemavenproject</groupId>
<artifactId>multimodulemavenproject</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>multimodulemavenproject</name>
 
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
 
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

在父POM的定义中有一些细节值得注意。

首先,由于我们使用的是Java 11,因此我们的系统上至少需要Maven 3.5.0,因为Maven从该版本开始支持Java 9及更高版本

而且,我们还需要至少3.8.0版本的Maven编译器插件。因此,让我们确保在Maven Central上检查该插件的最新版本。

4. 子Maven模块

请注意,到目前为止,父POM没有声明任何子模块

由于我们的Demo项目将从持久层获取一些域对象,因此我们将创建四个子Maven模块:

  1. entitymodule:将包含一个简单的域类
  2. daomodule:将持有访问持久层所需的接口(一个基本的DAO合约)
  3. userdaomodule:将包含daomodule接口的实现
  4. mainappmodule:项目的入口点

4.1 entitymodule Maven模块

现在,让我们添加第一个子Maven模块,它只包含一个基本的域类。

在项目根目录下,我们创建entitymodule/src/main/java/cn/tuyucheng/taketoday/entity目录结构,并添加一个User类:

public class User {

    private final String name;

    // standard constructor / getter / toString
}

接下来,让我们包含模块的pom.xml文件:

<parent>
    <groupId>cn.tuyucheng.taketoday.multimodulemavenproject</groupId>
    <artifactId>multimodulemavenproject</artifactId>
    <version>1.0</version>
</parent>
 
<groupId>cn.tuyucheng.taketoday.entitymodule</groupId>
<artifactId>entitymodule</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>entitymodule</name>

正如我们所见,实体模块与其他模块没有任何依赖关系,也不需要额外的Maven工件,因为它只包含User类。

现在,我们需要将Maven模块封装成Java模块。为此,我们只需将以下模块描述符文件(module-info.java )放在entitymodule/src/main/java目录下:

module cn.tuyucheng.taketoday.entitymodule {
    exports cn.tuyucheng.taketoday.entitymodule;
}

最后,让我们将子Maven模块添加到父POM中:

<modules>
    <module>entitymodule</module>
</modules>

4.2 daomodule Maven模块

让我们创建一个新的Maven模块,它将包含一个简单的接口,这对于定义从持久层获取泛型类型的抽象契约很方便。

事实上,有一个非常有说服力的理由将这个接口放在一个单独的Java模块中,通过这样做,我们有一个抽象的、高度解耦的契约,它很容易在不同的上下文中重用。从本质上讲,这是依赖倒置原则的另一种实现,它产生了更灵活的设计。

因此,让我们在项目地根目录下创建daomodule/src/main/java/cn/tuyucheng/taketoday/dao目录结构,并在其中添加Dao接口:

public interface Dao<T> {

    Optional<T> findById(int id);

    List<T> findAll();
}

现在,让我们定义模块的pom.xml文件:

<parent>
    // parent coordinates
</parent>
 
<groupId>cn.tuyucheng.taketoday.daomodule</groupId>
<artifactId>daomodule</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>daomodule</name>

新模块也不需要其他模块或工件,因此我们只需将其包装到Java模块中即可,让我们在daomodule/src/main/java目录下创建模块描述符:

module cn.tuyucheng.taketoday.daomodule {
    exports cn.tuyucheng.taketoday.daomodule;
}

最后,让我们将模块添加到父POM中:

<modules>
    <module>entitymodule</module>
    <module>daomodule</module>
</modules>

4.3 userdaomodule Maven模块

接下来,让我们定义包含Dao接口实现的Maven模块。

在项目的根目录下,我们创建userdaomodule/src/main/java/cn/tuyucheng/taketoday/userdao目录结构,并添加如下UserDao类:

public class UserDao implements Dao<User> {

    private final Map<Integer, User> users;

    // standard constructor

    @Override
    public Optional<User> findById(int id) {
        return Optional.ofNullable(users.get(id));
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(users.values());
    }
}

简单地说,UserDao类提供了一个基本的API,允许我们从持久层获取User对象

为了简单起见,我们使用Map作为支持数据结构来持久化域对象。当然,可以提供更彻底的实现,例如使用Hibernate的实体管理器

现在,让我们定义Maven模块的POM:

<parent>
    // parent coordinates
</parent>
 
<groupId>cn.tuyucheng.taketoday.userdaomodule</groupId>
<artifactId>userdaomodule</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>userdaomodule</name>
 
<dependencies>
    <dependency>
        <groupId>cn.tuyucheng.taketoday.entitymodule</groupId>
        <artifactId>entitymodule</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>cn.tuyucheng.taketoday.daomodule</groupId>
        <artifactId>daomodule</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

在这种情况下,情况略有不同,因为userdaomodule模块需要entitymodule和daomodule模块,这就是我们将它们作为依赖项添加到pom.xml文件中的原因。

我们还需要将这个Maven模块封装成一个Java模块,因此,让我们在userdaomodule/src/main/java目录下添加以下模块描述符:

module cn.tuyucheng.taketoday.userdaomodule {
    requires cn.tuyucheng.taketoday.entitymodule;
    requires cn.tuyucheng.taketoday.daomodule;
    provides cn.tuyucheng.taketoday.daomodule.Dao with cn.tuyucheng.taketoday.userdaomodule.UserDao;
    exports cn.tuyucheng.taketoday.userdaomodule;
}

最后,我们需要将这个新模块添加到父POM中:

<modules>
    <module>entitymodule</module>
    <module>daomodule</module>
    <module>userdaomodule</module>
</modules>

从高层次的角度来看,很容易看出pom.xml文件和模块描述符扮演着不同的角色。尽管如此,它们还是可以很好地互补。

假设我们需要更新entitymodule和daomodule Maven工件的版本,我们可以很容易地做到这一点,而不必更改模块描述符中的依赖关系,Maven会负责为我们包含正确的工件。

同样,我们可以通过修改模块描述符中的“provides..with”指令来更改模块提供的服务实现。

当我们一起使用Maven和Java模块时,我们会收获很多。前者带来了自动、集中依赖管理的功能,而后者则提供了模块化的内在优势

4.4 mainappmodule Maven模块

此外,我们需要定义包含项目主类的Maven模块。

正如我们之前所做的,我们在根目录下创建mainappmodule/src/main/java/mainapp目录结构,并在其中添加如下Application类:

public class Application {

    public static void main(String[] args) {
        Map<Integer, User> users = new HashMap<>();
        users.put(1, new User("Julie"));
        users.put(2, new User("David"));
        Dao userDao = new UserDao(users);
        userDao.findAll().forEach(System.out::println);
    }
}

Application类的main()方法非常简单。首先,它用几个User对象填充一个HashMap;接下来,它使用UserDao实例从Map中获取它们,然后将它们显示到控制台。

另外,我们还需要定义模块的pom.xml文件:

<parent>
    // parent coordinates
</parent>
 
<groupId>cn.tuyucheng.taketoday.mainappmodule</groupId>
<artifactId>mainappmodule</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>mainappmodule</name>
 
<dependencies>
    <dependency>
        <groupId>cn.tuyucheng.taketoday.entitymodule</groupId>
         <artifactId>entitymodule</artifactId>
         <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>cn.tuyucheng.taketoday.daomodule</groupId>
        <artifactId>daomodule</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>cn.tuyucheng.taketoday.userdaomodule</groupId>
        <artifactId>userdaomodule</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

该模块的依赖关系是不言自明的,所以,我们只需要将模块放在Java模块中即可。因此,在mainappmodule/src/main/java目录结构下,让我们包含模块描述符:

module cn.tuyucheng.taketoday.mainappmodule {
    requires cn.tuyucheng.taketoday.entitypmodule;
    requires cn.tuyucheng.taketoday.userdaopmodule;
    requires cn.tuyucheng.taketoday.daopmodule;
    uses cn.tuyucheng.taketoday.daopmodule.Dao;
}

最后,让我们将这个模块添加到父POM中:

<modules>
    <module>entitymodule</module>
    <module>daomodule</module>
    <module>userdaomodule</module>
    <module>mainappmodule</module>
</modules>

所有的子Maven模块都已经就位,并整齐地封装在Java模块中,项目的结构如下所示:

multimodulemavenproject (the root directory)
pom.xml
|-- entitymodule
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- cn
                |-- tuyucheng
                    |-- taketoday
                        |-- entity
                            |-- User.java
    pom.xml
|-- daomodule
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- cn
                |-- tuyucheng
                    |-- taketoday
                        |-- dao
                            |-- Dao.java
    pom.xml
|-- userdaomodule
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- cn
                |-- tuyucheng
                    |-- taketoday
                        |-- userdao
                            |-- UserDao.java
    pom.xml
|-- mainappmodule
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- cn
                |-- tuyucheng
                    |-- taketoday
                        |-- mainapp
                            |-- Application.java
    pom.xml

5. 运行应用程序

最后,让我们从IDE或控制台运行应用程序。

正如我们所料,当应用程序启动时,我们应该会看到一些User对象打印到控制台:

User{name=Julie}
User{name=David}

6. 总结

在本教程中,我们以实用的方式学习了如何在使用Java模块的基本多模块Maven项目的开发中让Maven和JPMS并行工作。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章