以下文章来源 原创 赵志强 [CodingBetterLife]
你真的会使用maven吗?
作为java工程师,相信你对maven一定不陌生,但是你真的了解maven吗?很多同学会在maven中添加依赖、升级版本,但也就如此了。
如果让你新建一个系统,并且使用maven来管理好各个模块、管理好各种依赖库的版本、处理包冲突、根据项目的情况打各种类型的包、使用一些插件来完成额外的功能等,可能就不会了。
此外,我们还会碰到例如“IDE中能够找到某个类,但是编译的时候就会报错说找不到”,“我的程序本地跑的时候没问题,打包到服务器上运行后就报类冲突要怎么解决”等等问题。
所以,很多人在用maven,但其实又不太会用maven。
本文内容
了解maven你就能解决上面这些问题。此外,maven还有一些高级用法能让我们做更多事情。这篇文章就来帮助你提升对maven的了解,解决你的现实问题。
通过这篇文章,你会了解到:
【1】Maven是什么
【2】Maven仓库的运行机理
【3】依赖管理与传递
【4】项目生命周期管理
【5】模块聚合与继承
【6】属性的使用
【7】配置文件与环境管理
【8】Maven插件实战(实现一个简单的插件)
说在前面,由于maven的内容其实很多,而且一些基础的概念大部分同学都了解(例如坐标、版本等),所以我会挑选我认为更有价值的部分来说。
另外,本着【实战第一】的目的,最后一部分我们会来实现一个简单插件(maven的插件机制让我们在项目周期过程中无所不能) ,如果你仅仅想学习如何写maven插件,也可以直接跳到最后一部分。
01
Maven是什么
Maven是一个Java项目管理工具,它可以自动化构建、打包、部署和依赖管理。Maven具有丰富的插件生态系统,可以满足各种各样的构建需求。
注意的是,maven仅仅是一个工具,而不是java编程的必须项。不使用maven我们一样可以用java编码、编译、部署及运行。
除了maven,我们也可以使用例如gradle等工具来管理java项目。所以要记住,maven的任何报错和业务逻辑无关,是编译时报错,而非运行时。
另外,Maven有非常丰富的插件机制,你可以发挥你的想象,实现各种有趣的功能,这个我们最后来讲。
02
Maven仓库的运行机理
虽然maven有各种厉害的能力,但是其核心的能力还是在于依赖管理。讲白了,就是【帮你下载各种你需要的库】以及【你可以上传你的包给自己或者别人用】。
我们来看下你是如何通过maven获取到一个jar包的。
如图所示,我们想要获取jar包,就是从仓库去下载。有两种方式:
【从中央仓库拉取】 对应图中橙色的交互方式。顾名思义,就是有一个maven团队维护的仓库,是maven配置文件中的默认仓库。这个仓库在国外,下载非常慢。而且这个仓库上的包是公开的,谁都可以下载。
【从私服仓库拉取】 对应图中蓝色交互方式。私服仓库相当于一个缓存,本地仓库委托私服仓库拉取jar包,私服拉取到jar包后会缓存在本地,之后jar包再拉取就不用去中央仓库请求了。私服仓库意义重大,包括加速下载、上传非公开jar包等。
所以,公司一般都会采用【从私服仓库拉取】的方式。
这里有一个细节,由于私服仓库会代理请求中央仓库,但是中央仓库很慢,那么私服仓库拉取jar也会很慢,这个怎么办呢?
我们可以设置私服仓库拉取的中央仓库为国内的镜像仓库,例如aliyun的镜像仓库,就可以了。
所以,其实更常见的一个仓库结构如下:在这个结构中,本地仓库、私服仓库、镜像仓库都会缓存拉到的jar包。所以,整个仓库方案其实就是一个“代理”+“缓存”的思路,是不是和我们的系统的一些设计异曲同工?
03
依赖管理与传递
在maven中,如果项目依赖了jar包A,而jar包A依赖了jar包B,那项目也就传递依赖了jar包B。这个很好理解。
但这里面会出现一个致命的问题,那就是jar包冲突问题,如下所示:
这个方法的解决有两个办法:
【1】项目X依赖A.jar时排除掉B.jar的间接依赖。可以使用exclusion标签。如下示例:
//项目X的pom.xml<dependency>
<groupId>org.xxx</groupId>
<artifactId>jarA</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.xxx</groupId>
<artifactId>jarB</artifactId>
</exclusion>
</exclusions>
</dependency>
【2】A.jar打包时忽略B.jar。可以使用optional和provided关键字。
//jarA的pom.xml//使用optional的方式<dependency>
<groupId>org.xxx</groupId>
<artifactId>jarB</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
//使用provided的方式
<dependency>
<groupId>org.xxx</groupId>
<artifactId>jarB</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
这里展开讲一下scope。provided关键字是写在标签内的,scope有很多个取值,主要是“compile”、“test”、“provided”、“runtime”。这些字段决定了在“编译时”、“测试时”、“运行时”的可见性。三个阶段的可见性关系见下表:
你可以尝试下给你项目的例如spring-boot-starter-web打上test,然后在执行mvn compile,分分钟就报错给你看。
04
项目生命周期管理
maven之所以称之为java项目管理工具,就是说包含了所有包括创建、编译、测试、打包等各种操作。我们把这些操作称之为“项目生命周期的管理”。
maven的生命周期分成三块:default LifeCycle、clean LifeCycle和site LifeCycle。这三个LifeCycle又含有很多phase,我们不一一介绍,你可以看下图感受一下:
(原图Maven来自官网) 其中我们主要涉及的阶段其实就是我们IDE中可以看到的阶段,如下所示:
其中重要的几个阶段我已经标明了作用。这里需要注意的是,每个phase执行的时候,都会把上面的所有phase都执行。例如,你执行了install,maven会执行包括clean、validate、compile等所有在install之前的阶段。
这里有小tips。我们上面提到,你可能碰到过这样的错误“你的IDE能找到X.jar中的代码,但是编译的时候就报找不到”。这就说明你的本地仓库没有X.jar,你需要先执行X.jar所在的maven工程的install命令,这样就解决了。
05
模块聚合与继承
使用maven我们会碰到一个典型场景,那就是如果我们依赖一堆相关的jar包,往往会需要一起升级。
例如我们需要升级springboot版本的话,我们可能需要同时升级很多个jar包,其中包括spring的、db的、各种中间件的等等。我们随便瞅一眼springboot某个版本的依赖。
如果我们每次想要升级springboot,都需要去手动调整这些依赖的版本号,不光繁琐而且容易遗漏。
那要怎么解决这个问题呢?maven提供了“聚合”+“继承”的方式来解决这个问题。
【maven聚合】
maven提供了一种专门的打包方式,叫做pom。一个maven项目被指定为pom的含义是“这个maven项目仅仅是用来做管理的,没有代码,只有一个pom.xml”。这样的maven项目我们也称之为bom包。
我们以spring的一个bom包为例:
在这个bom包的pom文件中可以添加各种依赖并指定版本。和一般的maven项目不同的是,一般的maven项目对资源的依赖使用的是标签,而bom包则使用标签来包裹所有的依赖声明。我们还是来看下spring的bom包:
【maven继承】
在maven聚合之后,我们就可以使用maven的继承能力。我们可以申明一个maven项目的parent为bom包,这样我们就继承了bom包的所有依赖版本(不需要手动指定),如下图所示:
上图中spring-framework-bom就是一个bom包。我们通过parent标签继承这个bom包以后,下面的依赖都不需要再指定版本号。
这就是maven的聚合和继承。不过,maven的聚合还可以实现对子模块的管理,从而实现同时打包的功能。这个比较简单,大家可以自行查找一些资料。
06
属性的使用
maven还有一种常见的用法,就是将版本号等一些内容抽取成属性统一管理,如下图:
这个相信大家都很常用。但这里我想要说的是,maven本身还内置了一些重要的属性。这些属性可以在插件中使用,也可以在后面提到的配置文件中使用。
我这里贴一个图,大家可以看看maven支持了哪些内置属性:
(原图Maven来自官网)
07
配置文件与环境管理
maven还有一个不太被使用到的功能,那就是提供配置管理的功能,同时可以支持根据环境隔离。
我们举个例子,看下图:
选择对应环境的profile并执行后,yml文件中的通配符就会被替换。
不过这里我想说,把所有的配置信息放在pom文件中并不是一个推荐的做法,尤其是是一些与业务相关的配置,独立管理会更好。
08
Maven插件实战
最后,也是本文压轴的内容,就是给大家实现一个简单的插件。
插件是maven的核心能力,maven的编译、运行测试、打包等操作其实都是调用插件来完成的,这些插件都是maven提供的原生插件。而maven插件更重要的点在于,你可以自己去开发插件,从而实现你想要的功能。比如,统计测试的代码覆盖率
在具体讲如何开发插件前,有几个重点要先说一下:
【1】maven插件本身也是一个maven项目,只是打包方式是特殊的maven-plugin,这个我们马上就会看到。
【2】maven插件需要指定在哪个生命周期执行。
下面,我们来实现一个插件,这个插件的功能是:统计【源文件的数量】以及【所有文件加起来的总行数】。
【step1】创建一个开发插件的maven工程可以直接在IDE中选择如图所示的archetype自动创建,创建好以后会生成一个默认的“插件类”和pom文件。
【step2】在pom文件中添加必要的依赖
自动生成的pom文件中缺少一些东西,你可以直接拿我下面的这个内容贴到你的pom中。
<dependencies> <!--这个依赖自动生成的pom里就有,可以升级下版本 -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.0</version>
</dependency> <!--这个依赖引入了插件开发需要的相关注解--> <dependency> <groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
【step3】写插件逻辑
废话不多说了,直接上代码了,代码也很容易理解,copy过去就能用。
package com.codingbetterlife;
import org.apache.maven.plugin.AbstractMojo;import org.apache.maven.plugin.MojoExecutionException;import org.apache.maven.plugins.annotations.Mojo;import org.apache.maven.plugins.annotations.Parameter;import java.io.BufferedReader;import java.io.File;import java.io.FileReader;import java.io.IOException;
@Mojo(name = "main")public class FileLineCountMojo extends AbstractMojo {
/** * 一个用于统计的内部类 * */ private class Counter { public int fileCount; public int lineCount; }
/** * project.build.sourceDirectory是一个maven内置属性 * */ @Parameter(defaultValue = "${project.build.sourceDirectory}", required = true, readonly = true ) private String sourceFileDir;
/** * 插件的执行入口 * */ public void execute() throws MojoExecutionException { Counter counter = new Counter(); File dir = new File(sourceFileDir); countLines(dir, counter); System.out.println("总文件数为:" + counter.fileCount); System.out.println("所有文件的行数总和为:" + counter.lineCount); }
/** * 遍历目录,读取文件 */ public static void countLines(File dir, Counter counter) { if (dir == null || !dir.exists()) { return; } File[] files = dir.listFiles(); for (File file : files) { if (file.isDirectory()) { countLines(file, counter); // 递归遍历子文件夹 } else { if (file.getName().endsWith(".java")) { // 只对Java文件进行行数统计 counter.fileCount++; countLinesInFile(file, counter); } } } }
/** * 计算指定文件的行数 */ public static void countLinesInFile(File file, Counter counter) { int count = 0; BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(file)); while (reader.readLine() != null) { count++; } counter.lineCount += count; } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
【step4】打包插件
如果你是一路读下来这篇文章的话,你应该已经知道了。打包插件到本地仓库的命令就是:mvn install
【step5】你的项目引入这个插件
在你的项目中,加入这个插件的依赖
<build> <plugins> <plugin> <groupId>com.codingbetterlife</groupId> <artifactId>file-line-count</artifactId> <version>1.0-SNAPSHOT</version> <executions> <execution> <goals> <goal>main</goal><!-- 与插件类中的@Mojo标签的name属性对应 --> </goals> <phase>compile</phase><!-- 指定在编译阶段执行 --> </execution> </executions> </plugin> </plugins></build>
【step6】run!
然后我们执行你的项目的compile命令。在控制台可以看到这样的输出:
bingo,搞定!
你只要延展一些想象,插件的功能是无穷的。可以对你的代码(源代码或者目标代码)做各种统计、分析,这些数据还可以上传到服务器上。甚至可以写一个插件改写源代码!只有你想不到,没有你办不到的事情。