Java的包管理和它的问题
在Java中package + 类名
就代表了一个唯一的类,如果只有我们自己的程序,这当然没有问题。
但是当你引用外部类的时候,则可能出现不同的jar包中会有相同的package,但是这并不会产生冲突,因为JVM会运行它找到的第一个jar包
假设我们的JVM找到的classpath是这样的(冒号表示分隔符):
1 | C:/test1/old.jar:C:/test2/new.jar |
这两个jar包中都有一个 com.github.util
包,包中还都有一个StringUtil
类
那么JVM在找到old.jar中的StringUtil
类之后就不会再往下找了,但我们想要的却是new.jar中的StringUtil
类
问题由此产生,但似乎又没办法解决
maven的作用之一就是来解决这样的问题
Maven坐标
让package成为唯一的
为了解决上面这个问题,Maven使用了一种约定来保证包的唯一性,那就是我们常见的三剑客。
1 | <dependency> |
groupId 加上 artifactId其实就已经确定了一个唯一的package,这个package就是org.apache.commons.collections4
(前面一部分groupId和org.apache.commons是严格对应的,但是后面一部分并不一定和artifactId是严格对应)
当然,这只是一种约定,你当然可以groupId写成这样,package写成那样,但是你这样乱搞,就没人跟你玩儿了,这就是Maven 约定重于配置 的理念
groupId
通常会采用公司的域名,域名大家都知道是唯一的,那么groupId就是唯一的,那么org.apache.commons就是唯一的
然后你只需要保证你的不同的jar包中没有重复的package就可以了,例如说另外一个这个公司的jar包:
1 | <dependency> |
它的package就是org.apache.commons.lang3
,我只需要保证collection4和lang3不同就可以保证我这两个package都是世界上独一无二的package了
当然,我们并没有忘记version
,我们放到包冲突中再讲。
包冲突
包冲突仍然存在
尽管我们可以保证使用上面的方法之后,不同人不同团队开发的jar包中package肯定是不一样的,但是你自己的jar包总不可能开发出来就十全十美吧,你总得升级换代。
但是Maven是不允许你对已经发布的jar包进行修改的,你只能发布一个新的jar包来应用你的更改。
这个规定的理由应该是显而易见的,你总不想你的项目今天测试还好好的,第二天啥也没改,发现项目中的类都找不到了,或是方法都不能用了吧。
所以说Maven坐标的最后一部分version
就是用来区分同一个jar包的不同版本的,但是这个version并不会体现到最后的package中,它是给Maven用来区分不同的包版本的。
所以说还是可能出现我们最初遇到的情况
1 | /org/apache/commons/commons-collections4-4.0.jar: |
那么此时我们应该用哪个包呢?
传递性依赖
在解决上面那个问题之前,我们先停下来看一下什么情况下会出现上面那种问题。
诶,不是讲了吗,引用相同的包的时候啊,但是一般来说我们是不会吃饱了没事儿干引入两个相同的dependency吧,但是包冲突依然时常发生,为什么?
答案就是Maven的传递性依赖,我们在引入一个包的时候,它可能需要其他的包,Maven会自动的帮我们导入它需要的包。
commons-collections4的maven依赖
在页面底部会有下面这样一个东西,列举了commons-collections4
本身还需要的4个jar包,而Maven会帮我们全部引入
Compile Dependencies (4)
说到这里问题已经呼之欲出了,尽管我们不会自己引入相同jar包的不同版本,但是在传递性依赖中则很有可能出现。
比如我的collection4和lang3都依赖了test.jar,但一个依赖的是test-1.0.jar,一个依赖的是test-2.0.jar,那么包冲突就产生了。
如何解决包冲突
原则
最终的依赖包中不允许相同的包出现(classpath中不会出现两个相同的jar包)
所以最终相同的包只会剩下一个,其他的都会被Maven丢掉。
具体策略(Maven 的策略)
选择最近的一个
1 | --- A —— B —— C2(被抛弃) |
那么最终C2.jar会被抛弃,留下C1.jar (但可能被抛弃的就是我们想要的,比如我们就是想要最新的C2版本)
如果一样近怎么办?
选择在pom中先声明的
1 | --- A —— C2(留下) |
如何手动解决
上面提到Maven的策略不一定就是我们想要的结果,所以我们有时还是需要自己动手,丰衣足食。
方法一: 把想要的包放到最前面
1 | --- A —— B —— C2 |
方法二 :排除掉不想要的包
在pom 文件中修改依赖,加入一个exclusions:
1 | <dependency> |
1 | --- A —— B —— C2 |
将D包中所依赖的C包干掉(注意:这里是不用加版本号的)
图形化操作:这个排除的过程可以通过idea的图形化操作来完成:
通过一个插件Maven helper,具体用法请自行google
如何发现冲突
讲完解决冲突才讲怎么发现冲突有点怪,但是我认为这样的理解可能更深入
当你在项目中突然报错某个类或者某个方法找不到,而这个类又不是你自己写的,这个时候你很可能就遇到了包冲突的问题
-
命令行
1 | mvn dependency:tree |
得到结果后可以贴出来,也可以用另外一个命令
1 | mvn dependency:tree > tmp.txt |
后面这一部分的意思是将结果重定向(或者可以暂时理解为导入)到tmp.txt中方便查看
当然这只是这个命令的一部分,我在网上找到一篇文章讲的比较清楚:
mvn dependency:tree 查看jar包依赖关系
-
IDE
我们更多的可能会在IDEA中进行Maven依赖包的查看,
在IDEA自带的maven模块中我们可以直接搜索包名进行查找
-
插件
在IDEA中安装一个叫做Maven Helper的插件,它会自动帮你分析哪些依赖冲突了,右键还可以点击exclude,它就会自动的帮你在pom中添加exclusions的标签。
-
命令行这么麻烦能不能不学
既然有了这么好的IDE工具,插件,那还要命令行干嘛,但是有时候线上依赖可能和本地依赖不一样,我们又没办法拿到源代码,只能使用命令行,所以命令行的方式还是要学会的,至少要了解,要用的时候知道去查什么
作用域
maven的作用域scope
的作用:
让不同的jar包在不同的环境下起作用,而在另外一些环境不起作用
-
test
1 | <dependency> |
只在test文件夹中有用,在main中没有用,不信你试试设置test作用域之后在main中试试@Test
能不能用
-
provided
只在编译的时候有效,运行的时候不生效
这有什么卵用?
比如我们在部署web应用的时候,Tomcat中一些jar包已经有了,例如servlet.jar,那你就应该在运行的时候自己的jar包去掉,避免冲突
-
compile
编译和运行的时候都生效
其他的一些小知识
-
Maven 中央仓库
http://repo1.maven.org/maven2/ -
Maven 快照版本 (SNAPSHOT)
Maven不允许对一个已经发布的版本进行更新,所以一般在开发者内部会通过SNAPSHOT来对一个版本进行频繁的更新
冰山之下
这篇文章中对于Maven具体怎么用的没有讲太多,更多的是一些概念上的理解,并且Maven远不止是一个包管理工具,更是一个自动化构建工具(留待另外一篇文章说明)。
对于Maven系统的了解可以参考《Maven实战》,书比较老,看看核心概念就好