maven排包
前言
总结一下我的排包过程,希望能够给和我遇到同样问题的小伙伴一些帮助。
常见包冲突错误
我们的项目在引入一个二方包或者对该二方包进行升级时,常常在编译阶段、启动阶段和运行阶段这三个阶段分别会遇到一些问题。
编译阶段
编译阶段的问题是最容易发现和解决的,因为这时在开发者电脑本地上就可以直接通过maven的编译指令对项目进行编译,编译过程中如果有问题就会通过日志输出,我们开发人员就可以根据日志很方便的解决问题。
【注意】maven有一定缓存能力,我就踩了这样一个坑,项目在我本地编译(执行指令:mvn compile)是能通过的,但是提交到预发环境部署时,就编译失败了。原因就是我们在本地编译时maven已经将所用到的依赖都打包到target目录下了,如果我们删除了某个依赖,mvn并不会立即在target目录下删除其相关文件。执行mvn clean命令就可以将tagget目录下的所有文件删除。因此如果我们修改了pom.xml,在本地编译时要执行mvn clean compile命令。
启动和运行阶段
启动阶段的包版本冲突引发的问题常见的有以下几种,运行阶段才发现的问题是最棘手的,因为项目启动过程没有问题,可能上线后观察期也发现不了问题,只有当项目运行时,执行到某段代码,这时才可能把问题暴露出来,所以这种情况就很可能会造成线上故障。
1、ClassNotFoundException
当动态加载Class的时候找不到类会抛出该异常。
一般在执行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的时候抛出。
2、NoClassDefFoundError
当编译成功以后执行过程中Class找不到导致抛出该错误,由JVM的运行时系统抛出。
JVM或者ClassLoader实例尝试加载类的时候,找不到类的定义而发生,通常在import和new一个类的时候触发。
3、NoSuchMethodError
该错误与NoClassDefFoundError类似,都是在调用该方法时找不到。
4、循环依赖导致StackOverFlow
最常见的循环依赖就是几个日志实现的二方包。
5、功能失效
原因一:高版本的功能在低版本中没有;
原因二:该二方包将异常吞掉,没有抛出来,程序虽然中间执行又问题,但是依然能够向下执行。
Maven自动引包原理
我们的项目在编译打包时,最终某个包只会引入某一个版本的,在没有任务干预的情况下,具体引入哪个版本的包是由maven的自动引包策略决定的。
当依赖冲突时,maven会采取最短路径优先策略、第一声明优先策略和dependency复写策略等引包策略。
最短路径优先策略
当导入的两个依赖中都直接或间接依赖某依赖时,maven将获取依赖链最短的那个。
例如项目X中同时导入A和B两个依赖,同时A依赖C,C依赖D(1.0.0),B依赖D(2.0.0),括号内为该包的版本,
即X->A->C->D(1.0.0),X->B->D(2.0.0)。可以看到D(2.0.0)的依赖链更短,系统在编译时将加载D(2.0.0)。
最先声明优先策略
当两个不同版本的包的依赖链长度一样时,将加载最先声明的包。
例如X->A->D(1.0.0)和X->B->D(2.0.0),在pox.xml中的配置如下
<dependency>
<dependency>
A依赖在前,系统最终将加载D(1.0.0)。
dependency复写策略
如果某包显式的pom.xml中写了两次,那么后面写的一次将覆盖掉前面的内容。
例如,配置如下
<dependency>
<dependency>
系统在编译打包时将加载A(2.0.0)。
排包工具
因为maven按照其排包引包策略引入的包可能不是我们业务逻辑想要的那个包,所以这时我们需要在对maven的引包排包结果进行纠正。首先我们先看一下一些常用的排包工具。
1、maven helper插件
可以通过idea的maven helper插件查看项目导入的包和有冲突的包。
安装:
使用:
打开pox.xml,然后点击文件下方的Dependency Analyzer。选择Conflicts即可看到冲突的包了。
常见冲突包
1、日志包冲突
日志包冲突非常容易发生,而且容易造成线上日志打不出来的问题。
我们常用的日志框架主要有以下几种:
框架 | 描述 |
---|---|
JUL | 是JDK自带的,功能简单,并没有广泛使用 |
Log4j | 设计较好,使用广泛 |
Conmons Loggings | 简称JCL,JCL仅是一个Facade,只提供Log API,不提供实现,实现可以用JUL或Log4j作为Implementation。 |
Slf4j/Logback | Slf4j作为Facade定义接口API,Logback是Implementation。性能比之前日志框架有了更大提升。 |
Log4j2 | 性能相比Log4j有了更大提升,并做了Facade/Implementation分离,分为log4j-api和log4j-core。 |
发展到最后,主流的日志框架都采用了Facade/Implementation分离的模式,这时为了能够实现不同框架的Facade和Implementation能够交叉使用,出现了很多桥接器,比如log4j-slf4j-impl就实现了Slf4j(Facade)和log4j-core(Implementation)的桥接。
在众多的桥接器出现后,当多个包共存时导致依赖冲突。主要冲突分为三类:Implementation 和Facade之间缺少桥接器;Implementation 和Facade之间存在多个桥接器,无法确定选哪个;多个桥接器导入后导致循环依赖,比如log4j-over-slf4j和slf4j-log4j12,jul-to-slf4j与slf4j-jdk14二者共存都会会造成循环依赖。
我们的日志包相关包的Implementation包到导入依赖时,建议scope都设置为provided,做负责任程序员,不给下游添麻烦。
引包排包总结
1、引包排除
在引包时,直接排除。
<dependency>
2、主动指定版本
项目代码中使用的二方包/三方包,要主动的在pom.xml中添加dependency,而不是借助于引入导入的依赖的间接依赖。主动倒入依赖,并在导入二方包时exclusion *。
3、删除无用依赖
如果是提供给别人依赖的Jar包,尽可能不要传递依赖不必要的Jar包,
使用mvn dependency:analyze-only命令用于检测那些声明了但是没被使用的依赖,删除无用依赖,不把麻烦向外传递。
4、使用scope限制范围
dependency的
<dependency>
scope的值目前有六个,scope的默认值是compile。
scope值 | 作用 |
---|---|
compile | 该依赖在编译和打包时都会被加进来,该包会传递到被依赖的项目中 |
provided | 在编译和测试的时候有效,在执行(mvn package)进行打包时不会加入。同时没有传递性,使用这个时,不会将包打入本项目中,只是依赖过来。 |
runtime | 表示dependency不作用在编译时,但会作用在运行和测试时。 |
test | 表示dependency作用在测试时,不作用在运行时。 |
system | 跟provided 相似,但是在系统中要以外部JAR包的形式提供,maven不会在repository查找它。使用方式如下:<dependency> |
import | maven2.9之后引入,可以实现依赖上的多重继承。这个功能可以将依赖配置复杂的pom文件拆分成多个独立的pom文件。这样处理可以使得maven的pom配置更加简洁,同时可以复用这些pom依赖。 |