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实例尝试加载类的时候,找不到类的定义而发生,通常在importnew一个类的时候触发。

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>

A

A

1.0.0

<dependency>

B

B

1.0.0

A依赖在前,系统最终将加载D(1.0.0)。

dependency复写策略

如果某包显式的pom.xml中写了两次,那么后面写的一次将覆盖掉前面的内容。

例如,配置如下

<dependency>

A

A

1.0.0

<dependency>

A

A

2.0.0

系统在编译打包时将加载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/LogbackSlf4j作为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>

com.aa.bb

xxx-yyy

${xxx-yyy.version}

cc

com.cc.dd

2、主动指定版本

项目代码中使用的二方包/三方包,要主动的在pom.xml中添加dependency,而不是借助于引入导入的依赖的间接依赖。主动倒入依赖,并在导入二方包时exclusion *。

3、删除无用依赖

如果是提供给别人依赖的Jar包,尽可能不要传递依赖不必要的Jar包,

使用mvn dependency:analyze-only命令用于检测那些声明了但是没被使用的依赖,删除无用依赖,不把麻烦向外传递。

4、使用scope限制范围

dependency的 是用来限制dependency的作用范围的, 影响maven项目在各个生命周期时导入的package的状态。

<dependency>

aa.xxx

aa-bb

1.x.x

provided

scope的值目前有六个,scope的默认值是compile。

scope值作用
compile该依赖在编译和打包时都会被加进来,该包会传递到被依赖的项目中
provided在编译和测试的时候有效,在执行(mvn package)进行打包时不会加入。同时没有传递性,使用这个时,不会将包打入本项目中,只是依赖过来。
runtime表示dependency不作用在编译时,但会作用在运行和测试时。
test表示dependency作用在测试时,不作用在运行时。
system跟provided 相似,但是在系统中要以外部JAR包的形式提供,maven不会在repository查找它。使用方式如下:<dependency>org.openopen-core1.5system${basedir}/WebContent/WEB-INF/lib/open-core.jar
importmaven2.9之后引入,可以实现依赖上的多重继承。这个功能可以将依赖配置复杂的pom文件拆分成多个独立的pom文件。这样处理可以使得maven的pom配置更加简洁,同时可以复用这些pom依赖。