Understanding SPI, Class Loading, and Dynamic Proxy in Java

类的生命周期

在开始前,得先知道 Java 核心是怎么样的,

1
2
3
类加载器   双亲委派   热部署    自定义类加载器
Class.forName 打破委派 模块化 字节码操作
SPI机制 OSGi/Jigsaw Agent技术

Java 在加载类的时候,一般有如下生命周期存在,类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading) 其中,验证、准备和解析这三个阶段可以统称为连接(Linking),一个类如果要想被运行,得先加载到 JVM 当中,也就是要加载到虚拟机才能够被运行和使用,JVM 一般加载一个类有三个步骤,加载->连接->初始化,连接过程又可以分为,验证->准备->解析 JVM 启动的时候,并不会一次性去加载所有的类,而是会则需索取,将要加载的类提前放到 JVM 里,在想要调用的时候就去调用,并且在加载一个新的类时,会先判断该类之前是否被调用过

类加载器层次结构

JVM 内部主要内置了三个重要的 ClassLoader

1
2
3
4
5
6
7
Bootstrap ClassLoader (JVM内核,C++实现)

Extension ClassLoader (加载jre/lib/ext)

Application ClassLoader (加载classpath)

自定义ClassLoader

自底向上查找判断类是否被加载,自顶向下尝试加载类

Bootstrap ClassLoader

这个为启动类加载器,是最后一层加载器,是 Java 中最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。

Extension ClassLoader

主要负责加载 %JRE_HOME%/lib/ext目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。

Application ClassLoader

面向用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类,每个 ClassLoader 可以通过 getParent() 获取其父 ClassLoader,如果获取到的 ClassLoader 为null的话,那么该类加载器的父类加载器是 BootstrapClassLoader 。

双亲委派

我们在加载类的时候通常会加载很多类,那么这时候 Java 就会自己去找需要加载哪一些类,这时候就用上双亲委派机制了

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 虚拟机中被称为 “bootstrap class loader”的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

  • 这里我们可以知道,每一个 ClassLoader 都有一个最顶层的父类
  • 并且启动一个 ClassLoader 实例之前,会先找一下与自身有关系的类

从上面这个机制我们可以得知,有了双亲委派机制实际上是给 Java 中提供了一层安全保护,它通过委派父加载器优先加载类的方式,来实现一系列的安全指标,例如可以防止恶意篡改系统的一些关键 API 类,或者是防止重复类的加载,比如我们想要重复加载一个 java.lang.String 这个类,那就不行了,例如我们可以自己去定义一个 java.lang.String,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.lang;

public class String {
@Override
public String toString() {
return "Breaking!";
}

public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
}
}

然后这时候运行,会显示下面这些信息,这个时候就是双亲委派机制发力了

1
2
3
4
5
Error: Main method not found in class java.lang.String, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Process finished with exit code 1

我们在加载的时候,他会一张往上面找,这时候如果找到已经存在的类,那他就会停下来,就不再加载了,例如 java.lang.String 这个类在 BootStrap ClassLoader 这里面是有的, 也就是 JRE 里面,那他发现了之后就会停止下来不会再去加载

打破双亲委派

我们现在来查看一下正常的双亲委派机制长啥样,示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ClassLoader.loadClass() 默认实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);

if (c == null) {
try {
// 2. 委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 父加载器为null,委派给Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 4. 父加载器加载失败,自己加载
c = findClass(name);
}
}
return c;
}

从上述代码很容易发现,如果我们要加载一个类,系统会先去加载类是否已经加载进JVM,然后在委派给父加载器,最后委派给 BootStrap,如果都加载失败了,那就自己加载,所以我们打破双亲委派主要有三种方式

  1. 覆写重写 loadClass() 方法
  2. 使用线程上下文类加载器(典型场景:SPI)
  3. 自定义 ClassLoader 直接调用 findClass

重写 loadClass() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BreakParentDelegationLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 关键:不让父加载器加载,自己先加载
// 但核心类还是要交给父加载器,否则会破坏JVM安全
// 已经加载过的类直接返回
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 其他类,自己先加载(打破双亲委派)
try {
// 自己去找类
c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
} catch (ClassNotFoundException e) {
// 自己加载失败,再交给父加载器
return super.loadClass(name, resolve);
}
}
}

应用

以 Tomcat 为例子 Web应用默认的类加载顺序是(打破了双亲委派规则):

  1. 先从JVM的BootStrapClassLoader中加载。
  2. 加载Web应用下 /WEB-INF/classes 中的类。
  3. 加载Web应用下 /WEB-INF/lib/*.jap 中的jar包中的类。
  4. 加载上面定义的 System 路径下面的类。
  5. 加载上面定义的 Common 路径下面的类。 如果在配置文件中配置了,那么就是遵循双亲委派规则,加载顺序如下:
  • 先从JVM的BootStrapClassLoader中加载。
  • 加载上面定义的System路径下面的类。
  • 加载上面定义的Common路径下面的类。
  • 加载Web应用下/WEB-INF/classes中的类。
  • 加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。

动态加载字节码

Java 中的字节码通常指的是 .class 的文件,我们所说的动态加载字节码,一般是运行时加载/生成/修改类的字节码,而不是编译时确定实现热部署、AOP、动态代理等技术的基础 例如我们可以先编写一个 Exploit.java

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;
public class Exploit {
static {
try {
Runtime.getRuntime().exec("open -a Calculator.app");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

编译一下,然后得到路径 /Users/icecliffs/Documents/Coding/java_basic/target/classes/Exploit.class,那么要怎么加载这个字节码呢,这时候就会用到一个 URLClassLoader,这实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程,我们可以来看一下 ClassLoader 的继承

1
2
3
4
5
java.lang.Object
└── java.lang.ClassLoader
└── java.security.SecureClassLoader
└── java.net.URLClassLoader
└── 你的自定义类加载器

传统加载

我们可以利用这个来动态加载字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package loader;

import java.net.URL;
import java.net.URLClassLoader;

public class URLLoader {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("file:/Users/icecliffs/Documents/Coding/java_basic/target/classes/")
// new File("/Users/icecliffs/Documents/Coding/java_basic/target/classes/").toURI().toURL()
});
Class clazz = urlClassLoader.loadClass("Exploit");
clazz.newInstance();
}
}

上面是通过 file 来加载的,当然我们也可以通过 http 来加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package loader;

import java.net.URL;
import java.net.URLClassLoader;

public class URLLoader {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("http://127.0.0.1:8000/exploit.class")
// new URL("file:/Users/icecliffs/Documents/Coding/java_basic/target/classes/")
// new File("/Users/icecliffs/Documents/Coding/java_basic/target/classes/").toURI().toURL()
});
Class clazz = urlClassLoader.loadClass("Exploit");
clazz.newInstance();
}
}

或者也可以通过 jar+file 协议来进行加载,首先先把 class 打包成一个 jar 包

1
jar -cvf exploit.jar exploit.class

然后使用 jar 协议进行加载,需要注意末尾感叹号的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package loader;

import java.net.URL;
import java.net.URLClassLoader;

public class URLLoader {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("jar:file:/Users/icecliffs/Documents/Coding/java_basic/target/classes/exploit.jar!/")
// new URL("http://127.0.0.1:8000/exploit.class")
// new URL("file:/Users/icecliffs/Documents/Coding/java_basic/target/classes/")
// new File("/Users/icecliffs/Documents/Coding/java_basic/target/classes/").toURI().toURL()
});
Class clazz = urlClassLoader.loadClass("Exploit");
clazz.newInstance();
}
}

ClassLoader#defineClass直接加载

无论你怎么加载一个 .class,他基本上都是按照下面这些步骤来进行加载的

1
ClassLoader#loadClass -> ClassLoader#findClass -> ClassLoader#defineClass
  • loadClass 的作用用于从已加载的类型、父加载器搭配双亲委派机制来加载一个类,如果前面都没有找到那会调用 findClass 来找类
  • 根据URL指定的方式来加载类的字节码,其中会调用 defineClass();
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类 示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package loader;

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Loader2 {
public static void main(String[] args) throws Exception {
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("/Users/icecliffs/Documents/Coding/java_basic/target/classes/Exploit.class"));
Class c = (Class) method.invoke(classLoader, "Exploit", code, 0, code.length);
c.newInstance();
}
}

上面代码通过反射直接调用 defineClass 来加载我们的字节码,如下图为 java.lang.ClassLoader#defineClass 的示例代码

1
2
3
4
5
6
7
8
9
10
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

UnSafe 加载字节码

这个很熟悉了,可以查看 26.关于Unsafe,其核心就是可以不用 ClassLoader 来加载我们的字节码,因为 Unsafe 内置了一个 defineClass,示例代码如下,请记住,Unsafe 一般只能通过反射来调用

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
package loader;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class Loader3 {
public static void setFieldValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
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("/Users/icecliffs/Documents/Coding/java_basic/target/classes/Exploit.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Exploit", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}

利用 TemplatesImpl 加载字节码

这个如果了解过 CC 链应该特别熟悉了,这里也可以查看 26.Java基础关键组件分析 > TemplatesImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package loader;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Loader4 {
public static void setFieldValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("/Users/icecliffs/Documents/Coding/java_basic/target/classes/ExploitTemplates.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "111");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
}

利用 BCEL 加载字节码

这个一般会搭配 fastjson 进行使用,我们可以看这两篇文章 15.Fastjson反序列化漏洞 和 [1.BCEL ClassLoader](1.BCEL ClassLoader),在开始之前我们需要把我们的字节码转换成 BCEL 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package loader;

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;

public class Loader5 {
public static void main(String[] args) throws Exception {
Class exploit = Class.forName("Exploit");
JavaClass javaClass = Repository.lookupClass(exploit);
String beclCode = Utility.encode(javaClass.getBytes(), true);
System.out.println(beclCode);
}
}

记住,jdk8u71没有这个东西,运行后我们会生成

1
$l$8b$I$A$A$A$A$A$A$AmQ$5dO$TA$U$3d$d3$96Nw$d9$a5PhAT$y$7e$d1$92H_$7c$x$e1$85$60b$5c$c5XR$c3$e3t$Y$cb$e0$b2$b3$d9N$b5$ff$c8g$5e$d4H$82$ef$fe$u$e3$9d$b5$a9Mp$93$bdw$ee9$e7$9e$7bw$f6$d7$ef$l7$A$9e$a3$e5$c3$c3$86$8f$3b$d8$ac$e0$ae$cb$f78$eesl$f9$u$e3$BG$93c$9b$a1$bc$af$Tm$P$Y$8a$adv$9f$a1th$ce$UC5$d2$89z3$be$i$a8$ecD$MbBj$91$91$o$ee$8bL$bbz$K$96$ec$b9$k1x$d1$d1$q$8d$8d$b6$5d$86$ca$be$8c$a7$8e$8c$U$f5$e8B$7c$S$jm$3a$_$8f$8f$sR$a5V$9b$84da$cf$K$f9$f1$b5Hs$t$da$8b$c1$ef$99q$s$d5$L$ed$9c$83$a9$e3$9ek$P$e0c$91$e3a$80Gx$cc$d00$a9J$9a$cfD$f3P$c4r$i$Lk$b2$3d$91$a6$B$9e$e0$v$c3$ea$7f$G2l$e6h$y$92a$e7$dd8$b1$faR$cdH$e7$be$c3$c0$a7$T$Z$96$ffi$8f$H$XJ$S$b4r$ab$9d$f6$j$w$3b$x$ea$advtKC$dfYR$T$r$ZvZsl$cff$3a$Zv$e7$h$defF$aa$d1$88$g6$e6$95$t$e7$99$f9$ec$$$a8$db$eec$h$V$fa$a1$ee$v$80$b9$h$a1$YP$d5$a1$cc$u$_$ec$7e$D$bb$ca$e9$90b9$H$LX$a2$Y$fc$V$a0$8ae$ca$V$ac$cc$9a$3f$a0$98s$eb$dfQ$a8$V$bf$a2$f4$fe$L$c2W$d7$u$9f$92$h$ffy$95$93$kI$XH$e8l$htrV$k$d9$E4$q$E$t$cc$9b$8d$J$e9$5c$c3$wUk$f4r$U$o$8e$baGD$p$dff$fd$P$d9$fb$D$d4$9f$C$A$A

然后直接执行即可

1
2
3
4
5
6
7
8
9
10
11
package loader;

import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class Loader6 {
public static void main(String[] args) throws Exception {
new ClassLoader().loadClass(
"$$BECL$$" + "$l$8b$I$A$A$A$A$A$A$AmQ$5dO$TA$U$3d$d3$96Nw$d9$a5PhAT$y$7e$d1$92H_$7c$x$e1$85$60b$5c$c5XR$c3$e3t$Y$cb$e0$b2$b3$d9N$b5$ff$c8g$5e$d4H$82$ef$fe$u$e3$9d$b5$a9Mp$93$bdw$ee9$e7$9e$7bw$f6$d7$ef$l7$A$9e$a3$e5$c3$c3$86$8f$3b$d8$ac$e0$ae$cb$f78$eesl$f9$u$e3$BG$93c$9b$a1$bc$af$Tm$P$Y$8a$adv$9f$a1th$ce$UC5$d2$89z3$be$i$a8$ecD$MbBj$91$91$o$ee$8bL$bbz$K$96$ec$b9$k1x$d1$d1$q$8d$8d$b6$5d$86$ca$be$8c$a7$8e$8c$U$f5$e8B$7c$S$jm$3a$_$8f$8f$sR$a5V$9b$84da$cf$K$f9$f1$b5Hs$t$da$8b$c1$ef$99q$s$d5$L$ed$9c$83$a9$e3$9ek$P$e0c$91$e3a$80Gx$cc$d00$a9J$9a$cfD$f3P$c4r$i$Lk$b2$3d$91$a6$B$9e$e0$v$c3$ea$7f$G2l$e6h$y$92a$e7$dd8$b1$faR$cdH$e7$be$c3$c0$a7$T$Z$96$ffi$8f$H$XJ$S$b4r$ab$9d$f6$j$w$3b$x$ea$advtKC$dfYR$T$r$ZvZsl$cff$3a$Zv$e7$h$defF$aa$d1$88$g6$e6$95$t$e7$99$f9$ec$$$a8$db$eec$h$V$fa$a1$ee$v$80$b9$h$a1$YP$d5$a1$cc$u$_$ec$7e$D$bb$ca$e9$90b9$H$LX$a2$Y$fc$V$a0$8ae$ca$V$ac$cc$9a$3f$a0$98s$eb$dfQ$a8$V$bf$a2$f4$fe$L$c2W$d7$u$9f$92$h$ffy$95$93$kI$XH$e8l$htrV$k$d9$E4$q$E$t$cc$9b$8d$J$e9$5c$c3$wUk$f4r$U$o$8e$baGD$p$dff$fd$P$d9$fb$D$d4$9f$C$A$A"
).newInstance();
}
}

感兴趣得可以去了解一下 BCEL 生成和解码过程

JDK 动态代理

直接上案例,先来区分一下静态代理动态代理的区别

静态代理

  • 写一个UserService接口和实现类(有save()等方法)
  • 想在不改原代码情况下,给save()加日志、权限检查 → 用静态代理(手写代理类)
  • 发现每个方法都要重复写代理逻辑 → 引出动态代理的需求 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package proxy;
interface UserService {
void save(String username);
void delete(Long id);
}
class UserServiceImpl implements UserService {
@Override
public void save(String username) {
System.out.println("保存用户:" + username);
// 假设这里有复杂的业务逻辑
}
@Override
public void delete(Long id) {
System.out.println("删除用户,ID:" + id);
}
}
public class StaticProxy {

}

这个时候我们有一个新的需求,每个方法执行前打印 [LOG] 开始执行XX方法,执行后打印 [LOG] 结束执行XX方法,基本上只能这样子写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserServiceStaticProxy implements UserService {
private UserService target;

public UserServiceStaticProxy(UserService target) {
this.target = target;
}

@Override
public void save(String username) {
System.out.println("[LOG] 开始执行 save 方法");
target.save(username);
System.out.println("[LOG] 结束执行 save 方法");
System.out.println();
}

@Override
public void delete(Long id) {
System.out.println("[LOG] 开始执行 delete 方法");
target.delete(id);
System.out.println("[LOG] 结束执行 delete 方法");
System.out.println();
}
}

然后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
// 原始对象
UserService original = new UserServiceImpl();

// 使用代理对象(增强后的对象)
UserService proxy = new UserServiceStaticProxy(original);

System.out.println("===== 调用原始对象 =====");
original.save("张三");
original.delete(100L);

System.out.println("===== 调用代理对象 =====");
proxy.save("李四");
proxy.delete(200L);
}
}

痛点很直观了,就是有大量的重复代码、复用很差、并且添加新的接口后维护成本更高了,所以这时候就引入了动态代理

动态代理

主要要了解两个核心 java.lang.reflect.Proxy + InvocationHandler,基本东西和静态代理差不多,无非就是动态代理的代理类是动态生成的,静态代理的代理类是提前写好的

  • InvocationHandler,调用处理程序,其中 invoke() 负责拦截方法调用,并且每个代理实例都有一个关联的调用处理程序 重新看我们上面那个静态代理代码,我们可以整理成动态代理核心三要素
  1. 被代理对象(目标对象) → 就是我们刚才的 UserServiceImpl
  2. 增强逻辑 → 日志、权限、事务等(写在 InvocationHandler 中)
  3. 代理对象 → JDK运行时自动生成(不需要手写代理类) 我们直接编写动态代理代码,深入理解
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
package proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1. 接口(保持不变)
interface UserService {
void save(String username);
void delete(Long id);
}

// 2. 原始实现类(保持不变)
class UserServiceImpl implements UserService {
@Override
public void save(String username) {
System.out.println("保存用户:" + username);
}

@Override
public void delete(Long id) {
System.out.println("删除用户,ID:" + id);
}
}

// 3. 核心:增强逻辑处理器
class LogInvocationHandler implements InvocationHandler {
private Object target; // 被代理的原始对象

public LogInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ---- 前置增强 ----
System.out.println("[LOG] 开始执行 " + method.getName() + " 方法");

// 调用原始对象的方法
Object result = method.invoke(target, args);

// ---- 后置增强 ----
System.out.println("[LOG] 结束执行 " + method.getName() + " 方法");
System.out.println();

return result;
}
}

// 4. 测试
public class DynamicProxyDemo {
public static void main(String[] args) {
// 原始对象
UserService original = new UserServiceImpl();

// 创建动态代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
original.getClass().getClassLoader(), // 类加载器
original.getClass().getInterfaces(), // 要代理的接口数组
new LogInvocationHandler(original) // 增强逻辑处理器
);

System.out.println("===== 调用原始对象 =====");
original.save("张三");
original.delete(100L);

System.out.println("===== 调用代理对象 =====");
proxy.save("李四");
proxy.delete(200L);

// 额外:看看代理对象的真面目
System.out.println("代理对象的类型:" + proxy.getClass().getName());
}
}

运行后的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
===== 调用原始对象 =====
保存用户:张三
删除用户,ID:100
===== 调用代理对象 =====
[LOG] 开始执行 save 方法
保存用户:李四
[LOG] 结束执行 save 方法

[LOG] 开始执行 delete 方法
删除用户,ID:200
[LOG] 结束执行 delete 方法

代理对象的类型:proxy.$Proxy0

SPI 机制

SPI 是 Java 提供的一种服务发现机制,允许框架或库定义接口,而让第三方(或不同厂商)提供具体实现,并在运行时动态加载这些实现,无需修改原有代码。

原理:SPI的原理是基于Java的ClassLoader机制实现的。在Java中,类的加载是由ClassLoader负责的。ClassLoader可以从不同的源加载类,例如从本地文件系统、网络、JAR文件或其他任何资源中加载类。SPI将服务的接口定义放在一个模块中,服务的实现放在另外的模块中,并通过ClassLoader动态地加载实现类。

为什么要有 SPI?比如我们设计了一个登录认证框架,并且定义了一个接口

1
2
3
public interface LoginService {
boolean authenticate(String user, String password);
}

不同公司可能想用自己的认证方式(数据库、LDAP、OAuth),没有 SPI 时,框架需要硬编码具体实现类,改一种认证方式就要改框架代码,有了 SPI,框架只定义接口,具体实现由外部提供,运行时自动发现并加载

工作方式

  1. 定义接口(服务提供方定义规范)
  2. 第三方实现接口(写自己的实现类)
  3. 配置文件注册:在 META-INF/services/ 目录下,创建一个以接口全限定名命名的文件,文件内容写实现类的全限定名
  4. 使用 ServiceLoader 加载:框架调用 ServiceLoader.load(接口.class) 自动获取所有可用实现

总的来说 API 是你调别人,SPI 是别人调你写的实现

References

Support via Solana

Solana

Solana

Solana Pay

Solana Pay

WeChat

WeChat