引言
当我们使用 Maven 来构建我们的程序时,我们可以用几句配置来代替大量的 Jar 包(一个依赖会引入其依赖的其他依赖,而那些依赖也会引入其依赖的依赖,所以有依赖树这种说法),同时因为这种配置在我们交流代码时可以不用自己引入 Jar 包(避免了版本不一致而出错),只要更新 Maven,它就会在后台帮我们解决这一切。但是在我们享受这种方便的同时,我们也在为这种方便付出代价。
首先我们先来看一个例子
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
其依赖树如下
我们可以清楚地看到 hibernate-core 所依赖的 jboss-logging 被省略了 (灰色的那一个依赖),因为 Maven ( 或者说是 Java 的 ClassLoader ) 先加载了 hibernate-validator 所依赖的 jboss-logging,就会忽略其他同名依赖。 一般情况来说, 这是不会出现问题的, 因为在你使用 Maven 时, 你已经不知不觉地通过这种方式多次使你的代码正确执行了, 但是一旦出现了错误, 你多半会被坑的死去活来 ~. ~
比如我举的这个例子, 当你初始化 SessionFactory 时, 你将会见到如下错误提醒:
java.lang.NoSuchMethodError: org.hibernate.internal.CoreMessageLogger.debugf(Ljava/lang/String;II)V
首先检查类或方法是否存在,如果找到类,却没有在该类及其父类中找到该方法,那么百分百就是版本错误。
- 先找 CoreMessageLogger 类
public interface CoreMessageLogger extends BasicLogger {
}
- 在 CoreMessageLogger 类中找不到 debugf 方法,在它的父类中查找
package org.jboss.logging;
import org.jboss.logging.Logger.Level;
public interface BasicLogger{
void debugf(String var1, Object... var2);
void debugf(String var1, Object var2);
void debugf(String var1, Object var2, Object var3);
void debugf(String var1, Object var2, Object var3, Object var4);
void debugf(Throwable var1, String var2, Object... var3);
void debugf(Throwable var1, String var2, Object var3);
void debugf(Throwable var1, String var2, Object var3, Object var4);
void debugf(Throwable var1, String var2, Object var3, Object var4, Object var5);
}
我们发现了 BasicLogger 是 jboss-logging 中的类,接着我们在来找 debugf(Ljava/lang/String;II)V 这个方法(这种写法是 JNI 字段描述符,翻译之后为 void debugf( String[ ] str , int i, int j ))。
恩。很遗憾,在这个方法中并没有找到需要的方法,那么答案就出来了-- jboss-logging 版本错误
让我们回到最开始,我们知道 Maven 加载的 jboss-logging 版本为 3.1.3,而 hibernate-core 依赖的版本为 3.3.0 ,在该版本的 BasicLogger 中我们可以找到如下方法
void debugf(Stringvar1, int var2);
void debugf(String var1, int var2, int var3); // 我们需要的那个方法
void debugf(String var1, int var2, Object var3);
P.S. 说句题外话,之所以列了这个例子出来,就是希望正在寻找问题的你了解 Maven 依赖树的重要性,以后遇到这种问题可以知道怎么解决
解决上述问题有几种:
一,将重要的依赖放到前面
因为 maven 会优先加载第一个遇到的版本,所以我们只需要将 hibernate-core 移到 hibernate-validator 的前面,这样 Maven 就会加载到 3.3.0 这个版本。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.0.5.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
如上可以看到版本 3.1.3 被忽略
二,使用 exclusions 标记排除依赖
将干扰的依赖排除后就没有后顾之忧了,如下:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
<exclusions>
<exclusion>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
三,使用 mvn dependency:tree 命令查看依赖树
推荐最好学会这种方法,虽然使用 IDEA 的 maven 视图可以很容易的看到依赖树的分布,但是因为存在缓存问题,maven 窗口不能实时改变,而且 maven 窗口也没有提供筛选功能,在很多时候不太方便。
mvn dependency:tree 选项
- verbose 显示全部,不使用此选项则显示部分。当然了,这不是让你一次性显示所有依赖出来,因为太多了,显示出来的话阅读难度指数上升,有兴趣可亲身一试 ~. ~
- includes 筛选出想要的的依赖,使用时只显示被加载的 jar 包,如果和 verbose 一起使用则可以查看被忽略的 jar 包,如下
- excludes 这个就没什么好说的,includes 相反的作用
未使用 verbose 的 includes
--- maven-dependency-plugin:2.8:tree (default-cli) @ test ---
tree:test:war:1.0-SNAPSHOT
\- org.hibernate:hibernate-validator:jar:5.1.3.Final:compile
\- org.jboss.logging:jboss-logging:jar:3.1.3.GA:compile
--------------------------------------------------------------------
使用了 verbose 的includes
--- maven-dependency-plugin:2.8:tree (default-cli) @ test ---
tree:test:war:1.0-SNAPSHOT
+- org.hibernate:hibernate-validator:jar:5.1.3.Final:compile
| \- org.jboss.logging:jboss-logging:jar:3.1.3.GA:compile
\- org.hibernate:hibernate-core:jar:5.0.5.Final:compile
+- (org.jboss.logging:jboss-logging:jar:3.3.0.Final:compile - omitted for conflict with 3.1.3.GA)
\- org.hibernate.common:hibernate-commons-annotations:jar:5.0.1.Final:compile
\- (org.jboss.logging:jboss-logging:jar:3.3.0.Final:compile - omitted for conflict with 3.1.3.GA)
--------------------------------------------------------------------
可以看到被忽略的版本被打上括号并且在最后跟着 omitted for conflict with x.x.x
命令组合
mvn dependency:tree -Dverbose -Dincludes=groupId:artifactId ,如
mvn dependency:tree -Dverbose -Dincludes=org.jboss.logging:jboss-logging
终上所述,当我们遇到 ClassNotFound 或 NoSuchMethod 异常时
- 我们首先要先去确认这些方法是否存在
- 接着再去寻找它的父类与接口,一般很快就会找到
(这是真的,代码的继承深度一般不可能太深) - 当我们发现是版本问题时首先就应该定位版本,然后切换成正确版本,方法如下:
1.通过 IDEA 的 maven 窗口,在灰色选项(被忽略)中查找
2.通过 mvn dependency:tree -Dverbose -Dincludes=xx:xx - 最后通过移动依赖位置覆盖错误依赖或使用 exclusions 标记将其移除
P.S. 本人使用的 IDE 为 IntelliJ IDEA 社区版(免费的),其含有 Maven 窗口和强大的反编译功能,所以才能像我刚才说的那样追踪源码和查看错误。当然了,Eclipse 也是有相应的插件的(习惯用 Eclipse 的同学推荐安装一款反编译的插件,详情见 Java 篇:让你的 Eclipse 飞起来)