上期我们讲了类加载子系统,这次我们来讲一下自定义类加载器,首先我们要明白,为啥要自定义类加载器,用JVM默认的类加载器一样能进行类加载啊,带着这个问题,我们进入接下来的环节。
为什么要自定义类加载器
我们围绕着以下场景来解答这个问题:
1.JVM提供的类加载器只加载特定的路径下的类,当我们自己写的类,或者第三方Jar包,为了实现某种业务需求,那我们只有自定义类加载器来实现我们的特殊业务需求。
2.当我们引入第三方Jar包时,存在与Java的类库具有相同的全类名时,比如引入某个工具类Jar包,它里面有个包名叫java.lang,在这个包下面有个String类,这个类的全类名与Java自带的String的全类名冲突了,我们用系统类加载器去加载,就会产生冲突,使程序崩溃,所以为了避免类的重复加载我们自定义类加载器就能很好的解决这个类冲突的问题。
3.为了安全起见,比如某个恶意程序里面也与Java核心类库的全类名相同,然后加载的时候就很容易出现加载了恶意程序,从而导致系统核心API被篡改,出现严重的安全事故,所有为了安全起见,我们需要自定义类加载器。
4.当我们写好的系统被别人反编译,然后窃取我们的源码,为了解决这个问题,我们也要自定义类加载器对源码进行加密。
总结:避免类的重复加载,隔离加载类,扩展加载源,防止源码泄露。
什么是双亲委派机制
举一个例子。比如小明在某家公司工作,然后随着用户量的增加,公司的服务器达到瓶颈了,需要加物理机扩展,然后小明就向上级反映要花钱购买服务器,然后部门经理收到情况后说:要公司支出几百万这个事情我还不能做主,还得向总经理反映,等情况到老板这了(老板没有上级了),然后老板说:我给你个证明,去财务那拿钱解决这个问题就可以了。
以上这个例子虽然不是很恰当,但是差不多了,我们就可以把老板比作引导类加载器(Bootstrap ClassLoader),总经理比作扩展类加载器(ExtClassLoader),部门经理比作应用程序类加载器(AppClassLoader)。
当我们进行类加载的时候,首先会进行判断,加载这个类的类加载器它有没有父加载器,有的话它会继续向上递归判断,直到父加载器为null的时候,才会停止,然后就进行判断,当前类加载能不能加载我这个类,能的话就由最顶层的类加载器加载,不能的话向下减一层,判断这一层的类加载器是否能加载此类,如果能则加载,不能则向下减一层,然后继续判断,最终能找到某一层的能加载就加载,然后停止
双亲委派机制的好处就是可以有效避免类的重复加载,还能够防止Java核心类库被篡改。
判断两个对象是否为同一个类的必要条件就是:
1.全类名必须一致。
2.加载这个类的类加载器(ClassLoader)必须为同一个(类加载器的实例要相同)。
在JVM中,如果这两个对象来源于同一个类,被同一个虚拟机加载,但是只要加载他们的类加载器的实例不相同,那么这两个对象也不相同。
JVM必须要知道一个类型是由引导类加载器加载的还是由用户自定义类加载器加载的,如果是由用户自定义类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中,当解析一个类型到另一个类型的引用时,JVM要保证这两个类型的类加载器是相同的。
如何自定义类加载器
前面讲类加载子系统的时候我们说过,类加载器分为两种,一种是引导类加载器(Bootstrap ClassLoader),另一种是自定义类加载器(继承ClassLoader的类),所以,我们要自定义类加载器,就去继承ClassLoader类,或者继承它的子类即可,上面我们还讲了自定义类加载器加载类的时候对字节码文件进行加密,防止被别反编译窃取源码。示例代码如下:
package com.tl666.jvm.classload;
import java.io.*;
import java.util.Base64;
/**
* 如果要实现字节码加密就继承ClassLoader类 ,重写findClass方法然后对二进制流加密
* 如果不要加密就继承URLClassLoader类
*/
public class MyClassLoader extends ClassLoader {
public MyClassLoader(ClassLoader parent) {
//设置AppClassLoader加载器为空,因为双亲委派关系,父加载器为空,则会使用子加载器,但是引导类加载器除外
super(parent);
}
/**
* 重写ClassLoader的findClass方法
*
* @param name 类的二进制名
* @return Class对象
* @throws ClassNotFoundException 没找到类则抛出异常
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = getDecryptionBytes(name);
try {
if (bytes.length == 0)
throw new FileNotFoundException();
Class<?> aClass = defineClass(name, bytes, 0, bytes.length);
if (aClass != null)
return aClass;
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
/**
* 获取解密后的字节码数组
*
* @param name 类的二进制名
* @return 解密后的字节码数组
*/
private byte[] getDecryptionBytes(String name) {
byte[] bytes = getEncryptionBytes(name);
String str = new String(bytes);
if (str.endsWith("TLBABY")) {
//将文件末尾的唯一标识去掉
bytes = str.replace("TLBABY", "").getBytes();
}
//通过Base64解密
bytes = Base64.getDecoder().decode(bytes);
return bytes;
}
/**
* 加载类之前把字节码文件加密
*
* @param name 类的二进制名
* @return 加密后的字节码数组
*/
private byte[] getEncryptionBytes(String name) {
//此处为本机电脑字节码的绝对路径
String fname = "E:/IdeaTL/review/out/production/JVM-Demo/" + name.replaceAll("[.]", "/") + ".class";
File file = new File(fname);
byte[] buff = new byte[(int) file.length()];
byte[] encode = null;
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
PrintStream printStream = null;
try {
fileInputStream = new FileInputStream(file);
fileInputStream.read(buff);
String str = new String(buff);
if (str.endsWith("TLBABY")) { //末尾有该标识则代表已经加密,直返回即可
return buff;
}
//通过Base64加密
encode = Base64.getEncoder().encode(buff);
String str2 = new String(encode);
StringBuilder stringBuilder = new StringBuilder(str2);
stringBuilder.append("TLBABY"); //在加密文件末尾添加标识
encode = stringBuilder.toString().getBytes();
//将加密后的字节码写入源文件中
fileOutputStream = new FileOutputStream(file);
printStream = new PrintStream(fileOutputStream);
printStream.write(encode, 0, encode.length);
printStream.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (printStream != null) {
printStream.close();
}
}
return encode;
}
}
测试代码:
package com.tl666.jvm.classload;
import java.lang.reflect.Constructor;
/**
* 自定义类加载器测试类
*/
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader(null);
Class<?> aClass = myClassLoader.loadClass("com.tl666.jvm.classload.MyDog");
System.out.println(aClass.getClassLoader());
Constructor<?> constructor = aClass.getConstructor();
constructor.setAccessible(true);//让编译阶段不要检验权限修饰符:如 private,protected
constructor.newInstance();
}
}
class MyDog {
static {
System.out.println("我被" + MyDog.class.getClassLoader() + "类加载器加载了");
}
public MyDog() {
}
}
控制台输出:
加密前的class文件:
我们反编译可以得到如下图所示:
通过自定义类加载器加载后:
此时再去反编译:
可以看到我们就成功的实现了自定义类加载器,并且还把class文件加密了。
这里在讲一下继承ClassLoader类要重写哪些方法?
可以看到ClassLoader类的findClass方法不能直接用,必须重写,不然就报异常,
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这个方法的作用是拿到要加载类的二进制字节码,然后再通过调用ClassLoader类的defineClass()方法实现类加载,然后在ClassLoader内部的defineClass()方法里面调用了java源生的方法。
好了本期的自定义类加载器就到这里了,下期咱们继续讲运行时数据区里面相关的内容。