您的位置 首页 java

高效使用Java构建工具Maven

目前业内使用Ant的人已经比较少,主要都在用 Maven Gradle 和Bazel,如何真正基于这三款工具的特点发挥出他们最大的效用,是这个系列文章要帮大家解决的问题。先从Maven说起。

> 优雅高效地用好Maven

当我们正在维护一个Maven工程时,关注以下三个问题,可以帮助我们更好的使用Maven。

● 如何优雅的管理依赖

● 如何加速我们的构建测试过程

● 如何扩展我们自己的插件

>> 优雅的依赖管理

在依赖管理中,有以下几个实践原则,可以帮助我们优雅高效的实现不同场景下的依赖管理。

● 在父模块中使用dependencyManagement,配置依赖

● 在子模块中使用dependencies,使用依赖

● 使用 profiles ,进行多环境管理

以我在日常开发中维护的一个标准的 spring -boot多模块Maven工程为例。

高效使用Java构建工具Maven

工程内各个module之间的依赖关系如下,通常这也是标准的 spring-boot restful api多模块工程的结构。

高效使用Java构建工具Maven

>>> 便捷的依赖升级

通常我们在依赖升级的时候会遇到以下问题:

● 多个依赖关联升级

● 多个模块需要一起升级

在父模块的 pom .xml中,我们配置了基础的spring-boot依赖,也配置了日志输出需要的logback依赖,可以看出,我们遵循了以下的原则:

(1)在所有子模块的父模块中的pom中配置dependencyManagement,统一管理依赖版本。在子模块中直接配置依赖,不用再纠缠于具体的版本,避免潜在的依赖版本冲突。

(2)把groupId相同的依赖,配置在一起,比如groupId为org.springframework.boot,我们配置在了一起。

(3)把groupId相同,但是需要一组依赖共同提供功能的artifactId,配置在一起,同时将版本号抽取成变量,便于后续一组功能共同的版本升级。比如spring-boot依赖的版本抽取成了spring-boot.version。

   <properties>
    ...
    <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    ...
    <logback.version>1.2.3.12- struct </logback.version>
    ...
    <h2.database.version>1.4.199</h2.database.version>
    ...
  </properties>


  <dependencyManagement>
    ...
    <dependencies>
      <!-- spring-boot -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>${spring-boot.version}</version>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
        <version>${spring-boot.version}</version>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>${spring-boot.version}</version>
        <scope>test</scope>
      </dependency>      
      ...
      <!-- log -->
      <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
      </dependency>
      <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>${logback.version}</version>
      </dependency>
      <!-- test db -->
      <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>${h2.database.version}</version>
        <scope>test</scope>
      </dependency>
      ...
    </dependencies>
  </dependencyManagement>
          

在子模块build-engine-api的pom.xml中,由于在父pom中配置了

dependencyManagement中依赖的spring-boot相关依赖的版本,因此在子模块的pom中,只需要在dependencies中直接声明依赖,确保了依赖版本的一致性。

   <parent>
    <groupId>com.alibaba.aone</groupId>
    <artifactId>build-engine</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>
 
   ...


  <dependencies>
     <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     ...
  </dependencies>
   

>>> 合理的依赖范围

maven 依赖有依赖范围(scope)的定义,compile/provieded/runtime/test/system/import,原则上,只按照实际情况配置依赖的范围,在必要的阶段,只引入必要的依赖。

90%的Java程序员应该都使用过

org.projectlombok:lombok来简化我们的代码,其原理就是在编译过程中将注解转化为 Java 实现。因此该依赖的scope为provided,也就是编译时需要,但在构建出最终产物时又需要被排除。

   ...
  <dependencies>
     <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
    ...
  </dependencies>
 ...  

当你的代码需要使用 JDBC 连接一个 mysql 数据库,通常我们会希望针对标准 JDBC 抽象进行编码,而不是直接错误的使用 MySQL driver实现。这个时候依赖的 scope 就需要设置为runtime。 这意味着我们在编译时无法使用该依赖,该依赖会被包含在最终的产物中 ,在程序最终执行时可以在 classpath 下找到它。

   ...
  <dependencies>
     <dependency>
        <groupId>my sql </groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    ...
  </dependencies>
 ...  

在子模块dao中,我们有对sql进行测试的场景,需要引入内存数据库h2。

因此,我们将h2的scope设置为test,这样我们在测试编译和执行时可以使用,同时避免其出现在最终的产物中。

   ...
  <dependencies>
     <dependency>
       <groupId>com.h2database</groupId>
       <artifactId>h2</artifactId>
       <scope>test</scope>
    </dependency>
    ...
  </dependencies>
 ...  

更多关于scope的使用,可以参考官方帮助文档。

>>> 多环境支持

举个简单的例子,当我们的服务在公有云部署时,我们使用了一个云上版本为8.0的MySQL,而当我们要进行专有云部署时,用户提供一个自运维的版本为5.7的MySQL。因此,我们在不同的环境中使用不同的 mysql:mysql-connector-java 版本。

类似的,在项目实际的开发过程中,我们经常会面临同一套代码。在多套环境中部署,存在部分依赖不一致的情况。

 <project>
  ...
  <profiles>
    <!-- 专有云 -->
    <profile>
      <id> private </id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <mysql.version>5.1.40</mysql.version>
      </properties>
    </profile>
    <!-- 公有云 -->
    <profile>
      <id>public</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <mysql.version>8.0.12</mysql.version>
      </properties>
    </profile>
    ...
  </profiles>
   
  ...
  <dependencyManagement>
    <dependencies>
      ...
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      ...
    </dependencies>
  </dependencyManagement>
  ...
</project>  

profiles的更多用法,可以参考官方帮助文档。

>>> 依赖纠错

如果你已经在父pom中使用

dependencyManagement来锁定依赖版本,大概率的,你几乎很少会碰到依赖冲突的情况。

但是当你还是意外的看到了

NoSuchMethodError,

ClassNotFoundException 这两个异常的时候,有以下两个方法可以快速的帮你纠错。

(1)通过依赖分析找到冲突的依赖

 # 查看完整的依赖树
mvn dependency:tree
# 查看特定依赖的版本
mvn dependency:tree -Dincludes=commons-lang:commons-lang  

(2)通过添加 stdout 代码找到冲突的类实际是从哪个依赖中查找的

 System.out.println(CollectionUtils.class.getProtectionDomain().getCodeSource().getLocation());  

通过具体的路径中对应的版本信息,找到对应的版本并校正。

高效使用Java构建工具Maven

当然这个方法也可以纠出一些依赖被错误的加载到classpath下,非工程本身依赖配置引起的冲突。

>> 测试构建过程加速

作为一个开发者,总会希望我们的工程无论在什么情况下,执行的又快又稳,那么在Maven的使用过程中,需要遵循以下原则。

● 尽可能复用缓存

● 尽可能的并行构建或测试

>>> 依赖下载加速

通常情况下,根据Maven配置文件 ${user.home}/.m2/settings.xml 中的配置,默认情况下是缓存在${user.home}/.m2/repository/。

 <settings>
  ...
  <localRepository>/path/to/local/repo/</localRepository>
  ...
</settings>  

通常在构建过程中,依赖的下载往往会成为比较耗时的部分,但是通过一些简单的设置,我们可以有效的减少依赖的下载与更新。

● 优化updatePolicy设置

updatePolicy指定了尝试更新的频率。Maven 会将本地 POM 的时间戳(存储在存储库的 maven-metadata 文件中)与远程进行比较。选项包括:always(总是)、daily(每天,默认值)、interval:X(其中 X 是以分钟为单位的整数)、never(从不)。

 <settings>
  ...
  <profiles>
    <profile>
      ...
      <repositories>
        <repository>
          <id>codehausSnapshots</id>
          <name>Codehaus Snapshots</name>
          <releases>
            <enabled>false</enabled>
            <updatePolicy>always</updatePolicy>
            < checksum Policy>warn</checksumPolicy>
          </releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>never</updatePolicy>
            <checksumPolicy>fail</checksumPolicy>
          </snapshots>
          <url>
          <layout>default</layout>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>myPluginRepo</id>
          <name>My Plugins repo</name>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
          <url>
        </pluginRepository>
      </pluginRepositories>
      ...
    </profile>
  </profiles>
  ...
</settings>  

● 使用离线构建

除此之外,如果构建环境已经存在缓存,可以使用Maven的offline模式进行构建,避免依赖或插件的下载更新。

 mvn clean install -o  

直观的,日志中将不会出现类似如下Downloading相关的信息。

高效使用Java构建工具Maven

>>> 构建过程加速

在默认情况下,Maven构建的过程并不会充分的使用你的硬件的全部能力,他会顺序的构建你的maven工程的每一个模块。这个时候,如果可以使用并行构建,那么将有机会提升构建速度。

 # 使用4个线程进行构建
mvn -T 4 install  
 # 每个cpu核心使用一个线程进行构建
mvn -T 1C install  

以上是并行构建的两个命令,可以根据实际的 cpu 情况来选择对应的命令。但是如果你发现构建时间并没有得到减少,那么你的maven模块间可能存在类似的依赖,模块之间只是一个简单的传递。

高效使用Java构建工具Maven

那么并行构建对你来说并不适用,如果你的模块间依赖关系存在并行的可能,那么使用上述命令进行构建,才能使并行构建发挥效果。

高效使用Java构建工具Maven

>>> 测试过程加速

当我们尝试加速maven工程 测试用例 的部分,那么就不得不提到一个插件,maven-surefire-plugin。

当你在执行mvn test的时候,默认情况下就是surefire插件在工作。如果我们想在测试中使用并行的能力,可以作如下配置。

 ...
  <plugins>
    ....
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.0.0-M3</version>
      <configuration>
        <parallel>classes</parallel>
        < thread Count>8</threadCount>
      </configuration>
    </plugin>
    ...
  </plugins>
...  

但是需要注意不恰当的使用并行能力进行测试,反而可能带来副作用。比如当parallel配置为methods,但是由于某些原因测试用例的执行之间存在顺序要求,反而会出现因为用例方法并行执行,导致用例失败,因此也倒逼我们,如果想获得更快的测试速度,case的编写也需要独立且高效。

更多关于surefire插件的使用,可以参考这篇文档。

>> Maven插件开发

maven本质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立完成的。关于maven的核心插件可以参考这篇文档。

maven默认为我们提供的这些插件比如maven-install-plugin/mvn-surefire-plugin/mvn-deploy-plugin外,还有一些三方提供的插件,单测覆盖率插件mvn-jacoco-plugin,生成api文档的swagger-maven-plugin等等。

在日常工作的过程中,我碰到了这样一个问题:有个存在明显问题的sql被发布到了预发布环境,同时由于预发与生产使用的是同一个db实例,由于sql的性能问题,影响了线上。

除了通过必要的 code review 准入,来避免类似的问题,更简单的,我们可以自己动手实现一个代码中sql扫描的插件,让代码在CI时直接失败掉,自动化的避免此类问题的发生。于是我们开发了一个maven插件,使用方法和效果如下:

在工程中引入我们开发并部署好的插件com.aliyun.yunxiao:mybatis-sql-scan。

   <plugin>
    <groupId>com.aliyun.yunxiao</groupId>
    <artifactId>mybatis-sql-scan</artifactId>
    <version>1.0-SNAPSHOT</version>
    <configuration>
      <mapperFiles>.*Mapper.java;</mapperFiles>
      <excludeFiles>.*/someMapper.java</excludeFiles>
    </configuration>
    <executions>
      <execution>
        <phase>validate</phase>
        <goals>
          <goal>check</goal>
        </goals>
      </execution>
    </executions>
  </plugin>  

执行以下命令,或其他包含 validate 阶段执行的命令。

 mvn validate  

我们将会在日志中看到如下插件执行的信息。

高效使用Java构建工具Maven

在扫描出缺陷时,build失败,并会在日志中出现对应的信息:

在GlobalLockMapper.java这个文件中,我们有一条全表扫描的 sql语句 可能存在风险,

高效使用Java构建工具Maven

同时build失败。

高效使用Java构建工具Maven

接下来我会从如何开发这个异常sql扫描的maven插件入手,帮助大家了解插件开发的过程。

1、创建工程

 mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-plugin  

生成的sample工程如下,

高效使用Java构建工具Maven

其中MyMojo.java定义了插件的入口实现,

此外在根pom.xml中可以看到,

● packaging为“maven-plugin”。

● 依赖配置中,依赖了一些插件开发的基础二方库。

● 插件节点下,依赖了maven-plugin-plugin协助我们完成插件的构建。

 <project ...>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.aliyun.yunxiao</groupId>
  <artifactId>mybatis-sql-scan</artifactId>
  <packaging>maven-plugin</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>mybatis-scan Maven Plugin</name>
  
  ...
  <dependencies>   
    <!-- plugin API and plugin-tools -->
    <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-plugin-api</artifactId>
      <version>3.5.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.maven.plugin-tools</groupId>
      <artifactId>maven-plugin-annotations</artifactId>
      <version>3.5.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.maven.shared</groupId>
      <artifactId>maven-shared-utils</artifactId>
      <version>3.2.0</version>
    </dependency>
    ...    
  </dependencies>
  
  
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-plugin-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <goalPrefix>scan</goalPrefix>
        </configuration>
        <executions>
          <execution>
            <id>default-descriptor</id>
            <phase>process-classes</phase>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
  </build>
  
  ...
</project>

  

2、Mojo实现

在开始实现我们的Mojo之前,我们需要做如下分析:

● 插件在maven的哪个生命周期执行

● 插件在执行时需要哪些入口参数

● 插件执行完成后怎么退出

由于我们要实现的插件是要做mybatis annotation扫描比如 @Update/@Select,判断是否有异常的sql,比如是否存在全表扫描的sql,是否存在全表更新的sql等,对于此种场景下,

● 由于需要扫描特定的源码,需要知道工程源码的所在目录,以及扫描哪些文件

● 插件扫描出异常时,只要报错即可,不用产出任何报告

● 希望在后续执行mvn validate时触发扫描

那么预期中的插件是这样的,

   ...
  <plugin>
    <groupId>com.aliyun.yunxiao</groupId>
    <artifactId>mybatis-sql-scan</artifactId>
    <version>1.0-SNAPSHOT</version>
    <configuration>
      <mapperFiles>.*Mapper.java;</mapperFiles>
      <excludeFiles>.*/someMapper.java</excludeFiles>
    </configuration>
    <executions>
      <execution>
        <phase>validate</phase>
        <goals>
          <goal>check</goal>
        </goals>
      </execution>
    </executions>
  </plugin>
  ...  

那么,

● @Mojo(name = “check”) 定义了goal

● @Parameter

○ @Parameter(defaultValue = “${project}”, readonly = true) 参数绑定了工程的根目录 ,project.getCompileSourceRoots()便可以获取到源代码的根路径

○ 我们定义了mapperFiles,用来负责扫描哪些文件的通配,excludeFiles用来负责排除哪些文件

● execute()

○ 有了以上的基础,在execute方法中我们便可以实现对应的逻辑,当扫描结出异常的sql时,抛出MojoFailureException异常,插件便会失败终止。

 package com.aliyun.yunxiao;


import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;




@Mojo(name = "check")
public class CheckMojo extends AbstractMojo {
    
    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;
    
    @Parameter
    private String mapperFiles;
    
    @Parameter
    private String excludeFiles;
    
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("execute risk sql check");
        
        //  do scan logic,print out scan result
        //  ...
        boolean scanResult = doScanLogic();
            
        //  scan result contains more than one invalid sql
        if (!scanResult) {
            throw new MojoFailureException("scan failed.");
        }
    }
}

  

以上,我们便完成了一个插件的基本能力的开发。

3、插件的打包与上传

插件开发完成后,我们可以通过配置distributionManagement,然后执行mvn deploy,完成插件的构建与发布。

 <project>
  ...
  <distributionManagement>
    <repository>
      <id>releases</id>
      <url>...</url>
    </repository>
    <snapshotRepository>
      <id>snapshots</id>
      <url>...</url>
    </snapshotRepository>
  </distributionManagement>
  ...
</project>  

《云效》文章摘选

文章来源:智云一二三科技

文章标题:高效使用Java构建工具Maven

文章地址:https://www.zhihuclub.com/190195.shtml

关于作者: 智云科技

热门文章

网站地图