Java自定义类加载器实现-原理分析
这篇文章主要聊一下如何自定义Java的类加载器,关于Java的类加载机制,可以参考Java的类加载机制双亲委派模型的文章:https://blog.csdn.net/strive_or_die/article/details/98664519
为什么要自定义
- 需要将我们的class文件放到自定义的
classpath
下,这时我们可以通过自己定义的类加载器实现加载指定目录下的class;其实这种情况能用到的情况并不多,因为我们可以通过java提供的指定加载目录实现我们需求。 - 某些类需要特殊的操作,例如该类的字节码数据加密过,这就需要做一些解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
- 热部署,例如Tomcat的jsp编译后的class。
怎么自定义
如果我们只是想实现自己的类加载器,但是没有打破双亲委派机制的,那么我们只需要继承自ClassLoader
即可,然后实现它的com.learn.classloader.custom.MyClassLoader#findClass
方法。为什么实现该方法即可呢?先来看一下loadClass(String name, boolean resolve)
的实现,这是出发类加载的入口:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果父加载器都没有加载到指定的类,则当前类执行findClass()尝试查找
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这个方法的基本步骤,就是先交给父加载器去加载类,如果父加载器都没有加载到指定的类,则当前类执行findClass()
尝试查找。因此我们只需要继承ClassLoader
,然后实现findClass()
方法即可。
实现自定义类加载器
首先定义了自己的类加载器如下:
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
private String rootDir;/*自定义类加载的查找class的路径*/
/*指定该类加载器会查找的rootDir目录,和父加载器*/
public MyClassLoader(String rootDir, ClassLoader parent){
super(parent);
this.rootDir = rootDir;
}
/*指定该类加载器会查找的rootDir目录*/
public MyClassLoader(String rootDir){
this.rootDir = rootDir;
}
/**
* 自定义自己的类加载器,如没有要改变类加载顺序的必要的话,则重写findClass方法,因为这个方法是JDK预留了给我们实现的,
* 否则就需要修改loadClass的实现。
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//<1>.根据类的全路径(包含包名)类名和放置的目录确定类文件的路径
String className = name.substring(name.lastIndexOf(".")+1)+ ".class";
String classFile = rootDir + File.separator + className;
FileInputStream fileInputStream = null;
byte[] classData = null;
try {
//<2>.将class文件读取到字节数组
fileInputStream = new FileInputStream(new File(classFile));
classData = new byte[fileInputStream.available()];
fileInputStream.read(classData,0,classData.length);
//<3>.将字节数据创建一个class
return defineClass(name,classData,0,classData.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//<4>如果父类加载器不是自定义的,上面的加载过程没加载成功,则此调用会throw ClassNotFoundException
return super.findClass(name);
}
}
定义一个用来测试的类Person
public class Person {
private String name = "Coco";
private int age = 23;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试类,rootDir
定义为了"D:/class/"
,所以MyClassLoader会从该目录下,加载指定的类。
public class CustomClassLoaderTest {
/*定义了一个目录存放class文件,这个其实可以修改为可配置参数*/
private static final String rootDir = "D:/class/";
public static void main(String[] args) throws Exception {
/*<1> 从指定的目录下查找对应的class文件,进行加载,然后创建该对象,如果加载存在则加载成功,则类加载器应为MyClassLoader*/
MyClassLoader classLoader = new MyClassLoader(rootDir);
Class c = classLoader.loadClass("com.learn.classloader.custom.Person");
Object object = c.newInstance();
Method getNameMethod = c.getMethod("getName");
Method getAgeMethod = c.getMethod("getAge");
System.out.println("name:" + getNameMethod.invoke(object) + ",age:" + getAgeMethod.invoke(object));
System.out.println("类加载器为:" + object.getClass().getClassLoader());
}
}
因为该Person
类,在IDE中编译后放在了classpath
,而classpath
默认是由ApplicationClassLoader
进行加载的,而MyClassLoader
的parent为ApplicationClassLoader
,所以如果在IDE中执行测试程序,根据双亲委派机制,Person
类的类加载器将一直是ApplicationClassLoader
,下图是运行的结果:
显然,直接在IDE中执行测试类是没有办法使用我们自定义的类加载器实现类加载的,这一切的原因就是Person
类在classpath
中,所以解决方法就是将编译后的Person类,放到"D:/class/"
目录下,这个是例子中定义的目录,根据具体情况自己指定。如下图所示:
接下来,我们先将项目打包成Jar包,此时Jar中含有编译后的Person
类,然后需要将该类从中删除掉,然后再运行Jar程序,如果不删除jar包中的Peson
类,则会仍会是ApplicationClassLoader
,如下所示:
下面删除jar包中的Person
类,那么就会从"D:/class"
里进行加载,也就是自定义的类加载器去加载的,如下所示,显然类加载器已经是MyClassLoader
了:
总结
在不打破双亲委托机制的前提下,自定义ClassLoader
主要是实现了findClass()
方法,这里主要是通过实例演示了自定义类加载的实现和原理。具体如何查找自己定义的类,例如如果是jar则读取jar包中的类,可以根据自己的实际需求自己实现。