ClassLoder 介绍
ClassLoader 的主要作用是 Java 类文件的加载。默认的类加载器是 AppClassLoader 应用程序类加载器(加载用户类路径 classpath 下的类库)。
【除此之外还有 Bootstrap ClassLoader 启动类加载器(将 JDK\jre\lib 目录下的类库加载到虚拟机内存,用来加载 java 核心库,并不继承自 java.lang.ClassLoader ,是虚拟机自身的一部分,无法被 java 程序直接引用);以及 Extension ClassLoader 扩展类加载器(将 JDK\jre\lib\ext 下的类库加载到虚拟机内存,即加载 java 的扩展库)。】
java 程序在运行时需要先编译成 class 文件,JVM 在执行 java 类之前会先解析 class 的二进制内容,JVM 执行的其实是 javap 命令生成的字节码 (ByteCode)。(可用程序生成 class 文件的字节码) java 类在进行初始化时会调用 java.lang.ClassLoder 加载类字节码。
ClassLoader 类的核心方法:
loadClass - 加载指定的 java 类
findClass - 查找指定的 java 类
findLoadedClass - 查找 JVM 已经加载过的 java 类
defineClass - 定义一个 java 类
resolveClass - 链接指定的 java 类
java 类加载可分为 显式 及 隐式 ,显式即通过 java 反射 或 ClassLoader 来动态加载一个类对象,隐式则是指通过 类名.方法名() 或 new 类实例 时进行 java 类加载。
java 反射:
Class.forName("org.example.App"); // 默认会加载类的静态属性及方法(不想初始化类则:Class.forName("类名", 是否初始化, 类加载器))
ClassLoder 加载:
this.getClass().getClassLoader().loadClass("org.example.App"); // 默认不会加载静态属性及方法
通过 ClassLoader 的 loadClass("org.example.App") 加载流程:首先通过 findLoadedClass 找该类是否已经初始化,如已经初始化则直接返回对象,若没有则依次向上通过父类加载器进行加载(如果配置了的话,没配置就使用 JVM 的 Bootstrap ClassLoader 进行加载)。如果Bootstrap ClassLoader 没法加载再由父类加载器依次向下加载,如果还没加载到,则调用 findClass 方法尝试加载,在 ClassLoader 中 findClass 会抛出 ClassNotFoundException 异常,所以当前 ClassLoader 如果没有重写 findClass 方法那么就会抛出异常退出,重写了则通过传入的类名找到对应的类字节码,接着通过 defineClass 将该类注册到 JVM 中。如果调用 loadClass 方法时传入的 resolve 参数为 true ,那么还需要调用 resolveClass 方法去链接类,默认为 false 。再然后就返回一个被 JVM 加载后的类了。
实现使用 ClassLoader 来加载我们的自定义类
根据前面的基础装备配置,我们就可以逐步实现使用 ClassLoader 来加载我们的自定义类啦。
前面有提到如果我们自定义类不存在于 classpath 中,ClassLoader 加载时会抛出异常,那么我们可以选择自定义一个类加载器重写 findClass 方法,也可以找一个重写了 findClass 方法的 ClassLoader 子类。再调用 defineClass 方法将自定义类注册进去,接着就可以通过反射调用类方法了。
01、自定义类加载器
我们看一下自定义一个类加载器实现此效果:
`package org.example;`` ``import java.lang.reflect.Constructor;`` ``public class LoadSelfClass extends ClassLoader {` `private static String className = "org.example.App";`` ` `private byte[] classByteCodes = new byte[]{` `-54,-2,-70,-66,0,0,0,51,0,42,10,0,10,0,24,9,0,25,0,26,8,0,27,10,0,28,0,29,8,0,30,8,0,31,9,0,9,0,32,8,0,33,7,0,34,7,0,35,1,0,4,110,97,109,101,1,0,18,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101,84,97,98,108,101,1,0,4,116,104,105,115,1,0,17,76,111,114,103,47,101,120,97,109,112,108,101,47,65,112,112,59,1,0,3,115,97,121,1,0,8,60,99,108,105,110,105,116,62,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,8,65,112,112,46,106,97,118,97,12,0,13,0,14,7,0,36,12,0,37,0,38,1,0,36,-17,-65,-67,-17,-65,-67,-17,-65,-67,-20,-73,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,7,0,39,12,0,40,0,41,1,0,2,72,105,1,0,10,109,101,100,105,48,99,114,49,116,121,12,0,11,0,12,1,0,22,115,116,97,116,105,99,32,109,101,116,104,111,100,32,105,110,118,111,107,101,100,33,1,0,15,111,114,103,47,101,120,97,109,112,108,101,47,65,112,112,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,16,106,97,118,97,47,108,97,110,103,47,83,121,115,116,101,109,1,0,3,111,117,116,1,0,21,76,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,59,1,0,19,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,1,0,7,112,114,105,110,116,108,110,1,0,21,40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,86,0,33,0,9,0,10,0,0,0,1,0,8,0,11,0,12,0,0,0,3,0,0,0,13,0,14,0,1,0,15,0,0,0,63,0,2,0,1,0,0,0,13,42,-73,0,1,-78,0,2,18,3,-74,0,4,-79,0,0,0,2,0,16,0,0,0,14,0,3,0,0,0,11,0,4,0,12,0,12,0,13,0,17,0,0,0,12,0,1,0,0,0,13,0,18,0,19,0,0,0,1,0,20,0,14,0,1,0,15,0,0,0,55,0,2,0,1,0,0,0,9,-78,0,2,18,5,-74,0,4,-79,0,0,0,2,0,16,0,0,0,10,0,2,0,0,0,27,0,8,0,28,0,17,0,0,0,12,0,1,0,0,0,9,0,18,0,19,0,0,0,8,0,21,0,14,0,1,0,15,0,0,0,59,0,2,0,0,0,0,0,23,18,6,-77,0,7,-78,0,2,-78,0,7,-74,0,4,-78,0,2,18,8,-74,0,4,-79,0,0,0,1,0,16,0,0,0,18,0,4,0,0,0,15,0,5,0,18,0,14,0,19,0,22,0,20,0,1,0,22,0,0,0,2,0,23` `};`` ` `@Override` `protected Class<?> findClass(String name) throws ClassNotFoundException {` `if (name.equals(className)){` `return defineClass(name, classByteCodes, 0, classByteCodes.length);` `}` `return super.findClass(name);` `}`` ` `public static void main(String[] args) {` `LoadSelfClass loadSelfClass = new LoadSelfClass();` `try {` `Class cls = loadSelfClass.loadClass(className); // 通过自定义的 ClassLoader 加载自定义类` `cls.newInstance();``// Constructor constructor = cls.getDeclaredConstructor();``// constructor.setAccessible(true);``// Object clsInstance = constructor.newInstance();``// cls.getMethod("say").invoke(clsInstance);` `} catch (Exception e) {` `e.printStackTrace();` `}` `}``}`
应该一看就懂,没什么好讲的【主要就是重写了 findClass 方法,在方法中通过 defineClass 注册到 JVM 。哦,还有不能将 org\example\App.class 文件放在类路径下,不然会由 AppClassLoader 来进行加载( loadClass 时会先由父加载器加载),不会由我们自定义的加载器来加载 】。
运行结果:
生成类字节码的代码:
`package org.example;`` ``import java.io.*;`` ``public class JavaByteCode {`` ` `public static void main(String[] args) {` `byte[] bs = getBytesByFile("D:\\JavaWorkspace\\JavaBasic\\target\\classes\\org\\example\\App.class"); // class 文件位置` `for(int i = 0; i < bs.length; i++){` `System.out.print(bs[i] + ",");` `}` `}` `private static byte[] getBytesByFile(String pathStr) {` `File file = new File(pathStr);` `try {` `FileInputStream fis = new FileInputStream(file);` `ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);` `byte[] b = new byte[1000];` `int n;` `while ((n = fis.read(b)) != -1) {` `bos.write(b, 0, n);` `}` `fis.close();` `byte[] data = bos.toByteArray();` `bos.close();` `return data;` `} catch (FileNotFoundException e) {` `e.printStackTrace();` `} catch (IOException e) {` `e.printStackTrace();` `}` `return null;` `}``}`
02、URLClassLoader
另一种方式,找到一个实现了 findClass 方法的子类 - URLClassLoader 。
核心代码:
`URL[] urls = {new URL("http://localhost:8000/")};``URLClassLoader ucl = new URLClassLoader(urls);``Class c = ucl.loadClass("Hello"); // Hello.class 放在 url 路径下``c.newInstance();`
03、defineClass
转念一想,ClassLoader 动态加载 loadClass 过程中是不是最重要的就是 defineClass 方法,由他注册进 JVM 。那我们是不是可以直接反射出 defineClass 方法将字节码传入注册进去呢?
`package org.example;`` ``import java.lang.reflect.Method;``import java.util.Base64;`` ``public class DefineClass {` `public static void main(String[] args) {` `try {` `Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);` `defineClass.setAccessible(true);``// byte[] code = Base64.getDecoder().decode("yv66vgAAADQAKgoACgAYCQAZABoIABsKABwAHQgAHggAHwkACQAgCAAhBwAiBwAjAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQARTG9yZy9leGFtcGxlL0FwcDsBAANzYXkBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAhBcHAuamF2YQwADQAOBwAkDAAlACYBACTvv73vv73vv73st73vv73vv73vv73vv73vv73vv73vv73vv70HACcMACgAKQEAAkhpAQAKbWVkaTBjcjF0eQwACwAMAQAWc3RhdGljIG1ldGhvZCBpbnZva2VkIQEAD29yZy9leGFtcGxlL0FwcAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAJAAoAAAABAAgACwAMAAAAAwAAAA0ADgABAA8AAAA/AAIAAQAAAA0qtwABsgACEgO2AASxAAAAAgAQAAAADgADAAAACwAEAAwADAANABEAAAAMAAEAAAANABIAEwAAAAEAFAAOAAEADwAAADcAAgABAAAACbIAAhIFtgAEsQAAAAIAEAAAAAoAAgAAABsACAAcABEAAAAMAAEAAAAJABIAEwAAAAgAFQAOAAEADwAAADsAAgAAAAAAFxIGswAHsgACsgAHtgAEsgACEgi2AASxAAAAAQAQAAAAEgAEAAAADwAFABIADgATABYAFAABABYAAAACABc=");` `byte[] code = new byte[]{` `-54,-2,-70,-66,0,0,0,51,0,42,10,0,10,0,24,9,0,25,0,26,8,0,27,10,0,28,0,29,8,0,30,8,0,31,9,0,9,0,32,8,0,33,7,0,34,7,0,35,1,0,4,110,97,109,101,1,0,18,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101,84,97,98,108,101,1,0,4,116,104,105,115,1,0,17,76,111,114,103,47,101,120,97,109,112,108,101,47,65,112,112,59,1,0,3,115,97,121,1,0,8,60,99,108,105,110,105,116,62,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,8,65,112,112,46,106,97,118,97,12,0,13,0,14,7,0,36,12,0,37,0,38,1,0,36,-17,-65,-67,-17,-65,-67,-17,-65,-67,-20,-73,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,-17,-65,-67,7,0,39,12,0,40,0,41,1,0,2,72,105,1,0,10,109,101,100,105,48,99,114,49,116,121,12,0,11,0,12,1,0,22,115,116,97,116,105,99,32,109,101,116,104,111,100,32,105,110,118,111,107,101,100,33,1,0,15,111,114,103,47,101,120,97,109,112,108,101,47,65,112,112,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,16,106,97,118,97,47,108,97,110,103,47,83,121,115,116,101,109,1,0,3,111,117,116,1,0,21,76,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,59,1,0,19,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,1,0,7,112,114,105,110,116,108,110,1,0,21,40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,86,0,33,0,9,0,10,0,0,0,1,0,8,0,11,0,12,0,0,0,3,0,0,0,13,0,14,0,1,0,15,0,0,0,63,0,2,0,1,0,0,0,13,42,-73,0,1,-78,0,2,18,3,-74,0,4,-79,0,0,0,2,0,16,0,0,0,14,0,3,0,0,0,11,0,4,0,12,0,12,0,13,0,17,0,0,0,12,0,1,0,0,0,13,0,18,0,19,0,0,0,1,0,20,0,14,0,1,0,15,0,0,0,55,0,2,0,1,0,0,0,9,-78,0,2,18,5,-74,0,4,-79,0,0,0,2,0,16,0,0,0,10,0,2,0,0,0,27,0,8,0,28,0,17,0,0,0,12,0,1,0,0,0,9,0,18,0,19,0,0,0,8,0,21,0,14,0,1,0,15,0,0,0,59,0,2,0,0,0,0,0,23,18,6,-77,0,7,-78,0,2,-78,0,7,-74,0,4,-78,0,2,18,8,-74,0,4,-79,0,0,0,1,0,16,0,0,0,18,0,4,0,0,0,15,0,5,0,18,0,14,0,19,0,22,0,20,0,1,0,22,0,0,0,2,0,23` `};` `Class app = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "org.example.App", code, 0, code.length);` `app.newInstance();` `} catch (Exception e) {` `e.printStackTrace();` `}` `}``}`
反射相关知识:Java 反序列化 - commons collection 之困(一)。
类加载的生命周期 - 初始化
还想补充一个小点,类加载与初始化。
我们前面提到的 ClassLoader 加载字节码其实只是将类加载到内存,在上图 java 类加载生命周期中完成加载和连接之后,是否需要初始化则需要根据具体场景进行判断。
初始化是做什么呢?主要为类的静态变量赋予正确的初始值,也会执行静态代码块。在初始化之前连接阶段中准备 Preparation 阶段主要为类的静态变量分配内存,并将初始化为默认值。
JVM 规范中有且仅有以下 5 种情况会完成类的初始化(也在加载、连接之后)。
遇到 new 、getstatic 、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化;
使用关键字 new 实例化对象时
读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)时
调用一个类的静态方法的时候
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
当虚拟机启动的时候,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个类
当使用 JDK1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
以下几种情况不会触发类进行初始化:
引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化;
定义类数组,不会引起类的初始化;
引用类的常量(被 static final 修饰的),不会引起类的初始化,已在准备阶段就将其存入调用类的常量池。
`package org.example;`` ``class InitClass{` `static {` `System.out.println("初始化InitClass");` `}` `public static String a = null;` `public final static String b = "b";` `public static void method(){}``}`` ``class SubInitClass extends InitClass{` `static {` `System.out.println("初始化SubInitClass");` `}``}`` ``public class Init {` `public static void main(String[] args) throws Exception{` `String a = SubInitClass.a; // 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化` `String b = InitClass.b; // 使用类的常量不会引起类的初始化` `SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化` `}``}`
为什么想分享初始化呢,因为这个也是我一直没怎么弄清楚的地方吧。
参考链接
[1] 攻击 Java Web 应用 - ClassLoader(类加载机制)
[2] Java 安全漫谈 - java 中动态加载字节码的那些方法