类的生命周期 在开始前,得先知道 Java 核心是怎么样的,
1 2 3 类加载器 双亲委派 热部署 自定义类加载器 Class.forName 打破委派 模块化 字节码操作 SPI机制 OSGi/Jigsaw Agent技术
Java 在加载类的时候,一般有如下生命周期存在,类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(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 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null ) { try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { c = findClass(name); } } return c; }
从上述代码很容易发现,如果我们要加载一个类,系统会先去加载类是否已经加载进JVM,然后在委派给父加载器,最后委派给 BootStrap,如果都加载失败了,那就自己加载,所以我们打破双亲委派主要有三种方式
覆写重写 loadClass() 方法
使用线程上下文类加载器(典型场景:SPI)
自定义 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 { 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应用默认的类加载顺序是(打破了双亲委派规则):
先从JVM的BootStrapClassLoader中加载。
加载Web应用下 /WEB-INF/classes 中的类。
加载Web应用下 /WEB-INF/lib/*.jap 中的jar包中的类。
加载上面定义的 System 路径下面的类。
加载上面定义的 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/" ) }); 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" ) }); 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!/" ) }); 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() 负责拦截方法调用,并且每个代理实例都有一个关联的调用处理程序 重新看我们上面那个静态代理代码,我们可以整理成动态代理核心三要素
被代理对象(目标对象) → 就是我们刚才的 UserServiceImpl
增强逻辑 → 日志、权限、事务等(写在 InvocationHandler 中)
代理对象 → 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;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); } } 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; } } 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,框架只定义接口,具体实现由外部提供,运行时自动发现并加载
工作方式
定义接口(服务提供方定义规范)
第三方实现接口(写自己的实现类)
配置文件注册:在 META-INF/services/ 目录下,创建一个以接口全限定名命名的文件,文件内容写实现类的全限定名
使用 ServiceLoader 加载:框架调用 ServiceLoader.load(接口.class) 自动获取所有可用实现
总的来说 API 是你调别人,SPI 是别人调你写的实现
References