关于 Java 的 SPI,我感觉就是一个面向接口编程的概念,为何要这样设计

2021-06-28 22:27:47 +08:00
 0576coder

目前因为数据库迁移导致的一系列问题,所以我这边阅读了一点点 mysql-connector-java 的源码

看到有这样的文件夹

了解了之后才知道是 java 的 spi 其实我感觉就是一种变相面向接口编程,解耦代码的方式

其实真的再你使用的时候你自己再注入

Class.forName("com.mysql.cj.jdbc.Driver");

就可以了

为什么还要设计这样一种文件夹格式,其实你还是手动要在配置文件指定这个 Driver

我不太能完全理解

2854 次点击
所在节点    Java
16 条回复
blindpirate
2021-06-28 22:45:59 +08:00
不能理解就去读 ServiceLoader 的煮食,里面说的很清楚:

> A service provider that is packaged as a JAR file for the class path is
> identified by placing a <i>provider-configuration file</i> in the resource
> directory {@code META-INF/services}. The name of the provider-configuration
> file is the fully qualified binary name of the service. The provider-configuration
> file contains a list of fully qualified binary names of service providers, one
> per line.
fkdog
2021-06-28 22:48:09 +08:00
SPI 和 API 同样都有程序开发接口的意思。
只不过 SPI 一般表示我是接口的提供方 /实现方。
比如一些支付类的第三方应用需要回调我方某个接口,这个接口对我方来说是 SPI,对调用方来说是 API 。
具体到 java 这种语言里,你实现某个接口类、抽象类,也算是一种 SPI 。
java 语言提供接口标准,具体的实现交给第三方,以前的 JavaEE EJB 里到处都是这样的设计。
GuuJiang
2021-06-28 22:48:55 +08:00
Class.forName 是在用了 SPI 机制之前的加载技术,实际上当包加入了 SPI 元信息后并不需要再写 Class.forName 了,只不过天下文章一大抄,各种只知其然不知其所以然的“教程”仍然在把 Class.forName 这种已经过时的东西传来传去
0576coder
2021-06-28 22:59:05 +08:00
@blindpirate
我知道 这其实就是一个规范
比如我也手写了实现了 jdbc 接口的包,我直接 new driver 的时候注入我这个实现类就好了 为什么要有这样的一个文件夹,感觉多此一举
0576coder
2021-06-28 23:02:20 +08:00
@fkdog
我手动这样实现接口 然后再具体使用的时候才注入具体的类 这种不应该叫 DI 么 依赖注入

这个 SPI 是不是类似的意思 只不过多了个文件夹- -
fantastM
2021-06-29 01:49:37 +08:00
如果你的应用需要连接多个不同的数据源( MySQL 、PostgreSQL 、Oracle…),那么就需要使用多个 JDBC 的驱动。按你说的方式,开发者需要先去各个驱动对应的官网查资料(得知道 com.mysql.cj.jdbc.Driver 这个约定值),然后再编写多次各个驱动对应的注册代码。这儿的「 JDK - JDBC 驱动 - 开发者」三个角色都被耦合了。

用 SPI 这种机制的话,起码在注册驱动这一点上,开发者是不用再顾虑了的,JDBC 驱动可以在其内部提供实现。楼主可能对 MySQL 已经很熟悉了,所以体会不是很深,不过假设现在要连接一个你完全没接触过的数据库(例如 SQLite ),你是不是会期望做的事情越少越好?

还有你说的 DI 什么的……那更好理解了,你看看一个单纯的 Spring 应用和基于 Spring Boot 应用有什么区别,然后 Spring Boot 是怎样提供一些默认配置的,它的 spring.factories 文件有什么作用?这难道和 SPI 的思想不一样吗
0576coder
2021-06-29 01:57:29 +08:00
@fantastM
这儿的「 JDK - JDBC 驱动 - 开发者」三个角色都被耦合了
我明白这个意思,因为 jdbc 都封好了接口,所以不管连什么数据库,接口都是基本一致的 开发者只需要面向接口编程 而不是面向实现编程,这个依赖注入就能解决掉。
我是不理解这种配置文件的方式,这个其实跟我手动注入,感觉本质上他没有很大的区别。
根据配置注入具体的实例=SPI 吗 那我感觉本质上也是一种依赖注入 不知道是不是可以这样理解
fantastM
2021-06-29 02:49:56 +08:00
> 我是不理解这种配置文件的方式,这个其实跟我手动注入,感觉本质上他没有很大的区别。

如果这个 JDBC 的 SPI 配置文件是你写的,那相当于你是 JDBC 驱动的开发者了,这样的话,确实和你手动注入没什么区别,毕竟工作量都是你一个人的......

> 根据配置注入具体的实例=SPI 吗 那我感觉本质上也是一种依赖注入 不知道是不是可以这样理解

SPI 就是 Service Provider Interface 的缩写,用「根据配置注入具体的实例」拿来做可扩展的服务发现,是一种解耦思想的体现。

例如,SDK ( JDK )提供约定行为的 Interface ( java.sql.Driver),并且对这个 Interface 使用逻辑还是在 SDK 里的(在 java.sql.DriverManager#getConnection(String url) 里会用到),然后 SPI 的实现者( mysql-connector-java )只需提供 Interface 的具体实现( com.mysql.cj.jdbc.Driver )即可,不需要关心 Interface 的使用逻辑。

从这方面看,SPI 和 DI 还是不太一样的吧,虽然这两者的都是为了解耦。

楼主你纠结的「这种配置文件的方式」和「跟我手动注入」两种方式,代码跑起来是没什么区别,但你站高处想一想,两者从设计上有什么区别,尤其是对使用者而言。
lionseun
2021-06-29 07:38:30 +08:00
从设计模式的角度考虑,都有了 drivermanager 了,还搞什么 class fromname
BBCCBB
2021-06-29 08:34:16 +08:00
有了 spi, 一般情况下就不需要你手动指定 Class.forName("com.mysql.cj.jdbc.Driver") 了.
qwerthhusn
2021-06-29 08:54:14 +08:00
就像 Servlet 一样,Tomcat/Jetty 实现 SPI,开发者去用 API
szq8014
2021-06-29 10:10:15 +08:00
> 为什么还要设计这样一种文件夹格式,其实你还是手动要在配置文件指定这个 Driver

用了 SPI 就不再需要手动指定了,#3 @GuuJiang 也说了,Class.forName 至少是在用了 SPI 规范后是不需要了。


SPI 就是把接口的具体实现给隔离了,
就像 Spring 里面各种 interface 与 xxImpl 一样。

也像#11 楼 @qwerthhusn 说的那样,你照着 Servlet 规范写了一个 web 就可以部署在 Tomcat, Jetty, WildFly 等等一样,你用 javax.servlet.http.HttpServletRequest 这个接口就可以,一般情况下不用关心是谁来实现的这个东西,你`代码`里面也不用去显式的声明我依赖 Tomcat 我依赖 Jetty 。


> 这个其实跟我手动注入,感觉本质上他没有很大的区别

那是,大家都在解决如何动态的加载一个接口实现,只不过 SPI 把这块抽取出来成了一个规范而已。
你没了 javax.servlet.api 照样能写 web 接口不是?
0576coder
2021-06-29 10:25:38 +08:00
其实我有点明白了
这个 SPI 应该就是 JDK 自带的一种规范,顺便自带了代码层面的服务发现类似的功能,我只要按照 SPI 的规范去实现接口,他就能自动知道我这个包是 xx 接口的具体实现。所以使用的时候只需要配置文件指定下就行

而 DI 依赖注入只是一种代码层面的设计模式

他们的本质都是为了面向接口编程 解耦
MineDog
2021-06-29 10:37:03 +08:00
其实都是约定,有了约定,很多东西就可以标准化了
GuuJiang
2021-06-29 11:45:48 +08:00
@0576coder 是的,你在主题中的疑惑应该来自于“反正都要手写 Class.forName,那么 spi 就没意义了”,事实上只要认识到 Class.forName 不但是多余的,甚至根本就是错误的,正确的做法是只需要把带有元信息的相应的实现放到 classpath 即可,当需要替换驱动时对代码完全无感,从这个角度看 spi 跟多态、DI 、服务发现等都是一回事,手写 Class.forName 反而依赖了具体实现
Bromine0x23
2021-06-29 12:10:23 +08:00
从 2006 的 JDBC 4.0 规范开始就可以使用 SPI 自动加载了,除非用老古董驱动不然可以忘掉 Class.forName 了。
SPI 本身是一种实现注册机制,JDBC 用的 java.sql.Driver 是最常见的,其他还有 java.security.Provider ( JCE )、javax.servlet.ServletContainerInitializer 、javax.annotation.processing.Processor 之类的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/786341

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX