目的 为什么需要自定义类加载器,其需求是什么?或者说其应用场景是什么?
安全性:Java 代码能够被轻易的反编译,在足够优秀的 IDE 面前,class 文件等价于 java 文件,为了保证代码的安全性,可以将编译后的代码使用约定好的算法进行加密,这样的 class 文件时无法使用 Java 自有的类加载器读取的,此时就需要通过自定义加载器,在读取类时先解密后加载;
资源隔离:对于同一 jvm 来说,不同类加载器实例化的同名类对象,实际上是不相等,每一个类加载器都相当于一个独立的容器;
解决 jar 包冲突:基于上一点,同一个类名可以同时存在每一个类加载器中;
读取加密 class 文件 原理 自定义类加载器需要继承 ClassLoader 或其增强类 URLClassLoader,其中存在 findClass 方法,用于读取 class 文件,并通过搜索类的限定名对其进行加载,最终返回一个 Class 对象。 因此在读取 class 文件时,可以对其二进制字节进行解密,将其还原成原始的 class 文件,然后挂载到 jvm 中。
局限 通常情况下,只会使用 java 代码来实现自定义类加载器,不能加载其自身,即类加载器是可以作为反编译的切入点;同时,jvm 加载的类字节存在内存中,也是可以被访问的,狠一点的方法是通过其他语言来实现类加载器,但也不能完全避免反编译。
注意事项
单例模式:自定义类加载器在使用时,会加载指定类,每次实例化类加载器,每个对象之间同样是资源隔离的,因此,频繁调用会使得 jvm 中挂载的类越来越多,最终导致内存溢出,因此需要将自定义类加载器设计成单例模式,spring 项目中,注册成 bean 即可被 spring 托管;
代码实现 类加载器
继承自 URLClassLoader
,特点是可以指定外部 jar 或 class 文件;
主要重写 findClass
方法,用于读取 class 文件二级制字节并加载类;
需要在读取字节后对其解密;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class SimpleClassLoader extends URLClassLoader { public SimpleClassLoader (URL... urls) { super (urls); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { for (URL url: this .getURLs()) { byte [] bytes = decrypt(url) ; return this .defineClass(name, bytes, 0 , bytes.length); } return super .findClass(name); } private byte [] decrypt(URL url) { try { return SimpleEncryptUtils.decrypt(url.openStream()); } catch (IOException e) { System.out.println("【类文件解码】jar 或 class 不存在" ); return null ; } } public static void main (String[] args) { testPeople(); } private static void testPeople () { String target = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-1581006788342.class" ; test(target, "com.example.demo.bean.People" ); } }
异或加密
特点是简单,便于测试;
核心方法是 byte[] encrypt(byte[] bytes, String key)
,其他方法都是基于此方法的重载,便于使用;
main
方法中对 People.class
文件使用异或加密,并导出得到加密后的文件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 public class SimpleEncryptUtils { public static final String DEFAULT_KEY = "daiwenzh5♪(^∀^●)ノ" ; public static byte [] encrypt(byte [] bytes, String key) { byte [] keyBytes = key.getBytes(StandardCharsets.UTF_8); for (int i = 0 ; i < bytes.length; i++) { for (byte keyByte : keyBytes) { bytes[i] = (byte ) (bytes[i] ^ keyByte); } } return bytes; } public static byte [] encrypt(byte [] bytes) { return encrypt(bytes, DEFAULT_KEY); } public static byte [] encrypt(InputStream inputStream) { return encrypt(inputStream, DEFAULT_KEY); } public static byte [] encrypt(InputStream inputStream, String key) { try { BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); byte [] bytes = new byte [bufferedInputStream.available()]; bufferedInputStream.read(bytes); return encrypt(bytes, key); } catch (IOException e) { System.out.println(e.getMessage()); } return new byte [0 ]; } public static byte [] decrypt(byte [] bytes, String key) { return encrypt(bytes, key); } public static byte [] decrypt(byte [] bytes) { return decrypt(bytes, DEFAULT_KEY); } public static byte [] decrypt(InputStream inputStream) { return decrypt(inputStream, DEFAULT_KEY); } public static byte [] decrypt(InputStream inputStream, String key) { return encrypt(inputStream, key); } public static void encryptFile (String src, String dist) { try { Path path = Paths.get(dist); if (!Files.exists(path)) { Files.createFile(path); } Files.write(path, Objects.requireNonNull(encrypt(Files.newInputStream(Paths.get(src))))); } catch (IOException e) { e.printStackTrace(); } } public static void decryptFile (String src, String dist) { encryptFile(src, dist); } private static void testDecryptFile () { long time = System.currentTimeMillis(); String src = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-1581006788342.class" ; String dist = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-decrypt-" + time + ".class" ; System.out.printf("文件名:People-decrypt-%d.class%n" , time); decryptFile(src, dist); } private static void testEncryptFile () { long time = System.currentTimeMillis(); String src = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-origin.class" ; String dist = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-" + time + ".class" ; System.out.printf("文件名:People-encrypt-%d.class%n" , time); encryptFile(src, dist); } private static void testString (String content) { byte [] encrypt = encrypt(content.getBytes(StandardCharsets.UTF_8)); System.out.println(new String(encrypt, StandardCharsets.UTF_8)); System.out.println(new String(decrypt(encrypt), StandardCharsets.UTF_8)); } public static void main (String[] args) { testEncryptFile(); } }
class 文件
执行结果
加密 class 文件被成功读取,并通过反射正确输出对象类名;
如期地打印其使用的类加载器;
1 2 People com.example.demo.configs.SimpleClassLoader@1218025c