diff --git a/README.md b/README.md index 8a8830c3..fb992fae 100755 --- a/README.md +++ b/README.md @@ -54,10 +54,11 @@ * [为什么 Java 中只有值传递?](docs/java/basis/why-there-only-value-passing-in-java.md) * [Java 序列化详解](docs/java/basis/serialization.md) * [泛型&序列化详解](docs/java/basis/generics-and-wildcards.md) -* [反射机制详解](docs/java/basis/reflection.md) +* [Java 反射机制详解](docs/java/basis/reflection.md) * [Java 代理模式详解](docs/java/basis/proxy.md) * [BigDecimal 详解](docs/java/basis/bigdecimal.md) * [Java 魔法类 Unsafe 详解](docs/java/basis/unsafe.md) +* [Java SPI 机制详解](docs/java/basis/spi.md) ### 集合 diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 9c74761e..980202ff 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -56,6 +56,7 @@ export const sidebarConfig = defineSidebarConfig({ "proxy", "bigdecimal", "unsafe", + "spi", ], }, ], diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md index 438cba4b..7f75cc23 100644 --- a/docs/java/basis/java-basic-questions-02.md +++ b/docs/java/basis/java-basic-questions-02.md @@ -117,7 +117,7 @@ public class Student { **区别** : -- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。 +- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。 - 一个类只能继承一个类,但是可以实现多个接口。 - 接口中的成员变量只能是 `public static final` 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。 diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index bd67b7ac..b8fb6522 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -323,22 +323,23 @@ printArray( stringArray ); ## 反射 -### 何为反射? +关于反射的详细解读,请看这篇文章 [Java 反射机制详解](./reflection.md) 。 -如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 +### 何谓反射? -反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 +如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 -### 反射机制优缺点 +### 反射的优缺点? -- **优点** : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 -- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) +反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。 -### 反射的应用场景 +不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。 -像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 +相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。 -但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 +### 反射的应用场景? + +像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 **这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** @@ -373,7 +374,9 @@ public class DebugInvocationHandler implements InvocationHandler { ## 注解 -`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。 +### 何谓注解? + +`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些 注解本质是一个继承了`Annotation` 的特殊接口: @@ -389,12 +392,51 @@ public interface Override extends Annotation{ } ``` +JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`),同时,我们还可以自定义注解。 + +### 注解的解析方法有哪几种? + 注解只有被解析之后才会生效,常见的解析方法有两种: - **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value` 、`@Component`)都是通过反射来进行处理的。 -JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`),同时,我们还可以自定义注解。 +## SPI + +关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](./spi.md) 。 + +### 何谓 SPI? + +SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 + +SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + +很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/22e1830e0b0e4115a882751f6c417857tplv-k3u1fbpfcp-zoom-1.jpeg) + +### SPI 和 API 有什么区别? + +**那 SPI 和 API 有啥区别?** + +说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png) + +一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 + +当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。 + +当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。 + +举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 + +### SPI 的优缺点? + +通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: + +- 遍历加载所有的实现类,这样效率还是相对较低的; +- 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 ## I/O @@ -472,3 +514,6 @@ Java IO 流共涉及 40 多个类,这些类看上去很杂乱,但实际上 问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** 回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 + + + diff --git a/docs/java/basis/reflection.md b/docs/java/basis/reflection.md index c70249ac..4f93d94b 100644 --- a/docs/java/basis/reflection.md +++ b/docs/java/basis/reflection.md @@ -1,5 +1,5 @@ --- -title: 反射机制详解 +title: Java 反射机制详解 category: Java tag: - Java基础 diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md index 5f9acd2f..ae0616a1 100644 --- a/docs/java/basis/serialization.md +++ b/docs/java/basis/serialization.md @@ -1,5 +1,5 @@ --- -title: Java 序列化详解 +title: Java 序列化详解 category: Java tag: - Java基础 diff --git a/docs/java/basis/spi.md b/docs/java/basis/spi.md new file mode 100644 index 00000000..cbda0865 --- /dev/null +++ b/docs/java/basis/spi.md @@ -0,0 +1,559 @@ +--- +title: Java SPI 机制详解 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: Java SPI机制 + - name: description + content: SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 +--- + +> 本文来自 [Kingshion](https://github.com/jjx0708) 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) 。 + +在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。 + +为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:**为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。** + +## SPI 介绍 + +### 何谓 SPI? + +SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 + +SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + +很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/22e1830e0b0e4115a882751f6c417857tplv-k3u1fbpfcp-zoom-1.jpeg) + +### SPI 和 API 有什么区别? + +**那 SPI 和 API 有啥区别?** + +说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png) + +一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 + +当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。 + +当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。 + +举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 + +## 实战演示 + +Spring 框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),但是 SLF4J 的具体实现可以有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/image-20220723213306039-165858318917813.png) + +这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。 + +### Service Provider Interface + +新建一个 Java 项目 `service-provider-interface` 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。) + +``` +│ service-provider-interface.iml +│ +├─.idea +│ │ .gitignore +│ │ misc.xml +│ │ modules.xml +│ └─ workspace.xml +│ +└─src + └─edu + └─jiangxuan + └─up + └─spi + Logger.java + LoggerService.java + Main.class +``` + +新建 `Logger` 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。 + +```java +package edu.jiangxuan.up.spi; + +public interface Logger { + void info(String msg); + void debug(String msg); +} +``` + +接下来就是 `LoggerService` 类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。 + +```java +package edu.jiangxuan.up.spi; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +public class LoggerService { + private static final LoggerService SERVICE = new LoggerService(); + + private final Logger logger; + + private final List loggerList; + + private LoggerService() { + ServiceLoader loader = ServiceLoader.load(Logger.class); + List list = new ArrayList<>(); + for (Logger log : loader) { + list.add(log); + } + // LoggerList 是所有 ServiceProvider + loggerList = list; + if (!list.isEmpty()) { + // Logger 只取一个 + logger = list.get(0); + } else { + logger = null; + } + } + + public static LoggerService getService() { + return SERVICE; + } + + public void info(String msg) { + if (logger == null) { + System.out.println("info 中没有发现 Logger 服务提供者"); + } else { + logger.info(msg); + } + } + + public void debug(String msg) { + if (loggerList.isEmpty()) { + System.out.println("debug 中没有发现 Logger 服务提供者"); + } + loggerList.forEach(log -> log.debug(msg)); + } +} +``` + +新建 `Main` 类(服务使用者,调用方),启动程序查看结果。 + +```java +package org.spi.service; + +public class Main { + public static void main(String[] args) { + LoggerService service = LoggerService.getService(); + + service.info("Hello SPI"); + service.debug("Hello SPI"); + } +} +``` + +程序结果: + +> info 中没有发现 Logger 服务提供者 +> debug 中没有发现 Logger 服务提供者 + +此时我们只是空有接口,并没有为 `Logger` 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。 + +你可以使用命令或者直接使用 IDEA 将整个程序直接打包成 jar 包。 + +### Service Provider + +接下来新建一个项目用来实现 `Logger` 接口 + +新建项目 `service-provider` 目录结构如下: + +``` +│ service-provider.iml +│ +├─.idea +│ │ .gitignore +│ │ misc.xml +│ │ modules.xml +│ └─ workspace.xml +│ +├─lib +│ service-provider-interface.jar +| +└─src + ├─edu + │ └─jiangxuan + │ └─up + │ └─spi + │ └─service + │ Logback.java + │ + └─META-INF + └─services + edu.jiangxuan.up.spi.Logger + +``` + +新建 `Logback` 类 + +```java +package edu.jiangxuan.up.spi.service; + +import edu.jiangxuan.up.spi.Logger; + +public class Logback implements Logger { + @Override + public void info(String s) { + System.out.println("Logback info 打印日志:" + s); + } + + @Override + public void debug(String s) { + System.out.println("Logback debug 打印日志:" + s); + } +} + +``` + +将 `service-provider-interface` 的 jar 导入项目中。 + +新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/523d5e25198444d3b112baf68ce49daetplv-k3u1fbpfcp-watermark.png) + +再点击 OK 。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/f4ba0aa71e9b4d509b9159892a220850tplv-k3u1fbpfcp-watermark.png) + +接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。 + +实现 `Logger` 接口,在 `src` 目录下新建 `META-INF/services` 文件夹,然后新建文件 `edu.jiangxuan.up.spi.Logger` (SPI 的全类名),文件里面的内容是:`edu.jiangxuan.up.spi.service.Logback` (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。 + +**这是 JDK SPI 机制 ServiceLoader 约定好的标准。** + +这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 `META-INF` 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。 + +所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有过个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。 + +接下来同样将 `service-provider` 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。 + +### 效果展示 + +为了更直观的展示效果,我这里再新建一个专门用来测试的工程项目:`java-spi-test` + +然后先导入 `Logger` 的接口 jar 包,再导入具体的实现类的 jar 包。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/image-20220723215812708-165858469599214.png) + +新建 Main 方法测试: + +```java +package edu.jiangxuan.up.service; + +import edu.jiangxuan.up.spi.LoggerService; + +public class TestJavaSPI { + public static void main(String[] args) { + LoggerService loggerService = LoggerService.getService(); + loggerService.info("你好"); + loggerService.debug("测试Java SPI 机制"); + } +} +``` + +运行结果如下: + +> Logback info 打印日志:你好 +> Logback debug 打印日志:测试 Java SPI 机制 + +说明导入 jar 包中的实现类生效了。 + +如果我们不导入具体的实现类的 jar 包,那么此时程序运行的结果就会是: + +> info 中没有发现 Logger 服务提供者 +> debug 中没有发现 Logger 服务提供者 + +通过使用 SPI 机制,可以看出服务(`LoggerService`)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 `service-provider` 项目中针对 `Logger` 接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗? + +如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。 + +那么接下来我们具体来说说 Java SPI 工作的重点原理—— **ServiceLoader** 。 + +## ServiceLoader + +### ServiceLoader 具体实现 + +想要使用 Java 的 SPI 机制是需要依赖 `ServiceLoader` 来实现的,那么我们接下来看看 `ServiceLoader` 具体是怎么做的: + +`ServiceLoader` 是 JDK 提供的一个工具类, 位于`package java.util;`包下。 + +``` +A facility to load implementations of a service. +``` + +这是 JDK 官方给的注释:**一种加载服务实现的工具。** + +再往下看,我们发现这个类是一个 `final` 类型的,所以是不可被继承修改,同时它实现了 `Iterable` 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。 + +```java +public final class ServiceLoader implements Iterable{ xxx...} +``` + +可以看到一个熟悉的常量定义: + +`private static final String PREFIX = "META-INF/services/";` + +下面是 `load` 方法:可以发现 `load` 方法支持两种重载后的入参; + +```java +public static ServiceLoader load(Class service) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return ServiceLoader.load(service, cl); +} + +public static ServiceLoader load(Class service, + ClassLoader loader) { + return new ServiceLoader<>(service, loader); +} + +private ServiceLoader(Class 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(); +} + +public void reload() { + providers.clear(); + lookupIterator = new LazyIterator(service, loader); +} +``` + +根据代码的调用顺序,在 `reload()` 方法中是通过一个内部类 `LazyIterator` 实现的。先继续往下面看。 + +`ServiceLoader` 实现了 `Iterable` 接口的方法后,具有了迭代的能力,在这个 `iterator` 方法被调用时,首先会在 `ServiceLoader` 的 `Provider` 缓存中进行查找,如果缓存中没有命中那么则在 `LazyIterator` 中进行查找。 + +```java + +public Iterator iterator() { + return new Iterator() { + + Iterator> knownProviders + = providers.entrySet().iterator(); + + public boolean hasNext() { + if (knownProviders.hasNext()) + return true; + return lookupIterator.hasNext(); // 调用 LazyIterator + } + + public S next() { + if (knownProviders.hasNext()) + return knownProviders.next().getValue(); + return lookupIterator.next(); // 调用 LazyIterator + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + }; +} +``` + +在调用 `LazyIterator` 时,具体实现如下: + +```java + +public boolean hasNext() { + if (acc == null) { + return hasNextService(); + } else { + PrivilegedAction action = new PrivilegedAction() { + public Boolean run() { + return hasNextService(); + } + }; + return AccessController.doPrivileged(action, acc); + } +} + +private boolean hasNextService() { + if (nextName != null) { + return true; + } + if (configs == null) { + try { + //通过PREFIX(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; +} + + +public S next() { + if (acc == null) { + return nextService(); + } else { + PrivilegedAction action = new PrivilegedAction() { + public S run() { + return nextService(); + } + }; + return AccessController.doPrivileged(action, acc); + } +} + +private S nextService() { + if (!hasNextService()) + throw new NoSuchElementException(); + String cn = nextName; + nextName = null; + Class c = null; + try { + c = Class.forName(cn, false, loader); + } catch (ClassNotFoundException x) { + fail(service, + "Provider " + cn + " not found"); + } + if (!service.isAssignableFrom(c)) { + fail(service, + "Provider " + cn + " not a subtype"); + } + try { + S p = service.cast(c.newInstance()); + providers.put(cn, p); + return p; + } catch (Throwable x) { + fail(service, + "Provider " + cn + " could not be instantiated", + x); + } + throw new Error(); // This cannot happen +} +``` + +可能很多人看这个会觉得有点复杂,没关系,我这边实现了一个简单的 `ServiceLoader` 的小模型,流程和原理都是保持一致的,可以先从自己实现一个简易版本的开始学: + +### 自己实现一个 ServiceLoader + +我先把代码贴出来: + +```java +package edu.jiangxuan.up.service; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +public class MyServiceLoader { + + // 对应的接口 Class 模板 + private final Class service; + + // 对应实现类的 可以有多个,用 List 进行封装 + private final List providers = new ArrayList<>(); + + // 类加载器 + private final ClassLoader classLoader; + + // 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。 + public static MyServiceLoader load(Class service) { + return new MyServiceLoader<>(service); + } + + // 构造方法私有化 + private MyServiceLoader(Class service) { + this.service = service; + this.classLoader = Thread.currentThread().getContextClassLoader(); + doLoad(); + } + + // 关键方法,加载具体实现类的逻辑 + private void doLoad() { + try { + // 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名 + Enumeration urls = classLoader.getResources("META-INF/services/" + service.getName()); + // 挨个遍历取到的文件 + while (urls.hasMoreElements()) { + // 取出当前的文件 + URL url = urls.nextElement(); + System.out.println("File = " + url.getPath()); + // 建立链接 + URLConnection urlConnection = url.openConnection(); + urlConnection.setUseCaches(false); + // 获取文件输入流 + InputStream inputStream = urlConnection.getInputStream(); + // 从文件输入流获取缓存 + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + // 从文件内容里面得到实现类的全类名 + String className = bufferedReader.readLine(); + + while (className != null) { + // 通过反射拿到实现类的实例 + Class clazz = Class.forName(className, false, classLoader); + // 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例 + if (service.isAssignableFrom(clazz)) { + Constructor constructor = (Constructor) clazz.getConstructor(); + S instance = constructor.newInstance(); + // 把当前构造的实例对象添加到 Provider的列表里面 + providers.add(instance); + } + // 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。 + className = bufferedReader.readLine(); + } + } + } catch (Exception e) { + System.out.println("读取文件异常。。。"); + } + } + + // 返回spi接口对应的具体实现类列表 + public List getProviders() { + return providers; + } +} +``` + +关键信息基本已经通过代码注释描述出来了, + +主要的流程就是: + +1. 通过 URL 工具类从 jar 包的 `/META-INF/services` 目录下面找到对应的文件, +2. 读取这个文件的名称找到对应的 spi 接口, +3. 通过 `InputStream` 流将文件里面的具体实现类的全类名读取出来, +4. 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象, +5. 将构造出来的实例对象添加到 `Providers` 的列表中。 + +## 总结 + +其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:**我们按照规定将要暴露对外使用的具体实现类在 `META-INF/services/` 文件下声明。** + +另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框的理解。 + +通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: + +1. 遍历加载所有的实现类,这样效率还是相对较低的; +2. 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 \ No newline at end of file