九、Java的代理模式和SPI机制
# 九、Java的代理模式和SPI机制
# 1、什么是代理模式?
代理模式( Proxy ),给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通常会通过代理对象来为原对象添加额外的功能。
代理模式属于结构型模式主要用于处理类或对象的组合。
上面是比较正式的书面释义,举个通俗点的例子来帮助理解:
周末你躺在床上饿了想吃火鸡面,但是你有点懒,你找你女朋友帮你泡好并端到你面前喂你吃,最后你把火鸡面吃完了,说了句宝宝你真好~
这个例子中:你吃了一包火鸡面,是你做的动作。 但是你请了你的女朋友帮你泡火鸡面,你的女朋友就可以理解为是你的代理,此时你女朋友这个代理帮你泡了火鸡面(代理对象添加了额外的功能)。为了完成你吃火鸡面这件事 ,最后你女朋友还得端到你面前喂你吃(这里的喂你吃火鸡面就相当于 (代理对象控制了原对象的引用) )。
# 2、静态代理和动态代理
根据名称我们可以猜测,静态的代理一定不够灵活,运用到代码中是耦合的。
而动态代理顾名思义是动态的,灵活的,运用到代码中是解耦的。
静态代理:
在静态代理中,代理类是在编译时就确定的,即在代码中显式地定义了代理类(需要手动编写一个代理类)。
代理类通常与被代理类实现相同的接口,并且在代理类的方法中调用被代理类的方法。
静态代理的一个缺点是每次添加一个新的功能,都需要创建一个代理类,这样会导致类的数量增加,并且会造成代码的冗余。
代理类通常需要直接引用被代理类,因此代理类对被代理类有一定的依赖关系。如果被代理类的接口发生变化,代理类也需要相应地进行修改,这增加了代码的耦合性。
静态代理实现步骤:
- ①、定义一个接口用于被代理类实现,定义一个被代理类实现前面定义的接口;
- ②、自定义一个代理类实现前面定义的接口 ,并重写接口中需要被代理的方法,在重写的方法中调用原方法(被代理类的方法)并自定义一些处理逻辑;
- ③、通过代理类创建代理对象,使用代理对象替换原对象调用方法;
静态代理图示:

静态代理的代码示例:
public class TestStaticProxy { public static void main(String[] args) { YourBehavior you = new You(); // 不使用代理 you.eat("火鸡面"); System.out.println("========================"); // 使用代理 YourBehavior yourGirlfriend = new YourGirlfriend(you); yourGirlfriend.eat("火鸡面"); } } /** * 行为接口 (代理类和被代理类都实现这个接口) * */ interface YourBehavior { /** * 吃 * @param something 吃的东西 */ void eat(String something); } /** * 这个类代表你 (被代理类) * */ class You implements YourBehavior{ @Override public void eat(String something) { System.out.println("你吃:" + something); } } /** * 这个类代表你女友 (代理类) * */ class YourGirlfriend implements YourBehavior{ private You you; public YourGirlfriend() { } // 静态代理 必须依赖被代理类 public YourGirlfriend(You you) { // 说明你的女朋友心里有你~ this.you = you; } @Override public void eat(String something) { System.out.println("帮你泡火鸡面,带到你面前"); // 代理对象调用 原对象的方法 you.eat("火鸡面"); System.out.println("你说:谢谢宝~"); } }
复制成功!
运行结果:
吃:火鸡面 ======================== 帮你泡火鸡面,带到你面前 吃:火鸡面 你说:谢谢宝~
复制成功!
动态代理:
动态代理是在运行时生成的代理类,而不是在编译时确定的。Java中的动态代理机制主要依靠Java反射机制实现。
动态代理更加灵活,因为它可以在运行时决定要代理的对象及其行为,而不需要显式地为每个类编写代理类。
动态代理通常用于实现横切关注点(cross-cutting concerns AOP相关概念后续在Spring框架相关博客会详细介绍)的功能,
例如日志记录、性能监控、事务管理等。因为动态代理可以在运行时将这些功能动态地添加到方法调用中,而不需要修改原始类的代码。这就达到了解耦的目的。
Java中的动态代理主要是通过java.lang.reflect.Proxy类 和 InvocationHandler 接口实现的。 动态代理的代码示例:
JDK动态代理实现的步骤:
- ①、定义一个接口用于被代理类实现,定义一个被代理类实现前面定义的接口;
- ②、自定义类实现 InvocationHandler接口 并重写invoke方法,在 invoke 方法中调用原方法(被代理类的方法)并自定义一些处理逻辑;
- ③、通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象,使用代理对象调用方法。
参数说明: loader 目标对象的类加载器,interfaces目标对象实现的全部接口,h 自定义的InvocationHandler实例;
JDK动态代理图示:

JDK动态代理代码示例:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class TestDynamicProxy { public static void main(String[] args) { You you = new You(); you.eat("火鸡面"); System.out.println("========================"); // 获取代理对象 YourBehavior proxyInstance = (YourBehavior)ProxyFactory.getProxyObject(you); // 使用代理对象 调用方法 proxyInstance.eat("火鸡面"); } } /** * 行为接口 (代理类和被代理类都实现这个接口) */ interface YourBehavior { /** * 吃 * * @param something 吃的东西 */ void eat(String something); } /** * 这个类代表你 (被代理类) */ class You implements YourBehavior { @Override public void eat(String something) { System.out.println("你吃:" + something); } } class YouInvocation implements InvocationHandler { private Object targetObj; public YouInvocation() { } public YouInvocation(Object targetObj) { this.targetObj = targetObj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("帮你泡火鸡面,带到你面前"); // 调用原对象的方法 Object result = method.invoke(targetObj, args); System.out.println("你说:谢谢宝~"); return result; } } // 代理工厂 class ProxyFactory { /** * 根据原对象生成代理对象 * * @param targetObj 原对象 * @return 代理对象 */ public static Object getProxyObject(Object targetObj){ return Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(), new YouInvocation(targetObj)); } }
复制成功!
运行结果:
你吃:火鸡面 ======================== 帮你泡火鸡面,带到你面前 你吃:火鸡面 你说:谢谢宝~
复制成功!
# 3、JDK动态代理的局限性
①、只能代理实现了接口的类: JDK动态代理的机制要求被代理的类必须实现至少一个接口,因为它是基于接口来生成代理类的。这意味着如果目标类没有实现接口,就无法使用JDK动态代理。
②、无法直接代理类的方法: JDK动态代理只能代理接口中定义的方法,无法直接代理类中的方法。如果需要代理类中的方法,就需要使用CGLIB等其他代理机制。
③、性能相对较低: 由于JDK动态代理是基于Java反射机制实现的,相比较于静态代理或者其他代理方式,它的性能会相对较低一些。这是因为在运行时生成代理类和方法调用的过程中,需要进行额外的反射操作,会带来一定的性能开销。 (持怀疑态度,随着JDK版本优化,JDK提供的动态代理性能也在逐渐升高)
# 4、使用CGLIB代理机制完成未实现接口的类的代理
CGLIB(Code Generation Library)是一个Java字节码生成库,它被广泛用于在运行时动态生成新的类以实现代理、混入(Mixin)和其他类似的功能。
CGLIB通过生成目标类的子类,并在子类中重写需要代理的方法来实现代理功能,因此它可以代理那些没有实现接口的类。
CGLIB通常与其他代理机制(如JDK动态代理)相比具有更高的性能,因为它不需要依赖于接口,而且可以代理类中的方法,但是不包括final方法,因为被final修饰的方法无法被子类重写。
(持怀疑态度,下面会使用JDK8版本 来简单比较下两者的性能)
Spring框架就集成了CGLIB,默认情况下Spring 的AOP功能实现代理的方式:如果目标对象实现了接口,默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB中实现动态代理的关键是 MethodInterceptor 接口和 Enhancer 类。 MethodInterceptor接口中的intercept方法 用来拦截增强被代理类的方法 ,Enhancer类的create方法用来创建代理对象。
CGLIB动态代理的实现步骤:
- ①、引入CGLIB坐标, 目前最新版本为3.3.0
https://central.sonatype.com/artifact/cglib/cglib/versions (opens new window)

<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
复制成功!
选择目标类:选择需要代理的目标类,这个目标类可以是任意的普通类,不一定需要实现接口;
- ②、实现 MethodInterceptor接口,重写 intercept 方法用于拦截增强被代理类的方法;
- ③、创建Enhancer对象设置相应参数,调用 create()方法创建代理类;
CGLIB动态代理图示:

CGLIB动态代理代码示例:
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class TestDynamicProxy { public static void main(String[] args) { You you = new You(); Class<? extends You> youClass = you.getClass(); // 创建动态代理增强类 Enhancer enhancer = new Enhancer(); // 设置类加载器 enhancer.setClassLoader(youClass.getClassLoader()); // 设置被代理类 enhancer.setSuperclass(youClass); // 设置自定义的代理类拦截器 enhancer.setCallback(new YourGirlFriend()); // 创建代理类 You youProxy = (You) enhancer.create(); you.eat("火鸡面"); System.out.println("================"); youProxy.eat("火鸡面"); } } /** * 这个类代表你 (被代理类) */ class You { public void eat(String something) { System.out.println("你吃:" + something); } public void sleep() { System.out.println("你睡觉了"); } } /** * 代理类 实现MethodInterceptor 重写 intercept */ class YourGirlFriend implements MethodInterceptor { /** * @param o 被代理对象 * @param method 被代理方法 * @param objects 方法入参 * @param methodProxy 用来调用原始方法 * @return 方法返回值 * @throws Throwable 异常 */ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { if(method.getName().equals("eat")) { System.out.println("帮你泡火鸡面,带到你面前"); } else if(method.getName().equals("sleep")) { System.out.println("帮你盖好被子,给你讲个故事"); } Object object = methodProxy.invokeSuper(o, objects); if(method.getName().equals("eat")) { System.out.println("你说:谢谢宝~"); } else if(method.getName().equals("sleep")) { System.out.println("你说:晚安~"); } return object; } }
复制成功!
# 5、JDK动态代理和CGLIB动态代理对比
①、原理
JDK动态代理:
JDK动态代理的原理主要基于Java的反射机制和java.lang.reflect.Proxy类与java.lang.reflect.InvocationHandler接口。 当通过代理对象调用方法时,实际上是调用了自定义的InvocationHandler的invoke()方法。这个方法内部会通过反射调用实际被代理对象的对应方法,并可以在此前后添加额外的处理逻辑。 在调用Proxy.newProxyInstance()时,如果代理类还没有被创建,JVM会动态地生成一个实现上述指定接口的代理类的字节码,并加载到JVM中。这个过程是透明的,开发者无需关心具体的生成细节。CGLIB动态代理: CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用。
CGLIB动态代理的核心在于通过继承和字节码操作技术,在运行时动态生成目标类的子类,并在子类中插入自定义的拦截逻辑,以此来达到在不修改目标类源码的情况下增强或控制其行为的目的。
②、使用场景
JDK动态代理:
目标类实现了接口,简单的功能增强CGLIB动态代理: 无接口的类,复杂的AOP逻辑
③、性能(下面的说法有待考证)
JDK动态代理:
在早期JDK版本中,JDK动态代理的性能通常被认为低于CGLIB,主要是因为每次方法调用都需要通过反射(Method.invoke())来完成,反射操作相对较慢。 但从JDK 1.8开始,尤其是随着后续版本的不断优化,JDK动态代理的性能有了显著提升。特别是在某些场景下,其性能已经与CGLIB相当,甚至有所超越。 JDK动态代理在创建代理对象时的开销较小,因为它基于接口实现,不需要生成大量的字节码。CGLIB动态代理: CGLIB通过字节码技术生成目标类的子类,这种方式在创建代理实例时的开销较大,因为需要生成新的字节码。 一旦代理对象创建完成,CGLIB的直接方法调用(通过子类覆盖父类方法)在运行时通常比JDK动态代理的反射调用更快,特别是在频繁调用方法的场景下(这个在JDK8版本的测试下,优势并不明显,也许是方法调用的次数还不够多)。 CGLIB能够代理没有实现接口的类,提供了更广泛的适用范围,但这也意味着在运行时对类进行了修改,增加了潜在的风险。
简单测试下(使用StopWatch计算,JDK8版本): 分别使用JDK和CGLIB的动态代理去测试创建对象的速度,和调用代理方法的速度,分别进行创建50000次对象的时间比较和使用创建好的代理对象调用50000次代理方法的比较。 PS:被代理类是一样的,只是放在了不同的包下,其中JDK的被代理类多实现了一个接口。
测试代码如下:
import com.kinggm.testb.ProxyFactory; import com.kinggm.testb.YourBehavior; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import org.springframework.util.StopWatch; import java.lang.reflect.Method; public class TestDynamicProxy { public static void main(String[] args) { StopWatch stopWatch = new StopWatch("测试CGLIB和JDK动态代理创建50000次代理对象的时间占比"); stopWatch.start("CGLIB创建代理对象50000次"); for (int i = 0; i < 50000; i++) { You you = new You(); Class<? extends You> youClass = you.getClass(); // 创建动态代理增强类 Enhancer enhancer = new Enhancer(); // 设置类加载器 enhancer.setClassLoader(youClass.getClassLoader()); // 设置被代理类 enhancer.setSuperclass(youClass); // 设置自定义的代理类拦截器 enhancer.setCallback(new YourGirlFriend()); // 创建代理类 You youProxy = (You) enhancer.create(); } stopWatch.stop(); stopWatch.start("JDK创建代理对象50000次"); for (int i = 0; i < 50000; i++) { com.kinggm.testb.You you = new com.kinggm.testb.You(); // 获取代理对象 com.kinggm.testb.YourBehavior proxyInstance = (YourBehavior) ProxyFactory.getProxyObject(you); } stopWatch.stop(); // ============================================================================================================ You you = new You(); Class<? extends You> youClass = you.getClass(); // 创建动态代理增强类 Enhancer enhancer = new Enhancer(); // 设置类加载器 enhancer.setClassLoader(youClass.getClassLoader()); // 设置被代理类 enhancer.setSuperclass(youClass); // 设置自定义的代理类拦截器 enhancer.setCallback(new YourGirlFriend()); // 创建代理类 You youProxy = (You) enhancer.create(); com.kinggm.testb.You you1 = new com.kinggm.testb.You(); // 获取代理对象 com.kinggm.testb.YourBehavior proxyInstance = (YourBehavior) ProxyFactory.getProxyObject(you1); StopWatch stopWatch1 = new StopWatch("测试CGLIB和JDK动态代理使用代理对象调用50000次代理方法的时间占比"); stopWatch1.start("CGLIB代理调用方法50000次"); for (int i = 0; i < 50000; i++) { youProxy.eat("火鸡面"); } stopWatch1.stop(); stopWatch1.start("JDK代理调用方法50000次"); for (int i = 0; i < 50000; i++) { // 使用代理对象 调用方法 proxyInstance.eat("火鸡面"); } stopWatch1.stop(); System.out.println(stopWatch.prettyPrint()); System.out.println("========================================"); System.out.println(stopWatch1.prettyPrint()); } } /** * 这个类代表你 (被代理类) */ class You { public void eat(String something) { System.out.println("你吃:" + something); } public void sleep() { System.out.println("你睡觉了"); } } /** * 代理类 实现MethodInterceptor 重写 intercept */ class YourGirlFriend implements MethodInterceptor { /** * @param o 被代理对象 * @param method 被代理方法 * @param objects 方法入参 * @param methodProxy 用来调用原始方法 * @return 方法返回值 * @throws Throwable 异常 */ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("帮你泡火鸡面,带到你面前"); Object object = methodProxy.invokeSuper(o, objects); System.out.println("你说:谢谢宝~"); return object; } }
复制成功!
运行三次结果如下:
第一次: StopWatch '测试CGLIB和JDK动态代理创建50000次代理对象的时间占比': running time = 250062000 ns --------------------------------------------- ns % Task name --------------------------------------------- 183527300 073% CGLIB创建代理对象50000次 066534700 027% JDK创建代理对象50000次 ======================================== StopWatch '测试CGLIB和JDK动态代理使用代理对象调用50000次代理方法的时间占比': running time = 1168326000 ns --------------------------------------------- ns % Task name --------------------------------------------- 583097900 050% CGLIB代理调用方法50000次 585228100 050% JDK代理调用方法50000次 第二次: StopWatch '测试CGLIB和JDK动态代理创建50000次代理对象的时间占比': running time = 270423500 ns --------------------------------------------- ns % Task name --------------------------------------------- 198542800 073% CGLIB创建代理对象50000次 071880700 027% JDK创建代理对象50000次 ======================================== StopWatch '测试CGLIB和JDK动态代理使用代理对象调用50000次代理方法的时间占比': running time = 1194475300 ns --------------------------------------------- ns % Task name --------------------------------------------- 616564500 052% CGLIB代理调用方法50000次 577910800 048% JDK代理调用方法50000次 第三次: StopWatch '测试CGLIB和JDK动态代理创建50000次代理对象的时间占比': running time = 243452600 ns --------------------------------------------- ns % Task name --------------------------------------------- 184046700 076% CGLIB创建代理对象50000次 059405900 024% JDK创建代理对象50000次 ======================================== StopWatch '测试CGLIB和JDK动态代理使用代理对象调用50000次代理方法的时间占比': running time = 1227753400 ns --------------------------------------------- ns % Task name --------------------------------------------- 572891200 047% CGLIB代理调用方法50000次 654862200 053% JDK代理调用方法50000次
复制成功!
从结果可以看出基本符合上面的性能总结: JDK的动态代理创建代理对象的速度比CGLIB快的多,代理对象调用方法的速度不相上下。
# 6、JDK动态代理为什么只能代理实现接口的类?
如果别人问我这个问题,我会这样回答: 因为JDK提供的创建代理对象的API
Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(), new YouInvocation(targetObj));
复制成功!
第二个参数就是要传被代理对象实现的接口,这是王八的屁股,规定!
网上还有一种说法是:
因为Java语言的类是单继承的,JDK自动生成的代理类已经继承了Proxy类,要想保证代理类和被代理类有一致的行为(方法),显然不能通过继承重写方法来实现了,因为JDK自动生成的代理类已经继承了Proxy类;那么只好让被代理类再实现一个接口,JDK自动生成的代理类也去实现这个接口,就能实现代理类和被代理类有一致的行为(方法)了。那么在代理类中重写被代理类的方法就能完成代理(功能增强)。
对于网上的说法持怀疑态度 有些比较模糊的观点最好亲自去比较权威的书上查证 ,或者去看源码查证
下面就去看看JDK动态代理相关的源码,我们来考证下网上的说法对不对。
进入Proxy.newProxyInstance方法中 有个getProxyClass0方法 (注释:Look up or generate the designated proxy class —— 查找或者生成指定的代理类) 该方法返回生成的代理类的class对象

再往下看

再看下getProxyClass0方法内部:(英语真的挺重要,尤其是看源码注释的时候)

再继续看 ProxyClassFactory
ProxyClassFactory是个静态的内部工厂类
作用就是根据给定的类加载器和接口数组生成、定义并返回代理类
生成的代理类的前缀是$Proxy

再往下看 代理类包名和类名的生成规则

答案已经近在眼前了
进入ProxyGenerator.generateProxyClass方法

那我们就在JVM启动的时候 加上 启动参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
通过IDEA设置JVM启动参数:



这个时候再执行下代码 查看结果:
public static void main(String[] args) throws Exception{ YourBehavior you = new You(); Class<?> youClass = you.getClass(); MyInvocationHandler myInvocationHandler = new MyInvocationHandler(you); YourBehavior proxyInstance = (YourBehavior)Proxy.newProxyInstance(youClass.getClassLoader(),youClass.getInterfaces(),myInvocationHandler); proxyInstance.eat("火鸡面"); System.out.println(proxyInstance.getClass()); }
复制成功!
结果:
帮你泡火鸡面,带到你面前 你吃:火鸡面 你说:谢谢宝~ class com.kinggm.test.$Proxy0
复制成功!
可以看到生成的代理类全限定名为: com.kinggm.test.$Proxy0
生成的文件在 项目根目录下:

由于我的接口并没有加public
interface YourBehavior{ void eat(String something); }
复制成功!
所以生成的代理类包名 并不是默认的 com.sun.proxy 而是和我的接口包名相同 并且继承了 Proxy类 和被代理类实现了相同的接口YourBehavior

最后再看下 JDK自动生成的代理类结构: (实际上生成的是.class文件,IDEA通过 FernFlower decompiler 插件把 .class字节码文件反编译成我们能看懂的.java类文件)
我们主要看三个地方$Proxy0的构造方法,初始化,重写的eat方法
①、构造方法
public $Proxy0(InvocationHandler var1) throws { super(var1); }
复制成功!
实际上执行的是父类 Proxy的构造方法,把我们自定义的InvocationHandler 传给父类Proxy中的InvocationHandler h
protected InvocationHandler h; protected Proxy(InvocationHandler h) { Objects.requireNonNull(h); this.h = h; }
复制成功!
②、静态代码块初始化 其中m3 就是我们实现的接口中的目标方法eat
private static Method m1; private static Method m2; private static Method m3; private static Method m0; static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m2 = Class.forName("java.lang.Object").getMethod("toString"); m3 = Class.forName("com.kinggm.YourBehavior").getMethod("eat", Class.forName("java.lang.String")); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } }
复制成功!
③、重写的目标(eat)方法
public static void main(String[] args) { YourBehavior you = new You(); MyInvocationHandler myInvocationHandler = new MyInvocationHandler(you); YourBehavior proxyInstance = (YourBehavior) Proxy.newProxyInstance(you.getClass().getClassLoader(), you.getClass().getInterfaces(), myInvocationHandler); proxyInstance.eat("火鸡面"); }
复制成功!
当我们执行代理类的eat方法时实际上执行的是 $Proxy0中的 eat方法
public final void eat(String var1) throws { try { super.h.invoke(this, m3, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } }
复制成功!
$Proxy0中的 eat方法调用的是父类中初始化的 MyInvocationHandler 中的invoke方法
class MyInvocationHandler implements InvocationHandler { private YourBehavior targetObj; public MyInvocationHandler() { } public MyInvocationHandler(YourBehavior targetObj) { this.targetObj = targetObj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("帮你泡火鸡面,带到你面前"); Object result = method.invoke(targetObj,args); System.out.println("你说:谢谢宝~"); return result; } }
复制成功!
所以在执行目标方法eat前后,你女朋友帮你泡火鸡面,带到你面前,你对你女朋友说了 谢谢宝~
分析至此, 发现网上说的好像也有点道理, JDK自动生成的代理类利用继承Proxy来初始化InvocationHandler , 并处理代理类和代理对象的生成逻辑,Java又是单继承的语言,想保证代理类和被代理类有一致的行为(方法),就只好让被代理类再实现一个接口,JDK自动生成的代理类也去实现这个接口,就能实现代理类和被代理类有一致的行为(方法)了。最后在代理类中重写被代理类的方法通过调用父类Proxy中初始化的自定义InvocationHandler 中的invoke方法,就能执行到我们自己写的增强逻辑了,同时invoke方法中又去调用了原方法,就完成了整个代理过程喽。
最后再画个图总结下吧:

# 7、Java的SPI机制
# 什么是Java的SPI ?
SPI全称 Service Provider Interface ,字面意思:“服务提供者的接口”,是一种服务发现机制。
用于实现框架或库的扩展点,允许在运行时动态地插入或更换组件实现。它提供了一个框架来发现和加载服务实现,使得软件模块能够灵活地选择和使用不同的服务提供商。SPI鼓励松耦合的设计,因为服务的消费者不需要直接依赖于具体的服务实现。
有没有想到面向对象的某个设计原则,SOLID中的 O 开闭原则,对扩展开放对修改关闭。
Java允许服务提供者,按照SPI给定的规则实现自己的服务,而Java应用使用通过SPI规则提供的服务时无需进行额外的配置,并且可以随时替换服务,也无需修改业务代码。
SPI可以和我们常用的API对比着理解。
API全称、Application Programming Interface,字面意思:“应用程序编程接口” 。
API是一组规则和定义,允许一个软件应用与另一个软件应用、库或操作系统进行交互。
它定义了如何进行数据传输、请求服务或执行特定功能的协议和工具。
API为开发人员提供了一种标准化的方式来访问和使用预先构建的功能,而无需了解这些功能内部的复杂实现细节。
总结:
名称 | 目的 | 使用者 | 举例 |
---|---|---|---|
SPI | 支持可插拔的架构,便于组件和服务的替换与扩展。 | 主要由服务提供者(如库、框架开发者)实现,但也需要应用开发者配置以启用特定的服务实现。(这里说的配置一般就是引入jar或者maven、gradle的坐标即可) | Java中,JDBC驱动的加载就是一个典型的SPI应用。 |
API | 简化开发过程,提高效率,促进不同系统间的互操作性。 | 通常由应用开发者使用,以集成外部服务或内部模块。 | 语音识别 API、文件上传 API等 |
个人感觉SPI就是种"设计模式",只是不在常见的23种设计模式之中,Java利用"SPI这种设计模式" 实现灵活的选择服务供应商实现,来达到解耦的目的,以此提高程序的可修改性和灵活性。
SPI和外观设计模式也有相通之处、外观模式(Facade) 定义了一个高层接口,为子系统中的一组接口提供一个一致的界面,从而简化子系统的使用。
简单的图示对比:
拿NASA的重返月球计划举例。
NASA准备把重返月球计划的着陆器设计部分分包给商业航天公司来完成。
如果还按照API的方式来实现,那么可能就是下图中的结果。NASA想替换某个方案就必须修改系统去适配其他公司的API接口。
这样代码的耦合性就大大提高了。并且导致NASA失去了主动性。NASA这个甲方才不愿意这么干。 所以NASA决定采用SPI规则来约束供应商。

NASA给出了关键性的技术指标。
假设目前有SpaceX 和 Blue Origin两家公司中标 去完成着陆器的设计。 这两家公司需要遵循NASA给定的SPI规则进行设计。
两家公司月球着陆器产品设计完成后,NASA只需要按照SPI规则去加载对应的产品即可。

下图可以看出,虽然两家公司的火箭设计不一样,但是他们的接口都符合NASA指定的接口规则。
这样NASA就可以很轻松的集成两家公司的方案,如果觉得SpaceX的方案不好,直接拿Blue Origin的方案无缝替代即可。

# JavaSPI 代码示例 (使用Maven项目演示)
①、NASA先定义SPI接口 并发布到本地仓库供SpaceX和BlueOrigin实现
项目结构:

pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nasa</groupId> <artifactId>SPI-interface</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
复制成功!
maven打包安装到本地仓库

打包好的jar名称

SPI接口:
package com.nasa; /** * NASA提供的SPI接口 * */ public interface LandingOnTheMoon { /** * 着陆方法 */ void land(); }
复制成功!
SPI加载实现(稍后会详细介绍):
package com.nasa; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; /** * 加载具体的服务实现 * */ public class LandingOnTheMoonLoader { private static volatile LandingOnTheMoonLoader LOADER; private final LandingOnTheMoon landingOnTheMoon; private final List<LandingOnTheMoon> landingOnTheMoons; /** * 加载服务 * */ private LandingOnTheMoonLoader() { ServiceLoader<LandingOnTheMoon> loader = ServiceLoader.load(LandingOnTheMoon.class); List<LandingOnTheMoon> list = new ArrayList<>(); for (LandingOnTheMoon landingOnTheMoon : loader) { list.add(landingOnTheMoon); } landingOnTheMoons = list; if (!list.isEmpty()) { // 取第一个 landingOnTheMoon = list.get(0); } else { landingOnTheMoon = null; } } /** * LandingOnTheMoonLoader 单例加载 * */ public static LandingOnTheMoonLoader getLOADER() { if (LOADER == null) { synchronized (LandingOnTheMoonLoader.class) { if (LOADER == null) { LOADER = new LandingOnTheMoonLoader(); } } } return LOADER; } public void land(){ if(landingOnTheMoons.isEmpty()){ System.out.println("LandingOnTheMoon服务未加载!"); }else { LandingOnTheMoon landingOnTheMoon = landingOnTheMoons.get(0); landingOnTheMoon.land(); } } }
复制成功!
②、SpaceX实现自己的登陆月球方案
先引入NASA 指定的SPI接口
pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.spacex</groupId> <artifactId>SpaceX</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.nasa</groupId> <artifactId>SPI-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
复制成功!
SpaceX的登陆月球实现:
package com.spacex; import com.nasa.LandingOnTheMoon; /** * SpaceX实现的月球着陆方案 * */ public class SpaceXLand implements LandingOnTheMoon { @Override public void land() { System.out.println("SpaceX landing on the moon with StarShip~"); } }
复制成功!
SpaceX的登陆月球实现配置信息:
注意: Maven项目中 META-INF/services/SPI接口全限定名 需要放在resources目录下

内容为SpaceX实现类的全限定名 :

③、BlueOrigin实现自己的登陆月球方案
先引入NASA 指定的SPI接口
pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.blue</groupId> <artifactId>BlueOrigin</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.nasa</groupId> <artifactId>SPI-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
复制成功!
BlueOrigin的登陆月球实现:
package com.blue; import com.nasa.LandingOnTheMoon; /** * 蓝色起源实现的月球着陆方案 * */ public class BlueOriginLand implements LandingOnTheMoon { @Override public void land() { System.out.println("BlueOrigin landing on the moon with NewGlenn~"); } }
复制成功!
BlueOrigin的登陆月球实现配置信息:
Maven项目中 META-INF/services/SPI接口全限定名 需要放在resources目录下

内容为BlueOrigin实现类的全限定名 :

编写测试代码:
新建一个Maven项目
pom文件中引入SpaceX的实现坐标
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nasa</groupId> <artifactId>NASA-LANDING</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--使用蓝色起源的着陆实现--> <!-- <dependency> <groupId>com.blue</groupId> <artifactId>BlueOrigin</artifactId> <version>1.0-SNAPSHOT</version> </dependency>--> <!--使用SpaceX的着陆实现--> <dependency> <groupId>com.spacex</groupId> <artifactId>SpaceX</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
复制成功!
package com.nasa; public class LandingShow { public static void main(String[] args) { LandingOnTheMoonLoader service = LandingOnTheMoonLoader.getLOADER(); service.land(); } }
复制成功!
执行结果:
SpaceX landing on the moon with StarShip~
复制成功!
此时如果NASA觉得SpaceX的方案不行,需要更换BlueOrigin的方案,操作起来非常简单,只需要注释掉SpaceX的坐标,添加BlueOrigin的坐标,业务代码完全不用改。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nasa</groupId> <artifactId>NASA-LANDING</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--使用蓝色起源的着陆实现--> <dependency> <groupId>com.blue</groupId> <artifactId>BlueOrigin</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--使用SpaceX的着陆实现--> <!-- <dependency>--> <!-- <groupId>com.spacex</groupId>--> <!-- <artifactId>SpaceX</artifactId>--> <!-- <version>1.0-SNAPSHOT</version>--> <!-- </dependency>--> </dependencies> </project>
复制成功!
执行结果:
BlueOrigin landing on the moon with NewGlenn~
复制成功!
# JavaSPI 机制的核心-ServiceLoader
上面代码中我们使用ServiceLoader 去加载具体的服务实现。
ServiceLoader 是从JDK1.6 开始提供的一个类,用于加载服务提供者。

我们看下源码:
其中 String PREFIX = "META-INF/services/";
这个就是JDK的SPI功能规定的具体服务实现的配置信息文件所在的目录 META-INF/services/
JDK的SPI规定 服务实现者需要在 META-INF/services/ 目录下新建文件名为 SPI接口全限定类名的文件
文件内容为 服务实现者需要被加载的具体类的全限定类名

我们上面代码在加载服务的时候调用了ServiceLoader.load(LandingOnTheMoon.class);方法
ServiceLoader<LandingOnTheMoon> loader = ServiceLoader.load(LandingOnTheMoon.class);
复制成功!
看下这个方法的源码:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
复制成功!
可以看到这个方法 只是获取了当前线程的上下文类加载器 然后又调用了一个load的重载方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
复制成功!
在重载的load方法中又调用了ServiceLoader的私有构造器
private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); }
复制成功!
然后调用 reload();方法
public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
复制成功!
reload();方法里面 调用了 new LazyIterator(service, loader);
具体的类加载行为就是在LazyIterator内部类中完成的。
这里的 lookupIterator = new LazyIterator(service, loader);
创建LazyIterator 懒加载迭代器对象,并没有马上去加载SPI的具体服务。
当我们调用 for (LandingOnTheMoon landingOnTheMoon : loader) {...} 循环时,会触发迭代器,
在ServiceLoader中,迭代器是由Iterable接口的iterator()方法提供的。
当第一次调用iterator()时,如果之前没有创建过迭代器,它会创建一个新的LazyIterator实例。
对应下面的源码:
// lookupIterator 在 调用reload 方法时 创建过了 private LazyIterator lookupIterator; // 存储已经加载过的服务提供者实例 在 调用reload 方法时 调用了providers.clear(); 清空了这个集合 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); public Iterator<S> iterator() { return new Iterator<S>() { // 第一次调用时 providers 是空集合 会调用 lookupIterator 也就是 LazyIterator 的实例 Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }
复制成功!
下面看 LazyIterator 的实现 具体的类加载就在这里:
LazyIterator 的 hasNext 和 next方法 中判断了 acc 是否是null
其中 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
这段代码是在 ServiceLoader初始化的时候执行的 主要用于判断 当前Java运行环境中是否存在安全管理器(SecurityManager),
默认情况下不会配置,如果配置了SecurityManager,某些敏感操作(比如文件访问、网络连接等)可能会受到安全策略的限制或需要相应的权限检查。
这个不重要,我们还是看具体的方法 hasNextService 和 nextService方法。
public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } }
复制成功!
LazyIterator 的 hasNextService 和 nextService方法
终于到了加载具体服务的地方了。
// 迭代器 Iterator<String> pending = null; private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 这里获取 SPI配置文件的文件名 包含 META-INF/services/ String fullName = PREFIX + service.getName(); // 获取配置 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } // 迭代器如果为空 或者没有值 while ((pending == null) || !pending.hasNext()) { // 配置内也没有值 if (!configs.hasMoreElements()) { return false; } // 解析配置内的 具体服务名称 pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); // nextName 在hasNextService中已经被赋值了 String cn = nextName; nextName = null; Class<?> c = null; try { // 关键点终于来了 最终还是利用Java的反射机制 完成类的加载 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } // 判断下 加载的 Class 的类型 是否实现了 给定的 SPI接口 拿上面NASA登月的例子看,这里的 service 就是 LandingOnTheMoon.class if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { // 通过反射实例化 SPI的实现类 并且转换成 SPI接口类型 S p = service.cast(c.newInstance()); // 保存到 LinkedHashMap<String,S> providers 缓存中 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
复制成功!
看 具体的parse 方法(解析服务实现者提供的配置文件)
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; // 保存解析配置文件内的 全限定类名 ArrayList<String> names = new ArrayList<>(); try { in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; // 一行一行的读取配置信息 并将每行解析出的全限定类名 保存到 names // parseLine 方法不用看了 就是解析字符串的 while ((lc = parseLine(service, u, r, lc, names)) >= 0); } catch (IOException x) { fail(service, "Error reading configuration file", x); } finally { try { if (r != null) r.close(); if (in != null) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file", y); } } // 返回 拥有所有实现SPI接口的服务提供者全限定类名集合的迭代器 return names.iterator(); }
复制成功!
绕了半天 通过源码发现 最终类的加载还是通过 Java反射机制实现的。
那了解了ServiceLoader的具体实现之后,我们也可以实现一个自己的 服务加载器。
ServiceLoader只不过在实现了核心的SPI服务加载功能的基础上增加了一些额外的功能,比如通过LazyIterator 实现延迟加载,
可以调用reload() 在应用运行时实现服务的动态加载(不用重启应用就能加载服务),访问控制(比如判断 当前Java运行环境中是否存在安全管理器),还有按照加载顺序保存已加载的服务等等。
不过ServiceLoader也是线程不安全的这点需要注意。
# 实现自己的ServiceLoader
我们实现一个最简单的ServiceLoader只考虑加载服务的功能,其他的安全、性能之类的都不考虑。
我们也仿照ServiceLoader类的结构,和SPI配置类规定的路径来实现。
package com.nasa; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.*; public class MySimpleServiceLoader<T> { // 直接copy ServiceLoader的规则 private static final String PREFIX = "META-INF/services/"; // 定义的SPI接口 private final Class<T> service; // 类加载器 private final ClassLoader loader; // 保存已加载的服务实例 private final LinkedHashMap<String, T> providers = new LinkedHashMap<>(); // 构造方法T private MySimpleServiceLoader(Class<T> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; // 加载服务 realLoad(); } public static <T> MySimpleServiceLoader<T> load(Class<T> service) { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); return new MySimpleServiceLoader<>(service, contextClassLoader); } private void realLoad() { String fullName = PREFIX + service.getName(); try { // 获取配置文件 Enumeration<URL> resources = loader.getResources(fullName); // 读取配置文件 while (resources.hasMoreElements()) { URL url = resources.nextElement(); InputStream inputStream = url.openStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); // 我们就不做解析错误之类的逻辑了 默认给的配置文件都是对的 String name = reader.readLine(); // 通过反射创建对象 Class<?> aClass = Class.forName(name, false, loader); // 判断 服务实现类是否实现了 给定的SPI接口 if (service.isAssignableFrom(aClass)) { T cast = service.cast(aClass.newInstance()); providers.put(name, cast); } } } catch (Exception e) { System.out.println("出现异常!"); e.printStackTrace(); } } // 返回加载好的 服务实现 public LinkedHashMap<String, T> getProviders() { return providers; } }
复制成功!
使用自定义的服务加载器 MySimpleServiceLoader
package com.nasa; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; /** * 加载具体的服务实现 * */ public class LandingOnTheMoonLoader { private static volatile LandingOnTheMoonLoader LOADER; private final LandingOnTheMoon landingOnTheMoon; private final List<LandingOnTheMoon> landingOnTheMoons = new ArrayList<>(); /** * 加载服务 * */ private LandingOnTheMoonLoader() { // 通过自定义的服务加载器 去加载服务实现 MySimpleServiceLoader<LandingOnTheMoon> mySimpleServiceLoader = MySimpleServiceLoader.load(LandingOnTheMoon.class); LinkedHashMap<String, LandingOnTheMoon> providers = mySimpleServiceLoader.getProviders(); providers.forEach((k,v)->{ System.out.println(k); landingOnTheMoons.add(v); }); if (!landingOnTheMoons.isEmpty()) { // 取第一个 landingOnTheMoon = landingOnTheMoons.get(0); } else { landingOnTheMoon = null; } } /** * LandingOnTheMoonLoader 单例加载 * */ public static LandingOnTheMoonLoader getLOADER() { if (LOADER == null) { synchronized (LandingOnTheMoonLoader.class) { if (LOADER == null) { LOADER = new LandingOnTheMoonLoader(); } } } return LOADER; } public void land(){ if(landingOnTheMoons.isEmpty()){ System.out.println("LandingOnTheMoon服务未加载!"); }else { LandingOnTheMoon landingOnTheMoon = landingOnTheMoons.get(0); landingOnTheMoon.land(); } } }
复制成功!
测试:
public static void main(String[] args) { LandingOnTheMoonLoader service = LandingOnTheMoonLoader.getLOADER(); service.land(); }
复制成功!
使用SpaceX的实现,结果如下:
com.spacex.SpaceXLand SpaceX landing on the moon with StarShip~
复制成功!
# Java中还有哪些SPI实现?
1、JDBC (Java Database Connectivity): JDBC驱动的加载就是SPI的一个经典应用。各个数据库厂商提供自己的JDBC驱动实现,应用程序无需直接引用具体驱动的实现类,只需将驱动JAR包放入类路径,Java SPI机制会自动发现并加载合适的驱动。
2、日志框架: 如SLF4J (Simple Logging Facade for Java) 和Logback、Log4j等,它们利用SPI机制来发现和加载具体的日志实现。用户可以根据需要选择或更换日志实现,而无需修改应用程序代码。
3、Spring框架: 虽然Spring框架本身更倾向于使用其自身的bean工厂和依赖注入机制来管理组件和服务,但Spring也支持SPI机制,特别是在某些扩展点和与第三方库集成时。
4、Dubbo: Apache Dubbo是一个高性能的RPC框架,它大量使用SPI机制来实现其插件体系,允许用户轻松替换或扩展序列化、负载均衡、集群容错等核心组件。
5、JNDI (Java Naming and Directory Interface): JNDI服务提供者也是通过SPI机制注册和发现的,允许应用程序访问不同的命名和目录服务。
6、JAX-WS (Java API for XML Web Services) 和 JAX-RS (Java API for RESTful Web Services): 这些Java Web服务技术框架使用SPI来发现和加载实现特定功能的服务提供者,比如SOAP绑定和HTTP连接器。
7、JavaBeans Activation Framework (JAF): 用于处理MIME类型的邮件附件等,通过SPI来发现数据处理器。
8、JavaMail: 用于发送和接收电子邮件的API,利用SPI来加载传输协议和其他服务提供者。
...