一、jar包冲突问题
1、谁需要面对 jar 包冲突?
先给结论:编订依赖列表的程序员。初次设定一组依赖,因为尚未经过验证,所以确实有可能存在各种问题,需要做有针对性的调整。那么谁来做这件事呢?我们最不希望看到的就是:团队中每个程序员都需要自己去找依赖,即使是做同一个项目,每个模块也各加各的依赖,没有统一管理。那前人踩过的坑,后人还要再踩一遍。而且大家用的依赖有很多细节都不一样,版本更是五花八门,这就让事情变得更加复杂。
所以虽然初期需要根据项目开发和实际运行情况对依赖配置不断调整,最终确定一个各方面都 OK 的版本。但是一旦确定下来,放在父工程中做依赖管理,各个子模块各取所需,这样基本上就能很好的避免问题的扩散。
即使开发中遇到了新问题,也可以回到源头检查、调整 dependencyManagement 配置的列表——而不是每个模块都要改。所以学完这一节你应该就会对前面讲过的『继承』有了更深的理解。
2、表现形式
由于实际开发时我们往往都会整合使用很多大型框架,所以一个项目中哪怕只是一个模块也会涉及到大量 jar 包。数以百计的 jar 包要彼此协调、精密配合才能保证程序正常运行。而规模如此庞大的 jar 包组合在一起难免会有磕磕碰碰。最关键的是由于 jar 包冲突所导致的问题非常诡异,这里我们只能罗列较为典型的问题,而没法保证穷举。
但是我们仍然能够指出一点:一般来说,由于我们自己编写代码、配置文件写错所导致的问题通常能够在异常信息中看到我们自己类的全类名或配置文件的所在路径。如果整个错误信息中完全没有我们负责的部分,全部是框架、第三方工具包里面的类报错,这往往就是 jar 包的问题所引起的。
而具体的表现形式中,主要体现为找不到类或找不到方法。
①抛异常:找不到类
此时抛出的常见的异常类型:
- java.lang.ClassNotFoundException:编译过程中找不到类
- java.lang.NoClassDefFoundError:运行过程中找不到类
- java.lang.LinkageError:不同类加载器分别加载的多个类有相同的全限定名
我们来举个例子:
httpclient 这个 jar 包中有一个类:org.apache.http.conn.ssl.NoopHostnameVerifier。这个类在较低版本中没有,但在较高版本存在。比如:
jar 包版本 | 是否存在 |
---|---|
4.3.6 | 否 |
4.4 | 是 |
那当我们确实需要用到 NoopHostnameVerifier 这个类,我们看到 Maven 通过依赖传递机制引入了这个 jar 包,所以没有明确地显式声明对这个 jar 包的依赖。可是 Maven 传递过来的 jar 包是 4.3.6 版本,里面没有包含我们需要的类,就会抛出异常。
而『冲突』体现在:4.3.6 和 4.4 这两个版本的 jar 包都被框架所依赖的 jar 包给传递进来了,但是假设 Maven 根据**『版本仲裁』**规则实际采纳的是 4.3.6。
②抛异常:找不到方法
程序找不到符合预期的方法。这种情况多见于通过反射调用方法,所以经常会导致:java.lang.NoSuchMethodError。比如 antlr:antlr:x.x.x 这个包中有一个接口:antlr.collections.AST
版本 | getLine()方法 |
---|---|
2.7.2 | 无 |
2.7.6 | 有 |
③没报错但结果不对
发生这种情况比较典型的原因是:两个 jar 包中的类分别实现了同一个接口,这本来是很正常的。但是问题在于:由于没有注意命名规范,两个不同实现类恰巧是同一个名字。
具体例子是有的同学在实际工作中遇到过:项目中部分模块使用 log4j 打印日志;其它模块使用 logback,编译运行都不会冲突,但是会引起日志服务降级,让你的 log 配置文件失效。比如:你指定了 error 级别输出,但是冲突就会导致 info、debug 都在输出。
3、本质
以上表现形式归根到底是两种基本情况导致的:
①同一jar包的不同版本
②不同jar包中包含同名类
这里我们拿 netty 来举个例子,netty 是一个类似 Tomcat 的 Servlet 容器。通常我们不会直接依赖它,所以基本上都是框架传递进来的。那么当我们用到的框架很多时,就会有不同的框架用不同的坐标导入 netty。大家可以参照下表对比一下两组坐标:
截止到3.2.10.Final版本以前的坐标形式: | 从3.3.0.Final版本开始以后的坐标形式: |
---|---|
但是偏偏这两个**『不同的包』里面又有很多『全限定名相同』**的类。例如:
org.jboss.netty.channel.socket.ServerSocketChannelConfig.class org.jboss.netty.channel.socket.nio.NioSocketChannelConfig.class org.jboss.netty.util.internal.jzlib.Deflate.class org.jboss.netty.handler.codec.serialization.ObjectDecoder.class org.jboss.netty.util.internal.ConcurrentHashMap$HashIterator.class org.jboss.netty.util.internal.jzlib.Tree.class org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Segment.class org.jboss.netty.handler.logging.LoggingHandler.class org.jboss.netty.channel.ChannelHandlerLifeCycleException.class org.jboss.netty.util.internal.ConcurrentIdentityHashMap$ValueIterator.class org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Values.class org.jboss.netty.util.internal.UnterminatableExecutor.class org.jboss.netty.handler.codec.compression.ZlibDecoder.class org.jboss.netty.handler.codec.rtsp.RtspHeaders$Values.class org.jboss.netty.handler.codec.replay.ReplayError.class org.jboss.netty.buffer.HeapChannelBufferFactory.class
……
其实还有很多,这里列出的只是冰山一角。
当然,如果全限定名相同,类中的代码也完全相同,那么用着也行。问题是如果**『全限定名相同』,但是『代码不同』**,那可太坑了。我们随便找一个来看看:
坐标信息:org.jboss.netty:netty:jar:3.2.10.Final |
---|
代码截图: |
坐标信息:io.netty:netty:jar:3.9.2.Final |
---|
代码截图: |
4、解决办法
①概述
很多情况下常用框架之间的整合容易出现的冲突问题都有人总结过了,拿抛出的异常搜索一下基本上就可以直接找到对应的 jar 包。我们接下来要说的是通用方法。
不管具体使用的是什么工具,基本思路无非是这么两步:
- 第一步:把彼此冲突的 jar 包找到
- 第二步:在冲突的 jar 包中选定一个。具体做法无非是通过 exclusions 排除依赖,或是明确声明依赖。
②IDEA 的 Maven Helper 插件
这个插件是 IDEA 中安装的插件,不是 Maven 插件。它能够给我们罗列出来同一个 jar 包的不同版本,以及它们的来源。但是对不同 jar 包中同名的类没有办法。
- 基于 pom.xml 的依赖冲突分析
③Maven 的 enforcer 插件
使用 Maven 的 enforcer 插件既可以检测同一个 jar 包的不同版本,又可以检测不同 jar 包中同名的类。
- 引入 netty 依赖
这里我们引入两个对 netty 的依赖,展示不同 jar 包中有同名类的情况。
|
|
- 配置 enforcer 插件
|
|
- 测试
执行如下 Maven 命令:
部分运行结果:
[INFO] — maven-enforcer-plugin:1.4.1:enforce (default-cli) @ pro32-duplicate-class — [WARNING] Rule 0: org.apache.maven.plugins.enforcer.BanDuplicateClasses failed with message: Duplicate classes found:
Found in: io.netty:netty:jar:3.9.2.Final:compile org.jboss.netty:netty:jar:3.2.10.Final:compile Duplicate classes: org/jboss/netty/channel/socket/ServerSocketChannelConfig.class org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.class org/jboss/netty/util/internal/jzlib/Deflate.class org/jboss/netty/handler/codec/serialization/ObjectDecoder.class org/jboss/netty/util/internal/ConcurrentHashMap$HashIterator.class
……
二、体系外jar包导入
1、提出问题
目前来说我们在 Maven 工程中用到的 jar 包都是通过 Maven 本身的机制导入进来的。
而实际开发中确实有可能用到一些 jar 包并非是用 Maven 的方式发布,那自然也没法通过 Maven 导入。
此时如果我们能够拿到该 jar 包的源码那还可以自己建一个 Maven 工程,自己打包。可是如果连源码都没有呢?
这方面的例子包括一些人脸识别用的 jar 包、海康视频监控 jar 包等等。
2、解决办法
①准备一个体系外 jar 包
通过学 Maven 以前的方式创建一个 Java 工程,然后导出 jar 包即可用来测试。
②将该 jar 包安装到 Maven 仓库
这里我们使用 install 插件的 install-file 目标:
例如(Windows 系统下使用 ^ 符号换行;Linux 系统用 \):
执行结果:
再看本地仓库中确实有:
我们打开 POM 文件看看:
|
|
③测试
在其它地方依赖这个 jar 包:
创建对象、调用方法:
三、Nexus私服
1、安装
- 下载
https://sonatype-download.global.ssl.fastly.net/repository/downloads-prod-group/3/nexus-3.38.1-01-unix.tar.gz
https://download.sonatype.com/nexus/3/latest-unix.tar.gz
- 上传到linux
上传到 Linux 系统,解压后即可使用,不需要安装。但是需要注意:必须提前安装 JDK
- java环境
|
|
- 开放端口或者禁用防火墙
2、启动nexus
[root@x ~]# /opt/nexus-3.37.0-01/bin/nexus start WARNING: ************************************************************ WARNING: Detected execution as “root” user. This is NOT recommended! WARNING: ************************************************************ Starting nexus [root@x ~]# /opt/nexus-3.37.0-01/bin/nexus status WARNING: ************************************************************ WARNING: Detected execution as “root” user. This is NOT recommended! WARNING: ************************************************************ nexus is running.
- 查看端口
[root@x ~]# netstat -anp | grep java tcp 0 0 127.0.0.1:45614 0.0.0.0:* LISTEN 9872/java tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN 9872/java
上面 45614 这个每次都不一样,不用管它。我们要访问的是 8081 这个端口。但是需要注意:8081 端口的这个进程要在启动 /opt/nexus-3.37.0-01/bin/nexus 这个主体程序一、两分钟后才会启动,请耐心等待。
- 问题
- 解决:
root为系统用户,前面要加上@
3、访问 Nexus 首页
首页地址:http://[Linux 服务器地址]:8081/
这里参考提示:
- 用户名:admin
- 密码:查看 /opt/sonatype-work/nexus3/admin.password 文件
[root@hello ~]# cat /opt/sonatype-work/nexus3/admin.password ed5e96a8-67aa-4dca-9ee8-1930b1dd5415
给 admin 用户指定新密码:
匿名登录,启用还是禁用?由于启用匿名登录后,后续操作比较简单,这里我们演示禁用匿名登录的操作方式:
4、nexus各种库
在仓库列表中,每个仓库都具有一系列属性:
- Type:仓库的类型,Nexus 中有 4 中仓库类型:group(仓库组)、hosted(宿主仓库)、proxy(代理仓库)以及 virtual(虚拟仓库)。
- Format:仓库的格式。
- Policy:仓库的策略,表示该仓库是发布(Release)版本仓库还是快照(Snapshot)版本仓库。
- Repository Status:仓库的状态。
- Repository Path:仓库的路径。
仓库类型 | 说明 |
---|---|
proxy | 用来代理远程公共仓库,如 Maven 中央仓库、JBoss 远程仓库。 |
group | 用来聚合代理仓库和宿主仓库,为这些仓库提供统一的服务地址,以便 Maven 可以更加方便地获得这些仓库中的构件。 |
hosted | 存放:本团队其他开发人员部署到 Nexus 的 jar 包 |
仓库名称 | 说明 |
---|---|
maven-central | 该仓库用来代理 Maven 中央仓库,其策略为 Release,只会下载和缓存中央仓库中的发布版本的构件 |
maven-public | 该仓库组将上述所有存储策略为 Release 的仓库聚合并通过统一的地址提供服务 |
maven-releasse | 策略为 Release 的宿主仓库,用来部署公司或组织内部的发布版本构件。 |
maven-snapshots | 策略为 Snapshot 的宿主仓库,用来部署公司或组织内部的快照版本构件。 |
初始状态下,这几个仓库都没有内容
由上图可知:
- Maven 可以直接从宿主仓库中下载构件。
- Maven 也可以从代理仓库中下载构件,代理仓库会从远程仓库下载并缓存构件。
- Maven 还可以从仓库组中下载构件,仓库组会从其包含的宿主仓库和代理仓库中获取构件。
5、开启索引
Nexus 作为一款成熟的仓库管理工具,它通过维护仓库的索引提供了构件搜索功能,以便帮助用户方便快速地找到所需构件。
Nexus 索引下载功能默认是关闭的,如果想在 Nexus 中搜索远程仓库中的构件,就需要先开启索引下载功能。
Nexus 能够遍历仓库的所有内容,搜集它们的坐标,校验和以及所包含的 Java 类等信息,然后以索引( nexus-indexer) 的形式保存起来。Nexus 索引保存在 Nexus 安装目录下 \sonatype-work\nexus\indexer 目录中,该目录下每个子目录都代表 Nexus 中的一个仓库,用来存放各个仓库的索引,大多数的远程公共仓库(例如,中央仓库)都维护了一个这样的索引,因此本地的 Nexus 在下载到这个索引后,就能在此基础上为用户提供构件搜索和浏览等服务。需要注意的是,并不是所有的公共仓库都提供了索引 ,对于那些没有提供索引的仓库来说,我们是无法对其进行搜索的。
- 搜索:
6、新建阿里云代理
- 修改maven-public
7、下载构件
两种方式:pom.xml 和 setting.xml
①、在 pom.xml 中配置
|
|
②、在 setting.xml 中配置
Nexus 私服通常会与镜像(mirror)结合使用,使 Nexus 成为所有远程仓库的私服,这样不仅可以从 Nexus 中获取所有所需构件,还能将配置集中到 Nexus 私服中,简化 Maven 本身的配置。
我们可以创建一个匹配任何仓库的镜像,镜像的地址为 Nexus 中仓库的地址,这样 Maven 对于任何构件的下载请求都会被拦截跳转到 Nexus 私服中,其具体配置如下。
|
|
8、上传构件
- 首先pom.xml :
|
|
- 并且settings.xml :
四、复制仓库
复制仓库,jar包的mirrorId不一样,所以不会用到本地包
_remote.repositories: mine和central是mirrorId
需要执行脚本 删除lastUpdated 和 _remote.repositories