JAVA拾遗--关于SPI机制
JDK提供的SPI(Service Provider Interface)机制,可能很多人不太熟悉,因为这个机制是针对厂商或者插件的,也可以在一些框架的扩展中看到。其核心类java.util.ServiceLoader
可以在jdk1.8的文档中看到详细的介绍。虽然不太常见,但并不代表它不常用,恰恰相反,你无时无刻不在用它。玄乎了,莫急,思考一下你的项目中是否有用到第三方日志包,是否有用到数据库驱动?其实这些都和SPI有关。再来思考一下,现代的框架是如何加载日志依赖,加载数据库驱动的,你可能会对class.forName(“com.mysql.jdbc.Driver”)这段代码不陌生,这是每个java初学者必定遇到过的,但如今的数据库驱动仍然是这样加载的吗?你还能找到这段代码吗?这一切的疑问,将在本篇文章结束后得到解答。
首先介绍SPI机制是个什么东西
实现一个自定义的SPI
1 项目结构
- invoker是我们的用来测试的主项目。
- interface是针对厂商和插件商定义的接口项目,只提供接口,不提供实现。
- good-printer,bad-printer分别是两个厂商对interface的不同实现,所以他们会依赖于interface项目。 这个简单的demo就是让大家体验,在不改变invoker代码,只更改依赖的前提下,切换interface的实现厂商。
2 interface模块
2.1 moe.cnkirito.spi.api.Printer
public interface Printer { void print(); }
interface只定义一个接口,不提供实现。规范的制定方一般都是比较牛叉的存在,这些接口通常位于java,javax前缀的包中。这里的Printer就是模拟一个规范接口。
3 good-printer模块
3.1 good-printer\pom.xml
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>
规范的具体实现类必然要依赖规范接口
3.2 moe.cnkirito.spi.api.GoodPrinter
public class GoodPrinter implements Printer {public void print() {System.out.println("你是个好人~");} }
作为Printer规范接口的实现一
3.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
moe.cnkirito.spi.api.GoodPrinter
这里需要重点说明,每一个SPI接口都需要在自己项目的静态资源目录中声明一个services文件,文件名为实现规范接口的类名全路径,在此例中便是moe.cnkirito.spi.api.Printer
,在文件中,则写上一行具体实现类的全路径,在此例中便是moe.cnkirito.spi.api.GoodPrinter
。 这样一个厂商的实现便完成了。
4 bad-printer模块
我们在按照和good-printer模块中定义的一样的方式,完成另一个厂商对Printer规范的实现。
4.1 bad-printer\pom.xml
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>
4.2 moe.cnkirito.spi.api.BadPrinter
public class BadPrinter implements Printer {public void print() {System.out.println("我抽烟,喝酒,蹦迪,但我知道我是好女孩~");} }
4.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
moe.cnkirito.spi.api.BadPrinter
这样,另一个厂商的实现便完成了。
5 invoker模块
这里的invoker便是我们自己的项目了。如果一开始我们想使用厂商good-printer的Printer实现,是需要将其的依赖引入。
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>moe.cnkirito</groupId><artifactId>good-printer</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>
5.1 编写调用主类
public class MainApp {public static void main(String[] args) {ServiceLoader<Printer> printerLoader = ServiceLoader.load(Printer.class);for (Printer printer : printerLoader) {printer.print();}} }
ServiceLoader是java.util
提供的用于加载固定类路径下文件的一个加载器,正是它加载了对应接口声明的实现类。
5.2 打印结果1
你是个好人~
如果在后续的方案中,想替换厂商的Printer实现,只需要将依赖更换
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>moe.cnkirito</groupId><artifactId>bad-printer</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>
调用主类无需变更代码,这符合开闭原则
5.3 打印结果2
我抽烟,喝酒,蹦迪,但我知道我是好女孩~
是不是很神奇呢?这一切对于调用者来说都是透明的,只需要切换依赖即可!
SPI在实际项目中的应用
先总结下有什么新知识,resources/META-INF/services下的文件似乎我们之前没怎么接触过,ServiceLoader也没怎么接触过。那么现在我们打开自己项目的依赖,看看有什么发现。
1.在mysql-connector-java-xxx.jar中发现了META-INF\services\java.sql.Driver文件,里面只有两行记录:
com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver
我们可以分析出,java.sql.Driver是一个规范接口,com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
则是mysql-connector-java-xxx.jar对这个规范的实现接口。
2.在jcl-over-slf4j-xxxx.jar中发现了META-INF\services\org.apache.commons.logging.LogFactory文件,里面只有一行记录:
org.apache.commons.logging.impl.SLF4JLogFactory
相信不用我赘述,大家都能理解这是什么含义了
3.更多的还有很多,有兴趣可以自己翻一翻项目路径下的那些jar包 既然说到了数据库驱动,索性再多说一点,还记得一道经典的面试题:class.forName(“com.mysql.jdbc.Driver”)到底做了什么事?
先思考下:自己会怎么回答?
都知道class.forName与类加载机制有关,会触发执行com.mysql.jdbc.Driver类中的静态方法,从而使主类加载数据库驱动。如果再追问,为什么它的静态块没有自动触发?可答:因为数据库驱动类的特殊性质,JDBC规范中明确要求Driver类必须向DriverManager注册自己,导致其必须由class.forName手动触发,这可以在java.sql.Driver中得到解释。完美了吗?还没,来到最新的DriverManager源码中,可以看到这样的注释,翻译如下:
DriverManager
类的方法 getConnection 和 getDrivers 已经得到提高以支持 Java Standard Edition Service Provider 机制。 JDBC 4.0 Drivers 必须包括META-INF/services/java.sql.Driver
文件。此文件包含java.sql.Driver
的 JDBC 驱动程序实现的名称。例如,要加载 my.sql.Driver 类,META-INF/services/java.sql.Driver 文件需要包含下面的条目: my.sql.Driver 应用程序不再需要使用Class.forName()
显式地加载 JDBC 驱动程序。当前使用Class.forName()
加载 JDBC 驱动程序的现有程序将在不作修改的情况下继续工作。
可以发现,Class.forName已经被弃用了,所以,这道题目的最佳回答,应当是和面试官牵扯到JAVA中的SPI机制,进而聊聊加载驱动的演变历史。 java.sql.DriverManager
public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null; }
当然那,本节的内容还是主要介绍SPI,驱动这一块这是引申而出,如果不太理解,可以多去翻一翻jdk1.8中Driver和DriverManager的源码,相信会有不小的收获。
这里我的理解与作者的不大相同
先看下com.mysql.jdbc.Driver
package com.mysql.jdbc;import java.sql.DriverManager; import java.sql.SQLException;public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}} }
逻辑很简单,在静态块中往DriverManager
注册自己.那么为什么它的静态块没有自动触发,这个回答和作者不太一样了. 我的理解是JVM只要加载类那么一定会执行静态块中的代码,换句话说没执行也就是这个类根本没加载进内存.因为ClassLoader是按需加载模式,这也就是你用了一个jar包,可以不全部引入他的依赖原因所在,只要不触发对应的类加载,那么即使没有引入jar,也不会报ClassNotFoundException
. 那么Class.forName
作用是什么?作用是触发类加载,告诉JVM我这个系统运行需要加载这个类,那么JVM加载时会自动触发相应的静态块代码执行. 那么SPI
与Class.forName
有什么不同?本质都是触发加载,实例化出对象,只是SPI对于开发者来说是被动,Class.forName是主动.
SPI在扩展方面的应用
SPI不仅仅是为厂商指定的标准,同样也为框架扩展提供了一个思路。框架可以预留出SPI接口,这样可以在不侵入代码的前提下,通过增删依赖来扩展框架。前提是,框架得预留出核心接口,也就是本例中interface模块中类似的接口,剩下的适配工作便留给了开发者。
例如我的上一篇文章 https://www.cnkirito.moe/2017/11/07/spring-cloud-sleuth/ 中介绍的motan中Filter的扩展,便是采用了SPI机制,熟悉这个设定之后再回头去了解一些框架的SPI扩展就不会太陌生了。
JAVA拾遗--关于SPI机制相关推荐
- Java API 之 SPI机制
SPI SPI全称是service provider interface,是Java定义的一套服务发现机制,如图: 调用方只需要面向接口,接口的实现由第三方自己去实现,服务启动的时候会自动去发现该服务 ...
- Java是如何实现自己的SPI机制的? JDK源码(一)
注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...
- java spi机制_Java是如何实现自己的SPI机制的? JDK源码(一)
注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...
- Java中的ClassLoader和SPI机制
深入探讨 Java 类加载器 成富是著名的Java专家,在IBM技术网站发表很多Java好文,也有著作. 线程上下文类加载器 线程上下文类加载器(context class loader)是从 JDK ...
- Java的SPI机制
Dubbo等框架使用到必须掌握. java.sql.Driver 是 Spi,com.mysql.jdbc.Driver 是 Spi 实现,其它的都是 Api. package org.hadoop. ...
- 高级开发必须理解的Java中SPI机制
本文通过探析JDK提供的,在开源项目中比较常用的Java SPI机制,希望给大家在实际开发实践.学习开源项目提供参考. 1 SPI是什么 SPI全称Service Provider Interface ...
- JAVA SPI机制概述
SPI概述 1.SPI概念 ①全称:Service Provider Interface ②是什么:是Java提供的一套用来被第三方实现或者扩展的接口,从JDBC4.0,JDK6开始Java实现了SP ...
- JDK/Dubbo/Spring 三种 SPI 机制,谁更好?
点击关注公众号,Java干货及时送达 来源:juejin.cn/post/6950266942875779108 SPI 全称为 Service Provider Interface,是一种服务发现机 ...
- Dubbo 源码分析 - SPI 机制
1.简介 SPI 全称为 Service Provider Interface,是一种服务发现机制.SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类.这样可以 ...
最新文章
- Java变量的修饰符
- PHP-代码审计-文件上传
- error: gnu/stubs-32.h: No such file or directory
- 非结构化数据与结构化数据提取---- BeautifulSoup4 解析器
- CVPR | 旷视提出Meta-SR:单一模型实现超分辨率任意缩放因子
- vsftp账号_Linux下vsftp的重新安装和配置虚拟用户
- 【转】Oracle 系统权限详细列表
- 三步搞定android应用底部导航栏
- OMRON欧姆龙Sysmac Studio软件--最新版快捷键
- Android App加固原理与技术历程
- 匹配区县代码_省份、城市、区县三级联动Html代码
- linux 注释批处理,Linux_批处理 正则表达式(findstr) 整理,语法 findstr [/b] [/e] [/l] [/r] [/s] - phpStudy...
- 电总协议串口调试助手
- Python全栈 Web(前端三剑客之JavaScript 从小白鼠到武林盟主)
- 空间机械臂Matlab/Simulink仿真程序自由漂浮空间机械臂(双臂)轨迹跟踪控制matlab仿真程序
- 教你用Ossim平台检测网络的Shellcode攻击
- python环境IDLE下载
- 这份赏金任务,人人都能做,只要……
- ApacheCN×Tesra 免费算力申请活动
- python怎么加逗号_python – 什么是最简单的方法添加逗号到一个整数?