文章目录
一、自定义类加载器的应用场景
1.1 加密
Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以`防止反编译`,可以先将编译后的代码用某种加密算法加密,
类加密后就不能再用Java的ClassLoader去加载类了,这时就需要`自定义ClassLoader`在加载类的时候先解密类,然后再加载。
1.2 从非标准的来源加载代码
如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从`指定的来源`加载类
1.3 实际综合运用
比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。
这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
二、双亲委派模型
在实现自己的ClassLoader之前,我们先了解一下系统是如何加载类的,那么就不得不介绍双亲委派模型的实现过程。
/*
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,查看该类是否已经被加载
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
// 父类加载器无法完成类加载请求
}
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;
}
}
2.1 双亲委派模型的工作过程
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。
父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。
因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,
然后再调用当前加载器的findClass()方法进行加载。
2.2 双亲委派模型的好处
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
三、自定义类加载器
(1)从上面源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。
(2)因此我们自定义的类加载器只需要继承ClassLoader,并覆盖findClass方法,下面是一个实际例子,
在该例中我们用自定义的类加载器去加载我们事先准备好的class文件。
3.1 案例一(Class.forName)
3.1.1 自定义一个People.java类做例子
public class People {
//该类写在记事本里,在用javac命令行编译成class文件,放在d盘根目录下
private String name;
public People() {}
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "I am a people, my name is " + name;
}
}
3.1.2 自定义类加载器
自定义一个类加载器,`需要继承ClassLoader类,并实现findClass方法`。
其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class(只要二进制字节流的内容符合Class文件规范)。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader
{
public MyClassLoader()
{
}
public MyClassLoader(ClassLoader parent)
{
super(parent);
}
protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = new File("D:/People.class");
try{
byte[] bytes = getClassBytes(file);
// defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e)
{
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(File file) throws Exception
{
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true){
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
3.1.3 在主函数里使用
MyClassLoader mcl = new MyClassLoader();
Class<?> clazz = Class.forName("People", true, mcl);
Object obj = clazz.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader()); // 打印出我们的自定义类加载器
3.1.4 运行结果
3.2 案例二(loadClass)
自定义类加载器的一种实现方式:
public class CustomClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String cname = "/Users/wuzhenyu/Desktop/spring-boot/src/main/java/" + name.replace('.', '/') + ".class";
byte[] classBytes = Files.readAllBytes(Paths.get(cname));
Class<?> cl = defineClass(name, classBytes, 0, classBytes.length);
if (cl == null) {
throw new ClassNotFoundException(name);
}
return cl;
} catch (IOException e) {
System.out.print(e);
throw new ClassNotFoundException(name);
}
}
}
编译好的类文件,源文件代码如下:
public class SayHello {
public static void main(String[] args) {
System.out.print("Hello World");
}
}
测试类:
public class ClassLoaderTest {
public static void main(String[] args) {
try {
ClassLoader loader = new CustomClassLoader();
// 调用loadClass加载sample.loader.SayHello类
// 无法加载到该类,因此会调用findClass方法
Class<?> c = loader.loadClass("sample.loader.SayHello");
// 获取main方法
Method m = c.getMethod("main", String[].class);
// 执行main方法
m.invoke(null, (Object) new String[]{});
} catch (Throwable e) {
System.out.println(e);
}
}
}
运行结果:
Hello World
参考转载
https://blog.youkuaiyun.com/seu_calvin/article/details/52315125
https://www.jianshu.com/p/e2c7a1335e16
https://blog.youkuaiyun.com/xyang81/article/details/7292380