Maven Projects Best Practices

本文整理构建Maven项目和模块的最佳实践的关键事项,其中包含依赖、版本、属性、模块划分等关键因素,推荐使用 Maven 3.6.3 及以上版本。 为了便于理解,我们假设有一个 API 网关项目,这个网关项目包含服务端、客户端、通知服务端支持插件。

目标

  • 通过多模块方式组织项目
  • 管理项目版本、依赖,属性
  • 规划模块依赖关系

主项目 POM

每个项目都应该在项目根目录下有一个主 POM 文件,并通过主 POM 文件管理下级子模块。在主 POM 中至少会使用一下标签

  • properties: 定义字符集编码、JDK 版本、插件版本;
  • modules: 下级子模块;
  • pluginRepositories: 插件仓库地址(非必须,主要解决国内访问慢的问题);
  • repositories: 定义 Maven 私服地址;
  • distributionManagement: 定义发布用 Maven 私服地址
  • pluginManagement: 定义管理类插件版本
  • profiles: (非必须)

例如:

<?xml version="1.0" encoding="UTF-8"?>
<build 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.coolbeevip.apigateway</groupId>
  <artifactId>apigateway-parent</artifactId>
  <version>${revision}</version>
  <packaging>pom</packaging>

  <properties>
    <!-- 使用 revision 管理项目版本 -->
    <revision>1.0.0-SNAPSHOT</revision>

    <!-- project -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncodi
      ng>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>

    <!-- plugins version -->
    <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
    <jacoco-maven-plugin.version>0.8.6</jacoco-maven-plugin.version>
    <docker-maven-plugin.version>0.34.1</docker-maven-plugin.version>
    <maven-deploy-plugin.version>2.7</maven-deploy-plugin.version>
    <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
    <sonar-maven-plugin.version>3.8.0.2131</sonar-maven-plugin.version>
    <dependency-check-maven.version>6.5.3</dependency-check-maven.version>

    <!-- sonar plugin -->
    <sonar.host.url>http://private.sonar:59000</sonar.host.url>

    <!-- maven deploy -->
    <distribution.url>private.nexus:8099</distribution.url>
    <distribution.username/>
    <distribution.password/>
  </properties>

  <!-- 下级子模块(后续会将到子模块的划分原则)-->
  <modules>
    <module>apigateway-dependencies</module>
    <module>apigateway-common</module>
    <module>apigateway-server-common</module>
    <module>apigateway-server</module>
    <module>apigateway-server-plugins</module>
    <module>apigateway-client</module>
  </modules>

  <!-- 插件下载仓库 -->
  <pluginRepositories>
    <!-- 国内使用阿里云可能快点 -->
    <pluginRepository>
      <id>aliyun</id>
      <url>https://maven.aliyun.com/repository/public</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </pluginRepository>
  </pluginRepositories>

  <!-- 下载依赖的私有仓库 -->
  <repositories>
    <!-- 国内使用阿里云可能快点 -->
    <repository>
      <id>aliyun</id>
      <name>aliyun</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public</url>
    </repository>
    <repository>
      <id>releases</id>
      <name>releases</name>
      <url>http://${distribution.url}/nexus/repository/releases/</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>snapshots</id>
      <name>Snapshots</name>
      <url>http://${distribution.url}/nexus/repository/snapshots/</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>always</updatePolicy>
      </snapshots>
    </repository>
  </repositories>

  <!-- 发布用私有仓库地址 -->
  <distributionManagement>
    <repository>
      <id>releases</id>
      <name>Release Repository</name>
      <url>
        http://${distribution.username}:${distribution.password}@${distribution.url}/nexus/repository/releases/
      </url>
    </repository>
    <snapshotRepository>
      <id>snapshots</id>
      <name>Snapshot Repository</name>
      <url>
        http://${distribution.username}:${distribution.password}@${distribution.url}/nexus/repository/snapshots/
      </url>
    </snapshotRepository>
  </distributionManagement>

  <!-- 项目管理类插件版本管理(根据自身情况自选) -->
  <build>
    <pluginManagement>
      <plugins>
        <!-- Java 编译插件 -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>${maven-compiler-plugin.version}</version>
          <configuration>
            <compilerArgument>-Xlint:unchecked</compilerArgument>
          </configuration>
        </plugin>
        <!-- Docker 发布插件 -->
        <plugin>
          <groupId>io.fabric8</groupId>
          <artifactId>docker-maven-plugin</artifactId>
          <version>${docker-maven-plugin.version}</version>
        </plugin>
        <!-- Maven 发布插件 -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>${maven-deploy-plugin.version}</version>
        </plugin>
        <!-- 执行测试用例插件 -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>${maven-surefire-plugin.version}</version>
        </plugin>
        <!-- 代码覆盖率插件 -->
        <plugin>
          <groupId>org.jacoco</groupId>
          <artifactId>jacoco-maven-plugin</artifactId>
          <version>${jacoco-maven-plugin.version}</version>
        </plugin>
        <!-- 代码质量管理插件 -->
        <plugin>
          <groupId>org.sonarsource.scanner.maven</groupId>
          <artifactId>sonar-maven-plugin</artifactId>
          <version>${sonar-maven-plugin.version}</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <profiles>
    <profile>
      <!-- 缺陷检查插件 ./mvnw org.owasp:dependency-check-maven:aggregate -->
      <id>owasp</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.owasp</groupId>
            <artifactId>dependency-check-maven</artifactId>
            <version>${dependency-check-maven.version}</version>
            <configuration>
              <name>notifier-dependency-check</name>
              <format>HTML</format>
              <failBuildOnCVSS>9</failBuildOnCVSS>
              <failBuildOnAnyVulnerability>false</failBuildOnAnyVulnerability>
              <failOnError>false</failOnError>
              <skipProvidedScope>true</skipProvidedScope>
              <skipRuntimeScope>true</skipRuntimeScope>
              <skipTestScope>true</skipTestScope>
              <skipDependencyManagement>true</skipDependencyManagement>
              <retireJsAnalyzerEnabled>false</retireJsAnalyzerEnabled>              
              <skipArtifactType>pom</skipArtifactType>
            </configuration>
            <executions>
              <execution>
                <goals>
                  <goal>aggregate</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>  
</project>

使用 Maven Wrapper 安装和管理 Maven 版本

为了降低参与难度,建议在项目中使用 Maven Wrapper 方式管理 Maven,这样参与者使用 mvnw 命令代替 mvn 命令执行时,会自动下载安装 Maven. 同样,您也可以看到我将镜像仓库地址也写到了主 pom 文件中,这也是为了避免参与者去手动修改本地 Maven 的 settings.xml 文件,从而降低参与的难度。

子模块 POM

通常我们为了解耦一个项目,通常会采用多模块的方式按照子系统垂直拆分模块,或则按照基础设施水平拆分模块,总之拆分模块是必须的,拆分的合理性和粒度也决定了开发者的参与门槛。

根据我们这个想象的项目,我们可以按如下方式拆子模块.


<modules>
  <!-- 依赖版本管理、公共参数管理 -->
  <module>apigateway-dependencies</module>
  <!-- 网关的公共代码 -->
  <module>apigateway-common</module>
  <!-- 网关服务的公共代码 -->
  <module>apigateway-server-common</module>
  <!-- 网关服务本身 -->
  <module>apigateway-server</module>
  <!-- 网关服务插件 -->
  <module>apigateway-server-plugins</module>
  <!-- 网关客户端服 -->
  <module>apigateway-client</module>
</modules>
  • 服务端模块 pigateway-server,包含 apigateway-dependenciesapigateway-server-commonapigateway-server-plugins 模块;
  • 客户端模块 apigateway-client 包含 apigateway-dependenciesapigateway-common 模块;
  • 插件模块 apigateway-server-plugins 包含 apigateway-dependenciesapigateway-server-common 模块;

子模块的 POM 要继承上级 POM

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.coolbeevip.apigateway</groupId>
    <artifactId>apigateway-parent</artifactId>
    <version>${revision}</version>
  </parent>
  <artifactId>apigateway-server</artifactId>

</project>
  • 子模块只能继承本系统的模块(只有这样参数才能继承,避免重复定义)
  • 子模块中不要单独定义版本号

依赖定义子模块

通常在项目中必须有一个地方几种定义依赖的版本,承担这个功能的模块一般是主 pom 或者一个单独的子模块,例如 apigateway-dependencies. 在这个模块里通常会用到以下定义

  • properties
  • dependencyManagement
  • pluginManagement
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.coolbeevip.apigateway</groupId>
    <artifactId>apigateway-parent</artifactId>
    <version>${revision}</version>
  </parent>
  <artifactId>apigateway-dependencies</artifactId>

  <properties>
    <!-- 定义版本号 -->
    <h2.version>1.4.200</h2.version>
    <dom4j.version>2.1.3</dom4j.version>
    <commons-httpclient.version>3.0</commons-httpclient.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <!-- 管理依赖版本 -->
      <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>${h2.version}</version>
      </dependency>
      <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>${dom4j.version}</version>
      </dependency>
      <dependency>
        <groupId>commons-httpclient</groupId>
        <artifactId>commons-httpclient</artifactId>
        <version>${commons-httpclient.version}</version>
      </dependency>      
    </dependencies>
  </dependencyManagement>

  <build>
    <pluginManagement>
      <plugins>
        <!-- Docker 发布的插件配置 -->
        <plugin>
          <groupId>io.fabric8</groupId>
          <artifactId>docker-maven-plugin</artifactId>
          <configuration>
            <skip>true</skip>
            <dockerHost>${docker.host}</dockerHost>
            <registry>${docker.registry}</registry>
            <authConfig>
              <push>
                <username>${docker.username}</username>
                <password>${docker.password}</password>
              </push>
            </authConfig>
            <images>
              <image>
                <name>${docker.registry}/${docker.namespace}/${project.name}:%v</name>
                <build>
                  <dockerFile>${project.basedir}/Dockerfile</dockerFile>
                </build>
              </image>
              <image>
                <name>${docker.registry}/${docker.namespace}/${project.name}:latest</name>
                <build>
                  <dockerFile>${project.basedir}/Dockerfile</dockerFile>
                </build>
              </image>
            </images>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

常用命令

编译

./mvnw clean package

编译 & 跳过测试

./mvnw clean package -DskipTests

编译 & 推送 Maven 私服

./mvnw clean package deploy -DskipTests \
-Ddistribution.url=private.nexus:8099 \
-Ddistribution.username=myuser \
-Ddistribution.password=mypass

编译 & 测试 & 推送 SONAR

./mvnw clean package sonar:sonar \
-Dsonar.projectKey=mykey \
-Dsonar.host.url=http://private.sonar:59000 \
-Dsonar.login=mypass

编译 & 测试 & 推送 Docker 私服

./mvnw -s settings.xml docker:build docker:push \
-Ddocker.username=myuser \
-Ddocker.password=mypass

生成缺陷报告

./mvnw org.owasp:dependency-check-maven:aggregate