maven小记

Posted by Thoughtliuw on 2019-11-09

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
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</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
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>

它的package就是org.apache.commons.lang3,我只需要保证collection4和lang3不同就可以保证我这两个package都是世界上独一无二的package了
当然,我们并没有忘记version,我们放到包冲突中再讲。

包冲突

包冲突仍然存在

尽管我们可以保证使用上面的方法之后,不同人不同团队开发的jar包中package肯定是不一样的,但是你自己的jar包总不可能开发出来就十全十美吧,你总得升级换代。
但是Maven是不允许你对已经发布的jar包进行修改的,你只能发布一个新的jar包来应用你的更改
这个规定的理由应该是显而易见的,你总不想你的项目今天测试还好好的,第二天啥也没改,发现项目中的类都找不到了,或是方法都不能用了吧。

所以说Maven坐标的最后一部分version就是用来区分同一个jar包的不同版本的,但是这个version并不会体现到最后的package中,它是给Maven用来区分不同的包版本的。
所以说还是可能出现我们最初遇到的情况

1
2
/org/apache/commons/commons-collections4-4.0.jar:
/org/apache/commons/commons-collections4-4.1.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
2
3
        --- A —— B —— C2(被抛弃)
我的项目
--- D —— C1(留下)

那么最终C2.jar会被抛弃,留下C1.jar (但可能被抛弃的就是我们想要的,比如我们就是想要最新的C2版本)

如果一样近怎么办?

选择在pom中先声明的

1
2
3
        --- A —— C2(留下)
我的项目
--- D —— C1(抛弃)

如何手动解决

上面提到Maven的策略不一定就是我们想要的结果,所以我们有时还是需要自己动手,丰衣足食。

方法一: 把想要的包放到最前面

1
2
3
4
5
        --- A —— B —— C2

我的项目 --- D —— C1

--- C2

方法二 :排除掉不想要的包
在pom 文件中修改依赖,加入一个exclusions:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.github.xxx</groupId>
<artifactId>lib-D</artifactId>
<version>0.1</version>
<exclusions>
<exclusion>
<groupId>com.github.thoughtliuw</groupId>
<artifactId>lib-C</artifactId>
</exclusion>
</exclusions>
</ddependency>
1
2
3
        --- A —— B —— C2
我的项目
--- D

将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
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</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实战》,书比较老,看看核心概念就好