在我们在用Springboot开发的时候,可能会引入spring-boot-devtools包,因为这个开发工具包提供了很多很好的供开发时使用的功能,比如提供一些默认的配置,自动重启,远程应用等。但是今天我就碰到了一个与它相关的问题,困扰了我不少时间。
一 简介
spring-boot-devtools
为Spring Boot
包含的一组额外的工具,可以使应用程序开发体验更加愉快。该spring-boot-devtools
模块可以包含在任何项目中,以提供额外的开发期间功能。Spring Boot
为了使开发者在开发时针对文件的修改能快速的生效,在spring-boot-devtools
中就提供了一个自动重启的功能,只要类路径上的文件发生更改,就会自动重新启动应用程序。
Spring Boot提供的重启技术使用两个类加载器。不更改的类(例如,来自第三方jar的类)将加载到基类加载器中。您正在积极开发的类将加载到RestartClassLoader加载器中。重新启动应用程序时,RestartClassLoader加载器会被丢弃并重新创建一个。这种方法意味着应用程序重新启动通常比“冷启动”快得多,因为基本类加载器加载的类已经可用。
二 问题
我在使用dozer来进行bean的映射的时候,报出了一个ClassCastException
异常。代码如下
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(new BeanMappingBuilder() {
@Override
protected void configure() {
mapping(type(ZxCreditReportDTO.class), type(CreditInfoDetailDTO.class)
).fields(field("username"), field("name"));
}
}).build();
CreditInfoDetailDTO map = mapper.map(creditReportDTO, CreditInfoDetailDTO.class);
首先这段代码在Main函数或者在单元测试类运行良好,但是当我启动springboot通过http接口去调用这段代码却出现如下错误:
这里奇怪的就是居然出现类同一个类CreditInfoDetailDTO
转换的异常,相同的类为什么会出现类型转换异常呢?于是就出现两个疑问,一,为什么用Main函数或者Junit单元测试没有出现问题?二,什么同一个类会出现类型转换异常?
三 原因
其实原因就是启动Springboot调接口与Main函数测试使用的类加载器是有差别的。使用Main函数的时候CreditInfoDetailDTO
类都是由AppClassLoader加载器加载的。而springboot由于添加了spring-boot-devtools依赖,会有所不同。上面有这么一段代码:
mapping(type(ZxCreditReportDTO.class), type(CreditInfoDetailDTO.class)
).fields(field("username"), field("name"));
进入这段代码内部,会调用MappingUtils的loadClass(String name, BeanContainer beanContainer)去加载指定的mapping类。
代码如下:
public static Class<?> loadClass(String name, BeanContainer beanContainer) {
DozerClassLoader loader = beanContainer.getClassLoader();
return loader.loadClass(name);
}
继续查看beanContainer.getClassLoader()
public class BeanContainer {
// 这一段
DozerClassLoader classLoader = new DefaultClassLoader(getClass().getClassLoader());
public DozerClassLoader getClassLoader() {
return classLoader;
}
...
这里Dozer默认使用的加载器为DefaultClassLoader,而DefaultClassLoader内部有一个ClassLoader属性,这个属性值就是上面的getClass().getClassLoader()
。由此可以知道加载CreditInfoDetailDTO的加载器与加载BeanContainer类的加载器是同一个都是AppClassLoader,由于spring-boot-devtools的作用,CreditInfoDetailDTO map = mapper.map(creditReportDTO, CreditInfoDetailDTO.class);
左侧的CreditInfoDetailDTO是由RestartClassLoader加载的,所以是由于不同的类加载器加载从而导致类型转换异常。
四 解决方案
1 不使用spring-boot-devtools
2 使用spring-devtools.properties
在reources目录下新建META-INF文件夹,再在META-INF下新建spring-devtools.properties,如下:
# 支持正则
restart.include.dozer=/dozer-[\\w\\d-\.]+\.jar
更多配置信息参考官方文档
这个配置就是dozer-[\w\d-.]+.jar里的类也使用RestartClassLoader加载器加载,这样就不会出现类型转换问题了。
注意:当然其实在生产环境我们是不建议使用spring-boot-devtools的。在运行完全打包的应用程序时会自动禁用,如果您的应用程序是从
java -jar
特殊的类加载器启动或启动的,那么它将被视为“生产应用程序”。如果这不适用(即,如果您从容器运行应用程序),请考虑排除devtools或设置-Dspring.devtools.restart.enabled=false
系统属性。