记一次由spring-boot-devtools引发的类型转换问题


在我们在用Springboot开发的时候,可能会引入spring-boot-devtools包,因为这个开发工具包提供了很多很好的供开发时使用的功能,比如提供一些默认的配置,自动重启,远程应用等。但是今天我就碰到了一个与它相关的问题,困扰了我不少时间。

一 简介

spring-boot-devtoolsSpring 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系统属性。


文章作者: shiv
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shiv !
评论
  目录