自定义Classloader导致ClassCastException
背景
java.lang.ClassCastException: cn.com.nightfield.Plugin cannot be cast to cn.com.nightfield.Plugin
相同的class
,竟然不能cast?这是什么鬼?
问题描述
自定义类加载器(Classloader
)是很常见的,它可以让我们从自定义的文件系统目录,网络甚至是数据库的各种文件类型(jar
, war
, zip
等)中加载class
文件。 我们项目中使用了一个开源的类管理工具PF4J,来加载指定目录下的class
文件。但奇怪的是,当我们把class
加载进来之后,将它强转为目标类型,却报了java.lang.ClassCastException
,两者明明是同一个class
!
问题分析
先说明,错误是跟自定义类加载器有关。上一个小demo来模拟一下上述错误:
package cn.com.nightfield.jvm.classloader;
// 在class path下定义一个类
public class Plugin {}
package cn.com.nightfield.jvm.classloader;
import java.net.URL;
import java.net.URLClassLoader;
// 自定义一个类加载器
public class CustomizedClassLoader extends URLClassLoader {
public CustomizedClassLoader(URL[] urls) {
super(urls);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 如果不是自定义目录下的class,统一委托给AppClassloader去加载
if (!name.startsWith("cn.com.nightfield.jvm.classloader")) {
return super.loadClass(name, resolve);
}
// 如果是自定义目录下的class,直接加载,此处违反了双亲委派模型
else {
Class<?> c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
}
package cn.com.nightfield.jvm.classloader;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException {
// 指定类加载器的加载路径
URL url = new File("/Users/zhochi/demo/target/classes").toURI().toURL();
ClassLoader customizedClassLoader = new CustomizedClassLoader(new URL[]{url});
// 用自定义类加载器加载Plugin class
Class clz = customizedClassLoader.loadClass("cn.com.nightfield.jvm.classloader.Plugin");
System.out.println(clz.getClassLoader());
Object pluginInstance = clz.newInstance();
// pluginInstance instanceof Plugin”输出false
System.out.println("pluginInstance instanceof Plugin: " + (pluginInstance instanceof Plugin));
// 报java.lang.ClassCastException错误
Plugin plugin = (Plugin) clz.newInstance();
}
}
控制台输出如下:
cn.com.nightfield.jvm.classloader.CustomizedClassLoader@60e53b93
pluginInstance instanceof Plugin: false
Exception in thread "main" java.lang.ClassCastException: cn.com.nightfield.jvm.classloader.Plugin cannot be cast to cn.com.nightfield.jvm.classloader.Plugin
at cn.com.nightfield.jvm.classloader.ClassLoaderTest.main(ClassLoaderTest.java:19)
要想知道错误的根源,需要了解对象可以被cast的前提:对象必须是目标类的实例。从上述输出也可以看到,instance instanceof Plugin
的结果是false
,为什么呢?因为对于任意一个类,都需要由它的类加载器和这个类本身,共同确立其在JVM
中的唯一性,也就是说,JVM
中两个类是否相等,首先要看它们是不是由同一个类加载器加载的。如果不是的话,即使这两个类来自于同一个class
文件,它们也不相等。
上例中,Plugin
类处于class path
下,默认是由AppClassloader
来加载的;但是pluginInstance
却是由CustomizedClassLoader
加载出来的class
的实例。JVM
尝试将CustomizedClassLoader.Plugin
转成AppClassloader.Plugin
,必然会报错。
问题解决
其实究其原因,是我们在自定义类加载器CustomizedClassLoader
中,违反了双亲委派模型。 我们都知道,Java
中有三大类加载器:BootstrapClassLoader
,ExtClassLoader
和AppClassLoader
,它们在组合上构成父子关系,前者是后者的"父亲",并且有各自的“领地”:BootstrapClassLoader
负责加载 Java
核心类库如JRE
中的rt.jar
,resource.jar
;ExtClassLoader
负责加载{java.home}/lib/ext
和java.ext.dirs
系统目录下的class
;AppClassLoader
则是加载class path
路径下,也就是我们自己写的class
文件。 所谓双亲委派模型,指的是当Classloader
收到一个加载class
请求的时候,首先会委托给其父亲去加载,如果父亲加载不成功,自己才会尝试去加载。双亲委派的机制是JVM
中类的安全性的一大保障:就算有人恶意自定义了一个String.class
,最终由类加载器加载到的依然是rt.jar
中的String
。以下是loadClass
的部分源码:
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 如果类已经被加载过了,直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父类去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 这种情况指的就是委托BootstrapClassLoader去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 3. 尝试自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
不过,双亲委派模型并不是一个强制的约束,而是Java
推荐的模式,所以我们在自定义类加载器的时候,推荐重写findClass()
方法,而不是loadClass()
方法。
回到最开始的问题,分析了一下PF4J
的源码,可以猜到,它也定义了自己的类加载器PluginClassLoader,且它重写的loadClass()
方法的默认实现,为了防止class
的版本问题,违反了双亲委派模型。
总结
Java
中的类加载器,相当于是其加载的class
的命名空间,两个类相等,首先要保证它们是由同一个类加载器加载的。 在实现自定义类加载器的时候,除非你对类加载机制有着深刻的认知且知道自己在做什么,否则不要违反双亲委派模型。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
obj-c中NSString的常用方法
#import <Foundation/Foundation.h> /** NSString中常用的类方法 + (nullable instancetype)stringWithUTF8String:(const char *)nullTerminatedCString; instancetypetype 作为返回值 代表返回的是当前这个类的对象 作用:将C语言的字符串转化为OC字符串对象 + (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); 作用:拼接一个字符串对象 NSString中常用的对象方法 1). length 方法 返回值 NSUInteger ,得到字符串的字符的个数,可以处理中文 2). characterAtIndex 得到字符串中指定下标的字符 - (unichar)characterAtIndex:(NSUInteger)index; 3). 判断两个字符串是否相等 - (BOOL)isEqualToString:(NSString *)aS...
-
下一篇
BeeCP 2.5.3 发布,一款高性能 JDBC 连接池
1:基本介绍 小蜜蜂连接池是一款高性能的JDBC连接池,下面是与主流连接池的性能测试对比图 测试机器:CPU: I3-7100,内存: 8G,操作系统:Win7_64 性能测试包: 地址1:https://github.com/Chris2018998/BeeCP/blob/master/doc/other/HikariCP-benchmark_SafeClose.zip 地址2: https://gitee.com/mirrors/BeeCP/blob/master/doc/other/HikariCP-benchmark_SafeClose.zip 2:版本内容(BeeCP-2.5.3) 1:调整DataSource取连接写锁代码位置 2:优化PreparedStatement缓存(采用LinkedHashMap作为LRU缓存) 3:优化getConnection处代码,降低代码嵌套层数(性能少量提升.) 3:版本下载 <dependency> <groupId>com.github.chris2018998</groupId> <...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Red5直播服务器,属于Java语言的直播服务器
- MySQL数据库在高并发下的优化方案
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Dcoker安装(在线仓库),最新的服务器搭配容器使用