背景
业务需要在运行时动态加载class文件,并注册到spring bean中。由于JDK的类加载会拿到Object实例,所以要用到强转类型。
加载例子
一般见网上的例子即可,本例使用a.class,定义一个cn.demo.MyDemo类,继承于IDemo接口。
public class MyDemo implements IDemo {
@Overided
public String show() {
System.out.println("YES");
}
}
在Main方法中,强转后使用show方法,无异常。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
String myPath = "file:///D:/a.class";
System.out.println(myPath);
byte[] cLassBytes = null;
Path path = null;
try {
path = Paths.get(new URI(myPath));
cLassBytes = Files.readAllBytes(path);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
return clazz;
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader loader = new MyClassLoader();
Class<?> aClass = loader.findClass("cn.demo.MyDemo");
try {
Object obj = aClass.newInstance();
IDemo demo = (IDemo) obj;
demo.show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在Spring 的Service层中,却提示ClassCastException。
...
MyClassLoader loader = new MyClassLoader();
Class<?> aClass = loader.findClass("cn.demo.MyDemo");
Object obj = aClass.newInstance();
// 此处报错
IDemo demo = (IDemo) obj;
// 注册到Bean中
...
排错过程
使用instanceof检测是否同一个类
boolean flag = obj instanceof IDemo //显示false
结果显示Spring下为false,main下为true
检测是否使用同一个类加载器
ClassLoader l1 = obj.getClassLoader();
ClassLoader l2 = IDemo.getClassLoader();
Spring下:
- l1为org.springframework.boot.devtools.restart.classloader.RestartClassLoader
- l2为sun.misc.Launcher$AppClassLoader
Main下:
- 都为sun.misc.Launcher$AppClassLoader
好了,总算知道问题在哪了,Spring的热部署惹的祸,把热部署去掉就能强转了。
要点
同一个类型判断:
- 必须包路径及名称一致
- 必须属于同一类加载器加载出来的,否则也是不同的
后记
若在Spring Boot中打包成Jar使用,类加载器会变成LaunchedURLClassLoader,这时更多会导致运行环境中出异常而开发环境无异常。
这时需要将自定义类加载器继承调整一下:
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
...
解释:使用URLClassLoader是因为它属于两种情况下共同的父类,而且借助构造函数传父类加载器,可以委托类加载器进行加载。