你好,我是看山。
今天项目依赖了一个基础组件之后,启动失败,排查过程走了一些弯路,最终确认是因为依赖组件版本冲突造成了java.lang.NoClassDefFoundError
异常。下面是排查过程,希望可以给你提供一些思路。
观察异常栈
下面是打印的异常栈信息,从其中提炼可能的关键信息,能够找到“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”,还有“Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]”。继续从异常栈中找一下发生的时机,可以发现是调用AbstractAutowireCapableBeanFactory.createBeanInstance
时,这个方法是创建 Bean 实例。
这块是异常信息(getMessage 的内容,横向太长,手动换行了):
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]:
Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0;
nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'addressMapper' defined in file [/Users/liuxinghao/Documents/work/code/cn.howardliu/effective-spring/target/classes/cn/howardliu/demo/AddressMapper.class]:
Unsatisfied dependency expressed through constructor parameter 0:
Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]:
Failed to convert value of type 'java.lang.String' to required type 'java.lang.Class';
nested exception is java.lang.IllegalArgumentException:
Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]
下面是异常栈:
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:799) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:540) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1341) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]
其他异常栈信息可以忽略了
我们可以根据目前有效的信息进行排查,首先看下我们的cn.howardliu.demo.AddressMapper
定义是否有问题,再看看依赖它的 Service 有没有问题,什么问题也没有发现。下一个检查点是配置,比如@MapperScan
是否正确、Mapper 类上有没有加上@Mapper
注解,发现也没有问题。
从异常信息找不到思路了,只能从代码入手了。
这里需要说一下,打印异常信息至关重要,直接影响我们排错的思路。如果打印的信息没有办法准确定位,我们将会花费大量的时间查找真正的错误,这就需要走查代码,有时候还需要一些经验。
定位问题
我们由异常栈``ConstructorResolver.createArgumentArray(ConstructorResolver.java:799) 入手,跟着断点往下追,最终会追到org.springframework.util.ClassUtils#forName
方法,其中会抛出异常的代码是下面这块:
try {
return Class.forName(name, false, clToUse);
}
catch (ClassNotFoundException ex) {
int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR);
if (lastDotIndex != -1) {
String innerClassName =
name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1);
try {
return Class.forName(innerClassName, false, clToUse);
}
catch (ClassNotFoundException ex2) {
// Swallow - let original exception get through
}
}
throw ex;
}
出现错误的是Class.forName(name, false, clToUse)
这句,name
传的是"cn.howardliu.demo.AddressMapper"字符串,抛出的异常是java.lang.NoClassDefFoundError
,由于不是ClassNotFoundException
异常,不会进入catch
逻辑,会直接向上抛出。
找到错误我们就好定位问题了。
一般来说,java.lang.NoClassDefFoundError
错误是需要加载的类能够找到,但是加载时出现了异常,简单说就是,类的定义有问题。我们借助 JD-GUI 反编译一下运行 jar 包,结果如下:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.howardliu.demo.Address;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AddressMapper extends BaseMapper<Address> {}
观察仔细的话,我们可以看到import com.baomidou.mybatisplus.core.mapper.BaseMapper;
这行没有下划线,也就是说,在反编译工具中追溯不到这个接口,推断出来就是在运行环境中,找不到BaseMapper
这个类定义。
所以,当Class.forName
加载类的时候抛出了java.lang.NoClassDefFoundError
异常。
解决问题
如果有一定经验,就会立刻想到,大概率出现了依赖 jar 的版本冲突。
我们可以借助 maven 命令行找到版本冲突的依赖:
mvn dependency:tree -Dverbose | grep conflict
打印结果为:
[INFO] | +- (com.baomidou:mybatis-plus:jar:3.1.2:compile - omitted for conflict with 2.1.6)
我们也可以借助 IDEA 的可视化工具,在 pom.xml 上打开依赖图:
我们可以看到 mybatis-plus 的红线指示出冲突信息:
结论就是 Mybatis-Plus 版本冲突了,项目中依赖了 mybatis-plus 的 2.1.6 和 3.1.2 两个版本,由于 2.1.6 路径更短,最终被选中。
此时只需要将低版本的依赖去掉即可。
复盘问题
mybatis-plus 的版本问题
为什么低版本的 mybatis-plus 会造成类加载失败呢?是因为 mybatis-plus 跨版本更新时,把BaseMapper
的包路径改了:
// 3.1.2 版本
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// 2.1.6 版本
import com.baomidou.mybatisplus.mapper.BaseMapper;
而且还不止这一个,IService
、ServiceImpl
、TableName
、TableField
、Model
、TableField
等等,很多常用的类都改了位置。所以会造成找不到依赖的类。编译是 3.1.2 依赖还在运行环境中,就会出现编译没有问题,执行时出现加载类异常。
想要工程化的解决这个问题,我们可以创建基础的依赖 bom 配置,定义好基础依赖包,在项目中不在指定版本。这样做到统一版本,可以有效的避免这类问题。
我们还可以在 CI/CD 中加入冲突依赖检查,如果发现冲突依赖,就终止流水线。
真实异常被隐藏问题
接下来我们看下为什么明明是java.lang.NoClassDefFoundError
异常,结果异常栈中打印的是一堆不相干的错误。继续跟着刚才的断点 Debug:
org.springframework.util.ClassUtils#resolveClassName
会捕捉LinkageError
错误,然后包装成IllegalArgumentException
异常,这个时候真是异常还是继续上抛。
然后在org.springframework.beans.TypeConverterSupport#convertIfNecessary
方法会包装成TypeMismatchException
异常,此时,真实异常还在异常cause
参数中,并没有丢失。
等回到org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray
方法后,捕捉异常的方法是:
try {
convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);
}
catch (TypeMismatchException ex) {
throw new UnsatisfiedDependencyException(
mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam),
"Could not convert argument value of type [" +
ObjectUtils.nullSafeClassName(valueHolder.getValue()) +
"] to required type [" + paramType.getName() + "]: " + ex.getMessage());
}
此时我们可以注意到,在包装成UnsatisfiedDependencyException
异常的时候,只是把捕捉到的TypeMismatchException
通过getMessage
方法追加在异常描述后面,此时经过前面几轮的包装再包装,真实的异常的异常信息仅剩Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]
这段经过处理的信息,完全没有java.lang.NoClassDefFoundError
的影子了。
至此,真实异常消失无踪。
这也给我们一个提醒,我们要保证异常的时候,一定要保留有效信息,否则,排错会非常麻烦。
文末总结
本文是抓虫文,从问题出发,到解决问题,给出完整的思路。java.lang.NoClassDefFoundError
一般都是出现在版本冲突的时候,这种异常是编译打包没有问题,在运行时加载类失败。在本文中之所以排查时走了一些弯路,是因为Spring
隐藏了真实异常,给我们排错造成了一些阻碍。所以,我们在日常开发时也要重视异常的明确信息,可以给我们排错提供准确的目标。
青山不改,绿水长流,我们下次见。
推荐阅读
- 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
- 一文掌握 Java8 的 Optional 的 6 种操作
- 使用 Lambda 表达式实现超强的排序功能
- Java8 的时间库(1):介绍 Java8 中的时间类及常用 API
- Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换
- Java8 的时间库(3):开始使用 Java8 中的时间类
- Java8 的时间库(4):检查日期字符串是否合法
- Java8 的新特性
- Java9 的新特性
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常