maven多模組管理

KerryWu發表於2023-02-10

基於 springboot + maven 開發,這裡整理一波多模組(module)專案的開發管理。

1. 聚合與繼承

多模組的專案結構,基本離不開聚合繼承兩種maven管理方式。二者不是相悖的,很多專案結構是二者組合在一起管理的。

1.1. 聚合

目的

可以一次構建多個模組的專案。

當一個專案中有多個需要構建的模組專案時,如果每個模組單獨構建,太費工作量。最好可以基於某個模組一次構建配置的所有模組,這就是聚合。

應用方式

先確定一個專門用於打包的maven專案,針對該專案pom 檔案做以下的特殊處理:

  • <packaging> 值為 pom。(後續會講 packaging 值的區別)
  • 基於<modules> 申明需要打包的所有模組,來實現模組的聚合。

1.2. 繼承

1、目的

減少重複的配置。

繼承比較好理解,類似於java類的繼承,子模組可以自動繼承父模組的一些屬性。在maven專案中,可以把多個子模組共同的配置放到父模組中,那麼子模組就不需要重複維護配置了。

2、應用方式

先確定一個父模組專案,針對該專案pom 檔案做以下的特殊處理:

  • <packaging> 值為 pom。(後續會講 packaging 值的區別)
  • 子模組中需要宣告:(1)<parent> 為父模組的資訊;(2)<relativePath>為父模組 pom 的相對路徑。當專案構建時,Maven會首先根據 <relativePath> 檢查父POM,如果找不到,再從本地倉庫查詢。
3、配置中可繼承的元素
  • groupId :專案組 ID ,專案座標的核心元素;
  • version :專案版本,專案座標的核心元素;
  • properties :自定義的 Maven 屬性;
  • dependencies :專案的依賴配置;
  • dependencyManagement :醒目的依賴管理配置;
  • repositories :專案的倉庫配置;
  • build :包括專案的原始碼目錄配置、輸出目錄配置、外掛配置、外掛管理配置等;
  • reporting :包括專案的報告輸出目錄配置、報告外掛配置等;
  • description :專案的描述資訊;
  • organization :專案的組織資訊;
  • inceptionYear :專案的創始年份;
  • url :專案的 url 地址
  • develoers :專案的開發者資訊;
  • contributors :專案的貢獻者資訊;
  • distributionManagerment :專案的部署資訊;
  • issueManagement :缺陷跟蹤系統資訊;
  • ciManagement :專案的持續繼承資訊;
  • scm :專案的版本控制資訊;
  • mailingListserv :專案的郵件列表資訊;
4、dependencies和dependencyManagement(plugins與pluginManagement)

(1)dependencies

如果父專案pom中定義的是單獨的 dependencies,則代表引用對應的所有依賴項。其所有子專案的pom中,就算沒有引入父專案中定義的依賴,也自動會繼承父專案pom檔案 dependencies 中的所有依賴項。

(2)dependencyManagement

父專案pom中只是宣告依賴,並不實現引入,因此子專案需要顯示的宣告需要的依賴。當父專案中申明過一些依賴專案,但如果不在子專案中宣告依賴,是不會從父專案中繼承的。

另外,只有在子專案中寫了該依賴項,並且沒有指定具體版本,才會從父專案中繼承該項,並且version和scope都讀取自父pom;
如果子專案中指定了版本號,那麼會使用子專案中指定的version和scope版本。

一般推薦用 dependencyManagement,因為配置起來更靈活,子專案應該按需配置需要的依賴項,以及可自定義版本號。但 dependency 也有它的作用,看實際情況搭配了。

pluginspluginManagement 的關係就如同 dependenciesdependencyManagement,這裡就不多說了。

程式碼不可被繼承

要注意,前面講的繼承都是 pom 檔案配置中的元素可被繼承。但父模組中的程式碼是不可以被繼承的。

如果你說,想和子模組依賴公共的配置一樣,想將子模組公共呼叫的程式碼放到父模組中,這樣子模組就能公共呼叫了。非放父模組也行,可以在子模組中引入 build-helper-maven-plugin 等外掛,將父模組的程式碼路徑手動新增到子模組,如下面配置外掛:

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
        <shared.srcdir>${project.basedir}/src</shared.srcdir>
    </properties>

    ... ...

    <build>
        <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>build-helper-maven-plugin</artifactId>
                    <version>${build-helper-maven-plugin.version}</version>
                    <inherited>true</inherited>
                    <executions>
                        <execution>
                            <id>add-source</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>${shared.srcdir}/main/java</source>
                                </sources>
                            </configuration>
                        </execution>
                        <execution>
                            <id>add-test-source</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-test-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>src/it/java</source>
                                    <source>${shared.srcdir}/test/java</source>
                                </sources>
                            </configuration>
                        </execution>
                        <execution>
                            <id>add-test-resource</id>
                            <phase>generate-resources</phase>
                            <goals>
                                <goal>add-test-resource</goal>
                            </goals>
                            <configuration>
                                <resources>
                                    <resource>
                                        <directory>src/it/resources</directory>
                                    </resource>
                                    <resource>
                                        <directory>${shared.srcdir}/test/resources</directory>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
        </plugins>
    </build>

這的做法比較麻煩,還不如定義一個公共的模組,將需要多模組複用的程式碼都放入該模組中。然後在子模組的pom中引用公共模組的依賴即可。後面的例子中會有這種體現(demo-comm)。

1.3. 聚合與繼承比較

聚合是為了方便快速構建專案,而繼承是為了消除重複配置。

相同點:打包方式都是pom,除了pom檔案之外沒有其他實際的內容。

不同點:聚合是聚合模組透過module引用被聚合模組,而繼承是子模組透過parent引用父模組。

實際專案中通常把聚合和繼承結合起來一起使用。parent專案既是聚合模組,也是父模組。

2. 多模組示例

下面會舉一個多模組專案的簡單例子,便於瞭解核心的配置過程。

2.1. 程式碼結構

示例的maven專案就叫 demo-service。刪掉了 src目錄,父pom 中定義的 artifactId 為 demo-parent。下面簡單列了三個子模組:

  • demo-api: 對外提供Http API的可啟動模組。
  • demo-mqc: MQ消費端的可啟動模組。
  • demo-comm:被其他子模組公共依賴的不可啟動模組。

程式碼結構如下:

.
├── HELP.md
├── demo-api
│   ├── HELP.md
│   ├── demo-api.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── demoapi
│           │               ├── DemoApiApplication.java
│           │               └── controller
│           │                   └── DemoController.java
│           └── resources
│               └── application.properties
├── demo-comm
│   ├── HELP.md
│   ├── demo-comm.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── democomm
│           │               ├── config
│           │               │   └── DemoConfig.java
│           │               ├── mapper
│           │               │   └── DemoMapper.java
│           │               └── service
│           │                   └── DemoService.java
│           └── resources
│               ├── application.properties
│               └── mapper
│                   └── DemoMapper.xml
├── demo-mqc
│   ├── HELP.md
│   ├── demo-mqc.iml
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── pers
│           │       └── kerry
│           │           └── demomqc
│           │               ├── DemoMqcApplication.java
│           │               └── consumer
│           │                   └── DemoConsumer.java
│           └── resources
│               └── application.properties
├── demo-service.iml
├── mvnw
├── mvnw.cmd
└── pom.xml

當然還可以基於業務再做模組細分,如:

  • 橫向:(1)可增加 demo-socket 服務處理長連線;(2)可增加 demo-job 服務處理如 xxl-job 之類的服務。
  • 縱向:(1)demo-api 如果有多個業務 api,可以在上面在提煉一層 api-apps 模組(demo-parent -> api-apps -> order-api、log-api);(2)公共模組的服務如果可細分,不希望其他模組依賴全量的公共模組程式碼,也可以提煉一層 comm-apps(demo-parent -> comm-apps -> order-comm、log-comm)。

2.2. pom配置(demo-parent)

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-parent</artifactId>
    <version>${revision}</version>
    <name>demo-parent</name>
    <description>demo-service</description>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <revision>0.0.1-SNAPSHOT</revision>
        <main.basedir>${project.basedir}</main.basedir>
        <spring-boot-starter.version>2.6.3</spring-boot-starter.version>
        <lombok.version>1.18.24</lombok.version>
        <rocketmq.version>2.2.0</rocketmq.version>
        <flatten-maven-plugin.version>1.2.7</flatten-maven-plugin.version>
        <maven-antrun-plugin.version>3.0.0</maven-antrun-plugin.version>
    </properties>

    <modules>
        <module>demo-api</module>
        <module>demo-mqc</module>
        <module>demo-comm</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>pers.kerry</groupId>
                <artifactId>demo-comm</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring-boot-starter.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-spring-boot-starter</artifactId>
                <version>${rocketmq.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>flatten-maven-plugin</artifactId>
                    <version>${flatten-maven-plugin.version}</version>
                    <configuration>
                        <updatePomFile>true</updatePomFile>
                        <flattenMode>clean</flattenMode>
                    </configuration>
                    <executions>
                        <execution>
                            <id>flatten</id>
                            <phase>process-resources</phase>
                            <goals>
                                <goal>flatten</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>flatten-clean</id>
                            <phase>clean</phase>
                            <goals>
                                <goal>clean</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-antrun-plugin</artifactId>
                    <version>${maven-antrun-plugin.version}</version>
                    <inherited>true</inherited>
                    <executions>
                        <execution>
                            <phase>validate</phase>
                            <goals>
                                <goal>run</goal>
                            </goals>
                            <configuration>
                                <target>
                                    <mkdir dir="${main.basedir}/.git/hooks"/>
                                </target>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
2. pom配置分析

在示例中,就是一個聚合與繼承合二為一的結構,具體分析 pom 特殊元素如下:

  • packaging:無論是聚合還是繼承,都要求父模組或聚合構建模組中該值為pom
  • properties:可定義變數,變數值可以是 ${project.basedir} 這類maven內建變數,也可以是依賴包版本這類常量。建議在 properties 中統一維護所有 <version>,便於變數統一管理。
  • modules:作為聚合結構的專案,需要透過 modules 來申明需要構建的模組。例如,如果註釋掉 <module>demo-api</module>,在 demo-parent 路徑執行 mvn package 命令時,就不會打包 demo-api 的模組。
  • dependencyManagement:配置 dependencyManagement,意味著父模組只做申明,子模組可以按需引用所需要的依賴項。
  • dependency:demo-comm:demo-comm 作為公共依賴的模組,因為會有多個子模組會依賴。可直接在父模組中申明(基於dependencyManagement方式),定義好 version。這樣需要用到中子模組直接申明即可,不用再定義版本號。
  • src:建議刪除,原因前面有說。
  • spring-boot-maven-plugin:建議刪除,該外掛是構建 springboot 特殊 jar包的,該模組打包方式是pom,用不上。

2.3. pom配置(demo-comm)

1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-comm</artifactId>
    <name>demo-comm</name>
    <description>demo-comm</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    
</project>
2. pom配置分析
  • packaging:雖然 demo-comm 中只是程式碼,並沒有可啟動的fat jar(內建tomcat 的 springboot程式)。但程式碼需供其他模組呼叫,依然是需要配置為 jar型別。
  • properties:繼承了父模組中的屬性,可自定義自己特性的屬性值。
  • dependency:因為只需要2個依賴項,所以只需要引入父模組中2個即可。如果版本沒特殊要求,可不特殊申明,繼承自父模組定義的version。
  • 啟動類:因為無需啟動,建議刪除無用的啟動類。
  • spring-boot-maven-plugin:和啟動項一樣,因為無需啟動,也無需使用 spring-boot-maven-plugin 將專案打包成 fat jar。這個外掛是建立 springboot 專案時預設生成的外掛,但這裡必須刪除掉,否則在專案構建時會報錯。因為該外掛在打包專案時,生成的jar(fat jar)包結構和普通jar結構不同,會導致其他模組依賴該模組程式碼時報錯。

2.4. pom配置(demo-api、demo-mqc)

1、pom.xml(demo-api)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-api</artifactId>
    <name>demo-api</name>
    <description>demo-api</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>pers.kerry</groupId>
            <artifactId>demo-comm</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>${maven-antrun-plugin.version}</version>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
2、pom.xml(demo-mqc)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>demo-parent</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo-mqc</artifactId>
    <name>demo-mqc</name>
    <description>demo-mqc</description>
    <packaging>jar</packaging>

    <properties>
        <main.basedir>${project.basedir}/..</main.basedir>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>pers.kerry</groupId>
            <artifactId>demo-comm</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
3. pom配置分析
  • packaging:兩個模組都是可執行模組,需要打包成正常內建tomcat 的 springboot fat jar的。值為 jar
  • properties:繼承了父模組中的屬性,可自定義自己特性的屬性值。
  • dependency:因為各自功能性,引入各自真正需要的依賴項。如果版本沒特殊要求,可不特殊申明,繼承自父模組定義的version。
  • 啟動類:需要。
  • spring-boot-maven-plugin:需要。因為兩個模組都是可執行模組,需要打包成正常內建tomcat 的 springboot fat jar的。

3. 工具幫助

3.1. maven內建變數

前面在 properties 的介紹中,有提到 maven內建變數,下面是一些總結:

  • ${basedir}:專案根目錄
  • ${project.build.directory}:構建目錄,預設為target
  • ${project.build.outputDirectory}:構建過程輸出目錄,預設為target/classes
  • ${project.build.finalName}:產出物名稱,預設為${project.artifactId}-${project.version}
  • ${project.packaging}:打包型別,預設為jar
  • ${project.xxx}:當前pom檔案的任意節點的內容

3.2. maven 構建(build)過程

maven 構建生命週期定義了一個專案構建和釋出的過程。
簡單來看,一個典型的 Maven 構建(build)生命週期是由以下幾個階段(phase)的序列組成的。

  1. validate 驗證專案:驗證專案是否正確且所有必須資訊是可用的
  2. compile 執行編譯:原始碼編譯在此階段完成
  3. test 測試:使用適當的單元測試框架執行測試
  4. package 打包:建立JAR/WAR包如在 pom.xml 中定義提及的包
  5. verify 檢查:對整合測試的結果進行檢查,以保證質量達標
  6. install 安裝:安裝打包好專案到本地倉庫, 以供其他專案使用。
  7. deploy 釋出:複製最終打包好的工程包到遠端倉庫,以共享給其他專案和開發人員

它們是按照順序執行的,當我們執行 mvn package 時,實際會自動按照 1~4 步驟執行,所以有時會發現自動執行了測試的階段。

不過 maven 只是規定了生命週期的各個階段和步驟,具體事情,由整合到 maven 中的外掛完成。maven 在生命週期的每個階段都設計了外掛介面。使用者可以在介面上根據專案的實際需要繫結第三方的外掛,做該階段應該完成的任務,從而保證所有 maven 專案構建過程的標準化。當然,maven 對大多數構建階段繫結了預設的外掛,透過這樣的預設繫結,又簡化和穩定了實際專案的構建。

有關maven 的內容比較深,這裡就講淺淺的這一點。

3.3. 外掛:flatten-maven-plugin

可以看到,我們在 demo-parent 的 pom檔案中,引入了 flatten-maven-plugin 的外掛。另外所有依賴 demo-parent 的版本時,都用了 ${revision}。這個外掛的作用,就是用來做統一的版本管理。

假設我們將 demo-parent 的 version 寫死為 1.0.0,那麼在所有子模組依賴 <parent> 的地方,version 也要寫成 1.0.0。可一旦我們需要將版本升為 1.0.1,就需要手動將多處所依賴的版本號一一做修改,工作量大也容易出錯。

flatten-maven-plugin 外掛就可以解決這個問題,${revision} 可以被理解成一種佔位符,會全域性的將這個變數的值覆蓋。

3.4. 外掛:maven-antrun-plugin

該外掛提供從Maven內執行Ant任務的功能。例如在 demo-api、demo-mqc 中的pom外掛定義:

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>${maven-antrun-plugin.version}</version>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy todir="${main.basedir}/target" overwrite="true">
                                    <fileset dir="${project.build.directory}">
                                        <include name="*.jar"/>
                                    </fileset>
                                </copy>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
  • <phase>package</phase> 表示外掛要在Maven 的 package 時執行
  • <goal>run</goal> 這時外掛內部的一個執行目標
  • <target></target> 之間可以寫任何Ant支援的task

那麼這段外掛的作用,就是在package階段,將子模組的生成的 jar 包複製到父模組的 /target 路徑中。因為有些 DevOps 部署指令碼,通常只在固定專案路徑中尋找可執行jar包,而不願意深入子模組路徑。

相關文章