maven
# 1. snapshot与release区别
1、Snapshot 版本代表不稳定、尚处于开发中的版本。
2、Release 版本则代表稳定的版本。
3、什么情况下该用 SNAPSHOT?
协同开发时,如果 A 依赖构件 B,由于 B 会更新,B 应该使用 SNAPSHOT 来标识自己。这种做法的必要性可以反证如下:
- a. 如果 B 不用 SNAPSHOT,而是每次更新后都使用一个稳定的版本,那版本号就会升得太快,每天一升甚至每个小时一升,这就是对版本号的滥用。
- b.如果 B 不用 SNAPSHOT, 但一直使用一个单一的 Release 版本号,那当 B 更新后,A 可能并不会接受到更新。因为 A 所使用的 repository 一般不会频繁更新 release 版本的缓存(即本地 repository),所以B以不换版本号的方式更新后,A在拿B时发现本地已有这个版本,就不会去远程Repository下载最新的 B
4、 不用 Release 版本,在所有地方都用 SNAPSHOT 版本行不行?
不行。正式环境中不得使用 snapshot 版本的库。 比如说,今天你依赖某个 snapshot 版本的第三方库成功构建了自己的应用,明天再构建时可能就会失败,因为今晚第三方可能已经更新了它的 snapshot 库。你再次构建时,Maven 会去远程 repository 下载 snapshot 的最新版本,你构建时用的库就是新的 jar 文件了,这时正确性就很难保证了。
# 2. 目录约定
${basedir} | 存放pom.xml和所有的子目录 |
---|---|
${basedir}/src/main/java | 项目的java源代码 |
${basedir}/src/main/resources | 项目的资源,比如说property文件,springmvc.xml |
${basedir}/src/test/java | 项目的测试类,比如说Junit代码 |
${basedir}/src/test/resources | 测试用的资源 |
${basedir}/src/main/webapp/WEB-INF | web应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面 |
${basedir}/target | 打包输出目录 |
${basedir}/target/classes | 编译输出目录 |
${basedir}/target/test-classes | 测试编译输出目录 |
Test.java | Maven只会自动运行符合该命名规则的测试类 |
~/.m2/repository | Maven默认的本地仓库目录位置 |
# 3. 常用命令
maven命令格式为:mvn [plugin-name]:[goal-name],可以接受的参数如下
参数 | 描述 |
---|---|
-D | 指定参数。例如:-Dmaven.test.skip=true 跳过单元测试 |
-P | 指定profile配置,可以用于区分环境 |
-e | 显示maven运行出错的信息 |
-o | 离线执行命令,即不去远程仓库更新包 |
-X | 显示maven允许的debug信息 |
-U | 强制去远程更新snapshot的插件或依赖,默认每天只更新一次 |
常用maven命令
创建maven项目 | mvn archetype:create指定group: -DgroupId=${groupId}指定artifact: -DartifactId=${artifactId}创建web项目:-DarchetypeArtifactId=maven-archetype-webapp需要指定参数,这是与generate区别 |
---|---|
生成maven项目 | mvn archetype:generate 直接敲就行了,不用任何参数,后面按照提示填写就能完成。 |
验证项目是否正确 | mvn validate |
maven 打包 | mvn package |
打包只打jar包 | mvn jar:jar |
生成源码jar包 | mvn source:jar |
产生应用需要的额外的源代码 | mvn generate-sources |
编译源代码 | mvn compile |
编译测试代码 | mvn test-compile |
运行测试 | mvn test |
运行检查 | mvn verify |
清理maven项目 | mvn clean |
生成eclipse项目 | mvn eclipse:eclipse |
清理eclipse配置 | mvn eclipse:clean |
生成idea项目 | mvn idea : idea |
安装项目到本地仓库 | mvn install |
发布项目到远程仓库 | mvnLdeploy |
在集成测试可以运行的环境中处理和发布包 | mvn integration-test |
显示maven依赖树 | mvn dependecny:tree |
显示maven依赖列表 | mvn dependency:list |
下载依赖包的源码 | mvn dependency:sources |
安装本地jar到本地仓库 | mvn install:install-file -DgroupId=${groupId} -DartifactId=${artifactId} -Dversion=${version} -Dpackaging=jar -Dfile=path |
Web项目相关命令
启动tomcat | mvn tomcat:run |
---|---|
启动jetty | mvn jetty:run |
运行打包部署 | mvn tomcat:deploy |
撤销部署 | mvn tomcat:undeploy |
启动web应用 | mvn tomcat:start |
停止web应用 | mvn tomcat:stop |
重新部署 | mvn tomcat:redeploy |
部署展开的war文件 | mvn war:exploded tomcat:exploded |
# 4. 关于java文件编译后的三个路径
编译依赖项在项目的所有类路径中都可用:指在编译类路径(src/main)、测试类路径(src/test)、运行类路径(target)下都可以使用。
由此就引出了 pom文件中的scope相关的属性,即依赖范围
依赖范围
依赖范围用于限制依赖的传递性并确定何时将依赖包含在类路径中,具体如下:
Scope | 描述 |
---|---|
compile | 范围默认值,如未指定,则使用 compile,编译依赖项在项目的所有类路径中都可用,并具备传递特性 |
provided | 此范围的依赖项将添加到用于编译和测试的类路径中,但不添加到运行时类路径中,是不可传递的,例如,在为 Java Enterprise Edition 构建 Web 应用程序时,您需要将对 Servlet API 和相关 Java EE API 的依赖设置为 provided 范围,因为 Web 容器提供了这些类 |
runtime | 此范围表示编译时不需要,但运行时需要的依赖项。此范围包含运行、测试类路径的依赖项,但不包括编译类路径 |
test | 此范围表示依赖项不是正常使用应用程序所必需的,并且仅适用于测试编译、测试运行阶段,此范围不是可传递的。通常此范围用于测试库,如 JUnit 和 Mockito,它也用于非测试库,如在单元测试中使用的 Apache Commons IO |
system | 此范围必须提供显式包含它的 JAR,不会在存储库中查找 |
import | 此范围仅在 pom 文件中的 |
# 依赖冲突问题
# 1 前言
你是否遇到过如下异常,导致其发生得原因可能是使用了不兼容的依赖、使用了错误版本依赖,造成的影响可能会导致应用发布失败,甚至在运行时抛出异常:
- java.lang.ClassNotFoundException: com.sankuai.xxxx,找不到某个类
- java.lang.NoSuchMethodError: 'xxx' on class 'com.xxx.xxx',找不到某个方法
其实上述的异常就是依赖冲突的具体表现,依赖冲突是指在软件开发过程中,Java应用程序因某种因素,加载不到正确的类而导致其行为跟预期不一致,此种情况会导致编译或运行时出现问题**。**由此引入了依赖管理的概念,在现代软件开发中,很少有项目是完全独立开发的,通常需要使用各种外部库和框架来提高开发效率和质量,依赖管理的目的是确保项目所依赖的外部库和框架能够正确地被引入和使用,以及能够在不同的开发环境中正确地运行,依赖管理在软件开发中具有重要的意义。
# 2 原理
依赖冲突具体来讲,可以分为两种情况:
- 应用程序依赖的同一个 Jar 包出现了多个不同版本,并选择了错误的版本而导致 JVM 加载不到需要的类或加载了错误版本的类
- 同样的类(类的全限定名完全一样)出现在多个不同的依赖 Jar 包中,即该类有多个版本,并由于 类加载的先后顺序导致 JVM 加载了错误版本的类。
以下会从原理方面讲解依赖冲突,描述侧重与依赖冲突有关的 Maven 依赖机制、类加载等方面。
# 2.1 Maven 依赖机制
# 依赖传递
Maven 依赖机制存在传递依赖关系的特性,传递依赖关系是指 Maven 会自动继承依赖项的依赖、父 pom 的依赖,以此类推,可以从中收集依赖项的层级数没有限制(循环依赖会发生异常),虽然可以使得项目的依赖关系变得清晰,同时也可避免重复依赖声明,但这种特性可能会导致同一个依赖被间接引入多个版本,反而增大依赖冲突的概率,如下图:
# 依赖仲裁
依赖的仲裁机制的目的在于确认唯一生效的依赖,具体规则如下:
优先按照依赖管理
元素中指定的版本声明进行仲裁,此时下面的两个原则都无效 若无版本声明,则按照“短路径优先”的原则进行仲裁,即选择依赖树中路径最短的版本,如下图会优先使用 D 1.0 版本
A ├── B │ └── C │ └── D 2.0 └── E └── D 1.0
1
2
3
4
5
6若路径长度一致,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本,如下图会优先使用 D 2.0 版本
A ├── B │ └── C │ └── D 2.0 └── E └── F └── D 1.0
1
2
3
4
5
6
7
# 依赖范围
依赖范围用于限制依赖的传递性并确定何时将依赖包含在类路径中,具体如下:
Scope | 描述 |
---|---|
compile | 范围默认值,如未指定,则使用 compile,编译依赖项在项目的所有类路径中都可用,并具备传递特性 |
provided | 此范围的依赖项将添加到用于编译和测试的类路径中,但不添加到运行时类路径中,是不可传递的,例如,在为 Java Enterprise Edition 构建 Web 应用程序时,您需要将对 Servlet API 和相关 Java EE API 的依赖设置为 provided 范围,因为 Web 容器提供了这些类 |
runtime | 此范围表示编译时不需要,但运行时需要的依赖项。此范围包含运行、测试类路径的依赖项,但不包括编译类路径 |
test | 此范围表示依赖项不是正常使用应用程序所必需的,并且仅适用于测试编译、测试运行阶段,此范围不是可传递的。通常此范围用于测试库,如 JUnit 和 Mockito,它也用于非测试库,如在单元测试中使用的 Apache Commons IO |
system | 此范围必须提供显式包含它的 JAR,不会在存储库中查找 |
import | 此范围仅在 pom 文件中的 |
# 2.2 Class 加载顺序
对于第二类依赖冲突,同名类存在于多个不同的依赖 jar 包当中,即类存在多个版本,这种情况是 Maven 无法解决的,因为 Maven 只会为你针对同一个 Jar 包的不同版本进行仲裁,而这俩是属于不同的 Jar 包,超出了 Maven 的依赖管理范畴。比如类 C 在 Jar 包 A、B 中都存在,那两个版本的 C 都出现在应用的类路径下,则先后的加载顺序决定了 JVM 选择的最终版本,选择了错误的 C 会导致出现第二类依赖冲突,决定类加载顺序的因素,具体如下:
- 类所处的加载路径:由于JVM类加载的双亲委派机制,层级越高的类加载器越先加载其加载路径下的类,顾名思义,引导类加载器(bootstrap ClassLoader,也叫启动类加载器)是最先加载其路径下 Jar 包的,其次是扩展类加载器(Extension ClassLoader),再次是系统类加载器(System ClassLoader,也就是应用加载器 AppClassLoader),类所处加载路径的不同,就决定了它的加载顺序的不同,对于 Spring 而言,类加载体系如下图:
- 文件系统的文件返回顺序:这个因素很容易被忽略,而往往又是因环境不一致而导致各种诡异冲突问题的罪魁祸首。因某些 ClassLoader 获取加载路径下的文件列表时是不排序的,这就依赖于底层文件系统返回的顺序,那么当不同环境之间的文件系统不一致时,就会出现有的环境没问题,有的环境出现冲突,例如,对于 Linux 操作系统,返回顺序则是由 iNode 的顺序来决定的。
# 3 应用
# 3.1 MDP 依赖管理
以上的描述可以看出,依赖冲突无非是相同依赖项目中引入了多个版本,选择错误版本导致,或是在不同的 jar 包中存在同名类导致,但实际项目当中会引入很多的依赖,很难保证不出现上述两种情况,因此如何确保依赖间的兼容,MDP 对此提供了一套解决方案,主要思路是通过管理一组版本兼容的依赖、插件等等,用户直接继承即可,详细如下图:
MDP 统一管理版本的落地还需用户的自觉性,因此 MDP 还引入了 mdp-maven-enforcer-plugin 插件,该插件是 MDP 基于 maven-enforcer-plugin 插件的封装扩展,用于对项目依赖进行检查,若存在不符合条件的依赖,则会强制构建报错,检查的规则有:
- 相同版本依赖重复引入检查
- 依赖封禁检查:比如版本区间、坐标匹配(支持正则表达式)
- 依赖互斥检查:比如 mafka、hystrix 互斥检查
mdp-maven-enforcer-plugin 插件检查相较 plus 的封禁检查的好处在于,首先更早的发现项目中依赖使用错误问题,其次支持的功能更多。
正确的使用依赖,除了 MDP 的管理和检查外,用户的良好习惯也是很重要,以下是一些建议的 Maven 依赖规范:
- dependency 的
定义在根 pom 的 内, 定义在根 pom 的 中,在 中引用,在其他地方不定义 - plugin 的
定义在在根 pom 的 中,在根 pom 的 引入,其他地方不定义 - 只在 test 中使用的依赖,需要设置
为 test,在实际引入的 pom 中设置 - lombok 依赖在引入时,需要设置
为 true,在实际引入的 pom 中设置 - maven-plugin 开发所需依赖在引入时,需要设置
为 provided,在实际引入的 pom 中设置 - 项目构建通过 mdp-enforcer 规则检查
# 3.2 MDP 插件
MDP 插件是提供给 MDP 用户的一款周边工具,主要目的是为了推进 MDP 标准化落地、降低 MDP 使用门槛、提升用户研发效率。其中依赖管理是其中功能执行,相较目前常见的依赖管理插件有如下特点:
- 依赖聚合管理:支持依赖聚合在一起进行管理,不用切换文件、页面等,使依赖管理更方便、易用。聚合功能分两层,第一层支持多 app 聚合,典型的例子比如 Nest 工程、多appkey 工程等;第二层支持多模块聚合,即每一个 app 下所涉及的相关 Module。
- 依赖版本对比:支持当前项目依赖与任意 MDP 版本的依赖进行对比,后续还会推出相应的报表功能。
- 依赖问题检查:支持依赖封禁、依赖冲突、依赖匹配(比如 mthrift 依赖 guava 20.0)等功能
- 依赖增删改查:支持依赖简单的 CRUD 操作
MDP 插件依赖管理功能的详细介绍如下:
如上图,依赖管理分为左右两个部分,左侧是项目依赖树,左侧页面会展示项目中所有的pom文件的依赖,总体功能如下:
- 依赖筛选:选择模块进行筛选,输入字符进行联动搜索
- 版本对比:选择MDP版本与当前项目进行版本对比,如不同,则在依赖尾部显示“[MDP管理版本号]”
- 树形切换:可以进行依赖平铺和树形的切换
- 新增依赖:可视化操作来新增依赖
- 问题筛选:将检测到问题的依赖进行展示
- 右击菜单:可以进行依赖的删除/排除、修改版本、打开POM文件、跳转引用处、MDP管理版本等功能
右侧是当选中某个依赖后,联动变化,主要功能如下:
- 依赖来源:当前选中依赖是直接引入还是间接引入,由哪些依赖间接引入
- 问题详情:展示该依赖的问题,并提供解决方式,用户点击会自动进行修复
# 4 实战
# 4.1 依赖冲突处理方法
- 判断是否为依赖冲突问题,根据上述章节,大部分类加载问题都可归属于依赖冲突,具体表现可归纳为一些特定异常,比如 ClassNotFoundException(详细可参见附录 -依赖冲突异常的发生场景及原因)
- 确定异常类来源于哪个依赖,只有确定来源依赖才能进行修复,因为修复处理的最小单元是依赖,而非类,常见定位手段有:
- 通过包名判断
- Idea 搜索
- Google 搜索
- 启动增加 -verbose:class 参数:启动时会打印类加载路径
- Debug 调试
- 根据依赖的情况确定处理方案,常见的处理方式有:
- 依赖在项目中不存在,需要引入
- 依赖在项目中存在多个版本,需要选取正确版本,一般选择高版本
- 依赖在项目中的版本与其他依赖不兼容,需要修改为正确版本
# 4.2 依赖缺失
场景:程序启动异常,报 java.lang.NoClassDefFoundError: feign/codec/Encoder
分析过程如下:
首先异常是 NoClassDefFoundError, 表示项目的依赖中某个类调用的类 feign.codec.Encoder 不存在,很显然属于依赖冲突问题
接下来需要确认类的来源,对于此类 case,需要确定 Encoder 的调用类的来源,才能定位到具体原因,以下分享下定位的方法
利用 Idea 进行搜索:利用 Find in Files-Scope 功能,可以进行文本匹配,该功能可以匹配到依赖的源码,但一开始在项目中搜关键字 “feign”,结果是空的,如下图:
这是因为相关依赖源码未下载,可以执行命令 mvn dependency:sources,然后再搜索即可,如左下图,当然也存在某些依赖就没有源码的情况;当找到调用类后,可以利用 Idea 的 Select Open File 功能,定位到相应依赖,可以看到来源依赖是 com.meituan.funds.common:common 依赖,如右下图:
进行 Debug 调试
Debug 方式选择,对于远程 Debug, 启动调试太耗时间了,因为触发一次就得机器部署一次,但本地 Debug 时,Idea 启动可能不会复现场景,这时候可以考虑本地 Jar 启动再进行 Debug,先将项目进行打包,然后在运行模块的 target 文件夹下执行 java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=44399 travel-cube-activity-tob.war 命令,然后接下来操作和远程 Debug 方式是一样的
断点选择,对于类加载问题,断点最好选择具体功能的类,比如此 case 若选择 URLClassLoader.findClass 的话,那调试工作量太大了,可以选择 OldConfigFactoryPostProcessor.postProcessBeanFactory ,但即便如此,某些场景Debug的工作量也不小,比如此 case,若要定位到异常类,Debug 最坏情况需要处理 1698+ 次,如下图:
当确定来源依赖后,需要确认处理方式,对于此 case 需要确认缺失依赖,然后再引入,此时可以借助相关代码搜索网站,比如 www.programcreek.com (opens new window),按照上边定位到的类进行搜索,如下左图,可以确认缺失 io.github.openfeign:feign-core 、io.github.openfeign:feign-jackson、 io.github.openfeign:feign-form,引入之后可以正常启动,添加依赖可以使用 MDP 插件快速添加,如下右图:
# 4.3 依赖版本错误
场景:程序启动报异常:com.dianping.squirrel.caucho.hessian.io.LocaleSerializer is invalid because it does not implement com.caucho.hessian.io.Serializer
分析过程如下:
首先查看异常堆栈,发现是类的类型错误抛出的异常,可以确认是类加载问题,可以判定属于依赖冲突问题,如下图:
1.由于抛出异常的类 com.dianping.squirrel.caucho.hessian.io.LocaleSerializer 在项目中存在,那直接在 Idea 中直接搜索即可,确认该类来源于 com.dianping.squirrel:hessian,如下图,其实异常信息中也有提示
找到来源依赖后,并且异常的类是存在的,那不是类冲突,就是依赖版本错误,先看下依赖版本问题,这个时候可以利用 MDP 插件查看依赖版本冲突问题,如下图:
通过插件可以看到项目中存在 1.1.0 版本、1.1.1 版本,那处理操作从此两个版本中选择正确的,通过测试保留 1.1.0 版本可正常启动,但此操作属于依赖降级,需要判定此操作没影响,查看 1.1.1 版本是由 cellar 依赖引入的,但是项目中并不存在使用 cellar 的代码,因此基本判断依赖降级是没有影响的
# 4.4 类冲突
场景:程序启动异常:java.lang.VerifyError: class net.sf.cglib.core.DebuggingClassWriter overrides final method visit
分析过程如下:
首先看异常,是 DebuggingClassWriter 重写了 final 方法 visit,并且异常堆栈中是类加载时抛出的异常,因此判定该问题属于依赖冲突问题
按照异常信息提示,需要确定 DebuggingClassWriter 及其父类的依赖来源,当在 Idea 搜索 net.sf.cglib.core.DebuggingClassWriter,可以确定该类来源于 cglib,如下图:
接着访问 DebuggingClassWriter 的父类 ClassWriter,该类属于 asm:asm 依赖,但发现 visit 方法并非 final 方法,如下图:
那说明程序运行中加载的类并非 asm:asm 依赖中的 ClassWriter,大概率存在同名类,导致类冲突了,这个时候利用 Idea 搜索 ClassWriter,发现有很多,很难确定运行时加载类归属的依赖,如下图:
对于此种存在很多个同名类,且来自多个依赖的情况,想要知道运行时加载的 ClassWriter 来自于哪个依赖,可以在 Jvm 参数中加 –verbose:class 参数,可以打印出类加载路径,如下图,可以从打印的日志当中,确认 ClassWriter 来自于 net.minidev:accessors-smart 依赖:
确认来源依赖后,则需要确认处理方案,这种情况是需要让程序在运行时,加载正确的依赖,那最直接的手段就是将该依赖排除,依赖分析后,发现 accessors-smart 是由 com.jayway.jsonpath:json-path 引入,json-path 是由 com.sankuai.jbox:jbox-embed-container 引入,在 Jbox 中搜索 json-path 类路径,发现仅有测试代码使用,如下图,将该 json-path 直接排掉即可:
依赖冲突异常的发生场景及原因
异常 | 发生场景 | 发生原因 | 备注 |
---|---|---|---|
java.lang.ClassNotFoundException | Java 虚拟机(JVM)尝试加载一个类但在类路径中找不到它时 | 类不在指定的包或目录中类路径没有正确设置类没有包含在应用程序的 JAR 文件中类已被删除或重命名 | |
java.lang.NoClassDefFoundError | Java 虚拟机(JVM)尝试调用一个不存在的类时 | 类的定义文件(.class 文件)已被删除或移动类的定义文件存在,但是该文件的访问权限不足类的定义文件存在,但是该文件中引用的其他类无法被找到或加载 | |
java.lang.NoSuchMethodError | Java 虚拟机(JVM)尝试调用一个不存在的方法时 | 方法名称拼写错误或方法不存在。方法存在,但是参数类型或数量与调用方不匹配方法存在,但是访问权限不足,无法从当前位置访问该方法 | |
java.lang.NoSuchFieldError | Java 虚拟机(JVM)尝试访问一个不存在的字段时 | 字段名称拼写错误或字段不存在字段存在,但是访问权限不足,无法从当前位置访问该字段 | |
java.lang.LinkageError | Java 虚拟机(JVM)在链接阶段遇到错误时 | 类或接口的定义不一致。类或接口的定义与其引用不一致类或接口的定义与其父类或实现的接口不一致类或接口的定义与其依赖的其他类或接口不一致 | 比如:类加载器 A 加载类 C,类加载器 B 也加载类 C,当 A 中 C 的引用指向 B 中 C 的对象,此时会此报错 |
其实上述的异常就是依赖冲突的具体表现:具体可以参见附录-依赖冲突异常的场景及原因 (opens new window)
Java应用程序因某种因素,加载不到正确的类而导致其行为跟预期不一致:引自重新看待Jar包冲突问题及解决方案 (opens new window)
依赖冲突具体来讲,可以分为两种情况:引自 重新看待Jar包冲突问题及解决方案 (opens new window)
2.2 Maven 依赖机制:参考 Introduction to the Dependency Mechanism (opens new window)
编译依赖项在项目的所有类路径中都可用:指在编译类路径(src/main)、测试类路径(src/test)、运行类路径(target)下都可以使用
Maven 依赖规范:参考Maven 依赖规范-MDP官方文档 (opens new window)
MDP 插件是提供给 MDP 用户的一款周边工具,主要目的是为了推进 MDP 标准化落地、降低 MDP 使用门槛、提升用户研发效率:参考 MDP 插件开发辅助工具-MDP官方文档 (opens new window)
类加载器 A 加载类 C,类加载器 B 也加载类 C,当 A 中 C 的引用指向 B 中 C 的对象,此时会此报错:引自Java LinkageError:loader constraint violation 异常分析与解决 (opens new window)