一、前言
最近在搭建SpringBoot的新应用,遇到个有意思的问题,如题就是在加载某一个类时候抛出了class is not visible from class loader
, 下面就带大家看看是如何产生的。
二、问题产生
- 首先有如下bean的定义:
public class TestProxy implements TestService {
private TestService testService;
public void init() throws Exception {
RemoteConsumerProxy proxy =
RemoteConsumerProxy()
.setInterfaceClass(TestService.class)
.build();
testService = proxy.getService();
}
。。。
}
如上代码代理类TestProxy继承了TestService类,并且在init方法里面消费了接口TestService的提供的远程服务。RemoteConsumerProxy类做了两件事,首先是生成接口TestService的远程服务bean这里假设为beanRemote,然后对beanRemote进行JDK代理生成代理类beanRemoteProxy,代理的作用是执行具体远程服务方法前进行统一限流处理或者指定调用的ip等。并且RemoteConsumerProxy是通过二方库方式引入。
然后引入了SpringBoot的开发工具模块spring-boot-devtools。
满足上面两个条件后注入TestProxy到IOC容器,运行Spring-boot工程的main函数(注意打成jar,然后运行jar则不会有这个问题),就会抛出:
TestService is not visible from class loader
从调用堆栈看是java.lang.reflect.Proxy的apply方法抛出的异常。
三、问题分析
既然是Proxy的apply方法抛出了异常,那么就看什么情况下会抛出异常,从Proxy的代码看是 interfaceClass != intf
时候抛出异常。
这里intf是通过 RemoteConsumerProxy
传递的,
而interfaceClass则是使用
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
创建的。
到这里对类加载器比较熟悉的童鞋应该会有所思了,同一个类两次加载后的Class对象不一样,那只有一种情况,那就是使用了两个类加载器加载了同一个类。
为了证明这个,可以在init方法里面添加如下代码:
System.out.println("TestProxy classloader:" + MassTopicQueryProxy.class.getClassLoader());
System.out.println("TestService classloader:" + MassTopicQueryService.class.getClassLoader());
System.out.println("RemoteConsumerProxy classloader:" + ACCSHSFConsumerProxy.class.getClassLoader());
运行后输出为:
TestProxy classloader:org.springframework.boot.devtools.restart.classloader.RestartClassLoader@63e66532
TestService classloader:org.springframework.boot.devtools.restart.classloader.RestartClassLoader@63e66532
RemoteConsumerProxy classloader:sun.misc.Launcher$AppClassLoader@4554617c
从结果可知TestProxy和TestService是使用RestartClassLoader类加载器加载的,所以调用代码
RemoteConsumerProxy.setInterfaceClass(TestService)
传递的时候传递的是RestartClassLoader加载的Class对象。从结果可知RemoteConsumerProxy.setInterfaceClass是AppClassLoader加载的,所以ACCSHSFConsumerProxy内部执行代码
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
时候也是使用AppClassLoader加载的,也就是这里的TestService是AppClassLoader加载的,所以同一个接口由两个类加载器加载,所以两个Class对象不相等。
另外通过debug可以发现RestartClassLoader的父加载器就是AppClassLoader。
那么RestartClassLoader又是什么那?从何而来?
经查阅资料的20.2 Automatic Restart章节可知,SpringBoot使用spring-boot-devtools模块实现当classpath下的文件被修改后自动重启的功能。这是通过使用两个类加载器来实现的,一些不需要的改变的类比如三方jar是使用base类加载器加载的(这里值AppClassloader),开发中一些需要修改的类则使用restart classloader进行重新加载。
总结:在IDE里面main函数方式运行时候由于会编译类,classpath下的内容会发生变化,所以会触发restart,从而导致抛出异常。而首先通过mvn clean package 打包,然后在java -jar jar方式由于jar内部不会变了所以不会触发restart,所以运行正常。
四、如何解决
方案一,排查掉spring-boot-devtools模块模块的maven引入可以解决,这时候所有类都是使用APPClassloader加载。
方案二,可以引入spring-boot-devtools模块,但是禁用禁用reStart功能
public static void main( String[] args )
{
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(Application.class, args);
}
五、总结
虽然是同一个类,但是使用不同的类加载器加载后得到的Class对象是不一样的,区分一个Class对象是否相等要看包名+类名,也要看是否是同一个类加载器。另外SpringBoot的spring-boot-devtools模块的restart功能在IDE里面运行main函数时候应该有bug。欢迎大家批评指正。
六、 参考
- https://docs.spring.io/sp
ring-boot/docs/current-SNAPSHOT/reference/html/using-boot-devtools.html
欢迎关注微信公众号: