类的动态加载

xiao1star2025-03-14文章来源:SecHub网络安全社区


类加载

类加载的基本流程

202501181940765.png

java在类初始化的时候会调用静态代码块,而其他的像构造方法等需要在实例化也就是使用的时候才会被调用

User类

有构造方法、静态代码块、静态方法

public class User { public String name; public static int age; public User() { System.out.println("这是无参构造"); } { System.out.println("这是构造代码块"); } static{ System.out.println("这是静态代码块"); } public static void People(){ System.out.println("这是静态方法"); } }

在Main类中进行类加载的测试

public class Main { public static void main(String[] args) throws ClassNotFoundException { User user = new User();//创建User类对象 //结果为 //这是静态代码块 //这是构造代码块 //这是无参构造 User.age=88;//调用静态成员变量,结果为这是静态代码块 User.People();//调用静态方法,结果为这是静态代码块、这是静态方法 } } public class Main { public static void main(String[] args) throws ClassNotFoundException { Class<User> userClass = User.class; //结果为无,User.class 只是加载类并创建 Class 对象, //而不会执行类的初始化代码 } }

动态类加载

public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("User");//结果为这是静态代码块 ClassLoader sc = ClassLoader.getSystemClassLoader();//获取当前系统的类加载器 Class.forName("User",false,sc);//结果为无 } }

原因:跟进forName方法可以发现,该方法会调用一个forName0的方法而其第二个参数是默认情况下是true(其表示默认情况下会进行初始化),进而完成初始化操作,调用静态代码块。当然可以将其设置为false来实现不初始化

202501181940311.png

对ClassLoader的思考

public class Main { public static void main(String[] args) throws ClassNotFoundException { ClassLoader sc = ClassLoader.getSystemClassLoader(); System.out.println(sc);//结果sun.misc.Launcher$AppClassLoader@18b4aac2 } }

得到的结果是Launcher类中的一个内部类AppClassLoader,这就引出了一个双亲委派的类加载机制

202501181940855.png

双亲委派模型是指,要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。Java平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。

通过类加载实现任意类的加载

URLClassLoader

动态加载.class文件

file协议

创建一个Hello类,在其中创建一个静态代码块进而实现计算机的弹出

import java.io.IOException; public class Hello { static{ try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } }

在idea中编译生成一个.class文件,将其移动到E盘,并将创建的Hello.java文件删除否则会直接去找.java文件实现类的加载

202501181942462.png

在测试类Main中通过URLclassLoader类来进行磁盘中.class文件的获取与加载,之后通过newInstance方法来实现初始化

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class Main { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///E:\\")}); //注意要在file:///E:后再加两个反斜杠,否则会去找E盘中的xxx.jar文件而不是.class文件 Class<?> hello = urlClassLoader.loadClass("Hello"); hello.newInstance(); } }

202501181940368.png

若直接是file:///E:会产生NotFound的报错

202501181941667.png

http协议

使用file协议只能进行加载本地的类,最重要的还是使用http协议

在E盘中开启一个http服务

python -m  http.server  8888

202501181941326.png

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class Main { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8888/")}); Class<?> hello = urlClassLoader.loadClass("Hello"); hello.newInstance(); } }

202501181941551.png

成功接受到访问的请求

202501181941630.png

动态加载jar包

将实例化URLClassLoader对象时的参数修改为jar:file:///E:\\Hello.jar!/

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class Main { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\\Hello.jar!/")}); //URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:8888/Hello.jar!/")}); Class<?> hello = urlClassLoader.loadClass("Hello"); hello.newInstance(); } }

202501181941808.png

使用ClassLoader中的defineClass实现动态类加载

因为ClassLoader是一个抽象类,无法直接实例化这个类,利用了反射来获取其中的defineClass方法

202501181941097.png

public class Main { public static void main(String[] args) throws Exception { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); byte[] code= Files.readAllBytes(Paths.get("E:\\Hello.class"));//读取Hello.class的字节码长度 Class test =(Class) defineClass.invoke(systemClassLoader, "Hello", code, 0, code.length); test.newInstance(); } }

202501181941924.png

缺点:因为在ClassLoader中的defineClass是方法是一个protect修饰符,无法直接使用反射来进行调用

利用Unsafe中的defineClass实现任意加载任意类

在Unsafe中有一个defineClass方法其是共有的

202501181941185.png

Unsafe的构造方法是私有的无法直接实例化Unsafe对象,但是其有一个getUnsafe的静态方法可以实例化一个Unsafe对象,但问题是该方法必须通过Security校验直接使用会报错

202501181941291.png

202501181941024.png

但是有一个成员变量可以实现Unsafe的实例化

202501181941130.png

但是该成员变量是私有的

202501181941812.png

那么我们可以利用反射getDeclaredFiled来获取这个成员变量,进而实现defineClass方法的调用

public class Main { public static void main(String[] args) throws Exception { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class<Unsafe> unsafeClass = Unsafe.class; Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");//获取theUnsafe成员变量 theUnsafe.setAccessible(true); Unsafe unsafe= (Unsafe) theUnsafe.get(null); byte[] code= Files.readAllBytes(Paths.get("E:\\Hello.class"));//获取字节码 Class<?> hello = unsafe.defineClass("Hello", code, 0, code.length, systemClassLoader, null); hello.newInstance(); } }

202501181941734.png