前序
在实际项目开发中,经常会遇到程序在没有做任何修改的情况下无法启动了。前两天正在电脑前发呆,突然隔壁的程序媛小王火急火燎的说,涛哥帮忙看一个问题吧?这个问题已经阻塞了一中午了。看到异常信息后,第一反应是这种问题99.9999%的原因是maven依赖包有不兼容的问题。不过用idea的Dependency Analyzer排查时发现根本就不存在有冲突的包。这下发现遇到真问题了。虽然嘴上跟人家说是因为版本冲突导致的,但是心里面还是开始犯嘀咕了。不过最后还是把问题给发现了,同时也证明了当时自己的判断是正确的。
问题重现与解决
为了重现当时的问题,在本文中创建了一个mini版本的项目来说明问题的表象,出现启动异常的原因以及解决方法。
演示项目的整体结构如下图
程序启动时的异常堆栈信息
针对以上异常,做一个简单的分析,其中collision为一个Parent,在该pom中引入的spring boot版本为2.1.3.RELEASE,同时该maven工程中又设置了属性,其中spring-boot的版本为2.0.5.RELEASE。进行初步的debug后发现org.springframework.core.KotlinDetector类里面的isKotlinReflectPresent()方法确实不存在。
这时再通过spring的doc发现,isKotlinReflectPresent()方法是从spring 5.1版本才引入的。通过依赖分析可以发现在项目中引入的spring-core版本是5.0.9。
这时,我们可以通过引入的spring的版本来反推出spring boot的版本。因为spring boot 2.0.5.RELEASE是基于spring 5.0.9.RELEASE来开发的,spring boot 2.1.3.RELEASE是基于spring 5.1.5.RELEASE来开发的。
有了上面的初步分析,将上面的spring boot版本进行统一处理后,上面的问题自然就解决了。
透过现象看本质
在上一章节中,已经对产生的原因做了初步的分析并把问题解决掉了。针对上面的问题,我们是否可以在做一些深入的分析和总结呢?答案是肯定了。在分析的过程中,笔者还学到了maven的依赖加载相关的知识点,在这里也一并与大家进行分享和实例演示。与大家一起学习和探讨。
在实际的项目开发中除了会遇到NoSuchMethodError,还会遇到例如ClassNotFoundException,NoClassDefFoundError等问题。一般都会把这些异常归结为依赖版本的冲突或者说版本兼容性问题。
NoSuchMethodError
根据java api文档里面的描述,当类的定义发生了不兼容的改变,程序运行时便会抛出该异常。为了简单化,笔者做了三个更简化的演示程序。其中incompatible项目依赖于third-party-dependency-a项目,third-party-dependency-a项目依赖于third-party-dependency-b的1.1-SNAPSHOT版本。同时在本地的私服里面还存在third-party-dependency-b的1.0-SNAPSHOT版本。
third-party-dependency-b 1.0-SNAPSHOT版本的接口定义与实现
package com.mc.third.party.dependency.service;
/**
* @author M.C
* @description SayFeedBackService
* @date 2019-02-28 14:44
**/
public interface SayFeedBackService {
/**
* sayFeedBackForChinese
*/
public String sayFeedBackForChinese();
}
接口的具体实现
package com.mc.third.party.dependency.service.impl;
import com.mc.third.party.dependency.service.SayFeedBackService;
import lombok.extern.slf4j.Slf4j;
/**
* @author M.C
* @description SayFeedBackServiceImpl
* @date 2019-02-28 14:45
**/
@Slf4j
public class SayFeedBackServiceImpl implements SayFeedBackService {
/**
* sayFeedBackForChinese
*/
@Override
public String sayFeedBackForChinese() {
log.info("你好!我来自中国。");
return "你好!我来自中国。";
}
}
third-party-dependency-b 1.1-SNAPSHOT版本的接口定义与实现
package com.mc.third.party.dependency.service;
/**
* @author M.C
* @description SayFeedBackService
* @date 2019-02-28 14:44
**/
public interface SayFeedBackService {
/**
* sayFeedBackForChinese
*/
public String sayFeedBackForChinese();
/**
* sayFeedBackForAmerican
*/
public String sayFeedBackForAmerican();
}
具体实现
package com.mc.third.party.dependency.service.impl;
import com.mc.third.party.dependency.service.SayFeedBackService;
import lombok.extern.slf4j.Slf4j;
/**
* @author M.C
* @description SayFeedBackServiceImpl
* @date 2019-02-28 14:45
**/
@Slf4j
public class SayFeedBackServiceImpl implements SayFeedBackService {
/**
* sayFeedBackForChinese
*/
@Override
public String sayFeedBackForChinese() {
log.info("你好!我来自中国。");
return "你好!我来自中国。";
}
/**
* sayFeedBackForAmerican
*/
@Override
public String sayFeedBackForAmerican() {
log.info("Hello! I am come from the USA.");
return "Hello! I am come from the USA.";
}
}
third-party-dependency-a的接口定义与实现
package com.mc.third.party.dependency.service;
/**
* @author M.C
* @description SayHelloService
* @date 2019-02-28 14:31
**/
public interface SayHelloService {
/**
*
*/
public String sayHello(String country);
}
具体的实现类
package com.mc.third.party.dependency.service.impl;
import com.mc.third.party.dependency.service.SayFeedBackService;
import com.mc.third.party.dependency.service.SayHelloService;
import lombok.extern.slf4j.Slf4j;
/**
* @author M.C
* @description SayHelloServiceImpl
* @date 2019-02-28 14:32
**/
@Slf4j
public class SayHelloServiceImpl implements SayHelloService {
SayFeedBackService sayFeedBackService = new SayFeedBackServiceImpl();
/**
* sayHello
* @param country
*/
@Override
public String sayHello(String country) {
String result="";
switch (country){
case "China":
result = sayFeedBackService.sayFeedBackForChinese();break;
case "US":
result = sayFeedBackService.sayFeedBackForAmerican();break;
default: log.info("功能还未实现");break;
}
return result;
}
}
incompatible工程为一个调用的客户端
package com.mc.incompatible;
import com.mc.third.party.dependency.service.SayHelloService;
import com.mc.third.party.dependency.service.impl.SayHelloServiceImpl;
import lombok.extern.slf4j.Slf4j;
/**
* @author M.C
* @description Incompatible
* @date 2019-02-28 16:23
**/
@Slf4j
public class Incompatible {
/**
* main
* @param args
*/
public static void main(String[] args) {
SayHelloService sayHelloService = new SayHelloServiceImpl();
log.info(sayHelloService.sayHello("China"));
log.info(sayHelloService.sayHello("US"));
}
}
正常情况下maven的依赖配置
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>com.mc</groupId>
<artifactId>third-party-dependency-a</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
正常时程序执行结果
把上面的pom文件做一个修改,比如我们错误的引入了third-party-dependency-b的1.0版本。
<dependency>
<groupId>com.mc</groupId>
<artifactId>third-party-dependency-a</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.mc</groupId>
<artifactId>third-party-dependency-b</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
再次执行上面的程序,就会发现出现异常了。
通过日志可以发现,出现的异常信息与之前遇到的问题是如此的惊人相似。因为他们的根本原因都是一样的。客户端通过maven依赖引入了a,然后a通过依赖传递引入了b的1.1版本。同时客户端也直接引入了b的旧版本1.0。但是为什么maven不选择一个较高的依赖版本,反而用的是旧的版本呢。这时候就需要简单了解一下maven的仲裁机制。
- 优先按照依赖管理**<dependencyManagement>**元素中指定的版本声明进行仲裁,此时下面的两个原则都无效了
- 若无版本声明,则按照“短路径优先”的原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本
- 若路径长度一致,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本
有了上面的仲裁机制,就能知道为什么在客户端运行的时候,会出现异常了。首先在演示项目中没有dependencyManagement,所以进行短路径优先的原则。针对依赖b的路径,一条是app->a-b 另外一个是app->b 。
ClassNotFoundException/NoClassDefFoundError
在什么情况下,会出现ClassNotFoundExcetion/NoClassDefFoundError 呢?针对这个问题,先把之前的演示项目做一个改造。
<dependency>
<groupId>com.mc</groupId>
<artifactId>third-party-dependency-a</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.mc</groupId>
<artifactId>third-party-dependency-b</artifactId>
</exclusion>
</exclusions>
</dependency>
执行客户端程序结果
Exception in thread "main" java.lang.NoClassDefFoundError: com/mc/third/party/dependency/service/SayFeedBackService
at com.mc.incompatible.Incompatible.main(Incompatible.java:19)
Caused by: java.lang.ClassNotFoundException: com.mc.third.party.dependency.service.SayFeedBackService
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
因为在程序运行时需要SayFeedBackService,但是在POM中把该类包含的依赖做了排除处理。导致在运行时没有找到SayFeedBackService。这类典型异常通常是由于,没有在依赖管理中声明版本。还有一个原因是maven仲裁的时候选取了错误版本,而这个版本缺少我们需要的某个class而导致该错误。
NoClassDefFoundError和ClassNotFoundException都是由于在CLASSPATH下找不到对应的类而引起的,通常是缺少对应的jar包,JVM认为:(1)当应用运行时没有找到对应的引用,则会抛出java.lang.NoClassDefFoundError;(2)当你在代码中显式加载类(使用Class.forName())时没有找到对应的类,则会抛出java.lang.ClassNotFoundException。开发者经常遇到的情况是:ClassNotFoundException异常引起了ClassNoDefFoundError。
最佳实践
对于NoSuchMethodError、NoClassDefFoundError或ClassNotFoundException包冲突问题,通常的做法是用**<excludes>排除不需要的版本,但这种做法带来的问题是每次引入带有传递性依赖的Jar包时,都需要一一进行排除,非常麻烦。maven为此提供了集中管理依赖信息的机制,即依赖管理元素<dependencyManagement>**,对依赖Jar包进行统一版本管理,一劳永逸。通常的做法是,在parent模块的pom文件中尽可能地声明所有相关依赖Jar包的版本,并在子pom中简单引用该构件即可。
重新回头看一开始的那个问题,根据最佳实践,最终修改后的pom文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mc</groupId>
<artifactId>collision</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>collision-service</module>
<module>collision-domain</module>
<module>collision-dao</module>
<module>collision-controller</module>
<module>collision-common</module>
</modules>
<properties>
<spring.boot.version>2.1.3.RELEASE</spring.boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
子模块的pom定义
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>collision</artifactId>
<groupId>com.mc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>collision-controller</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mc</groupId>
<artifactId>collision-service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
后序
在实际的开发中,一个小小的异常就可以引起这么多的知识点,也是在开始写这篇文字是没有想到的。感觉有时候问题别看着小,但是就怕深入的挖掘。