V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
0576coder
V2EX  ›  Java

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

  •  
  •   0576coder · 2021-06-28 22:27:47 +08:00 · 2825 次点击
    这是一个创建于 1004 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    看到有这样的文件夹

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

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

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

    就可以了

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

    我不太能完全理解

    16 条回复    2021-06-29 12:10:23 +08:00
    blindpirate
        1
    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
        2
    fkdog  
       2021-06-28 22:48:09 +08:00
    SPI 和 API 同样都有程序开发接口的意思。
    只不过 SPI 一般表示我是接口的提供方 /实现方。
    比如一些支付类的第三方应用需要回调我方某个接口,这个接口对我方来说是 SPI,对调用方来说是 API 。
    具体到 java 这种语言里,你实现某个接口类、抽象类,也算是一种 SPI 。
    java 语言提供接口标准,具体的实现交给第三方,以前的 JavaEE EJB 里到处都是这样的设计。
    GuuJiang
        3
    GuuJiang  
       2021-06-28 22:48:55 +08:00 via iPhone   ❤️ 4
    Class.forName 是在用了 SPI 机制之前的加载技术,实际上当包加入了 SPI 元信息后并不需要再写 Class.forName 了,只不过天下文章一大抄,各种只知其然不知其所以然的“教程”仍然在把 Class.forName 这种已经过时的东西传来传去
    0576coder
        4
    0576coder  
    OP
       2021-06-28 22:59:05 +08:00
    @blindpirate
    我知道 这其实就是一个规范
    比如我也手写了实现了 jdbc 接口的包,我直接 new driver 的时候注入我这个实现类就好了 为什么要有这样的一个文件夹,感觉多此一举
    0576coder
        5
    0576coder  
    OP
       2021-06-28 23:02:20 +08:00
    @fkdog
    我手动这样实现接口 然后再具体使用的时候才注入具体的类 这种不应该叫 DI 么 依赖注入

    这个 SPI 是不是类似的意思 只不过多了个文件夹- -
    fantastM
        6
    fantastM  
       2021-06-29 01:49:37 +08:00 via iPhone
    如果你的应用需要连接多个不同的数据源( 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
        7
    0576coder  
    OP
       2021-06-29 01:57:29 +08:00
    @fantastM
    这儿的「 JDK - JDBC 驱动 - 开发者」三个角色都被耦合了
    我明白这个意思,因为 jdbc 都封好了接口,所以不管连什么数据库,接口都是基本一致的 开发者只需要面向接口编程 而不是面向实现编程,这个依赖注入就能解决掉。
    我是不理解这种配置文件的方式,这个其实跟我手动注入,感觉本质上他没有很大的区别。
    根据配置注入具体的实例=SPI 吗 那我感觉本质上也是一种依赖注入 不知道是不是可以这样理解
    fantastM
        8
    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
        9
    lionseun  
       2021-06-29 07:38:30 +08:00 via Android
    从设计模式的角度考虑,都有了 drivermanager 了,还搞什么 class fromname
    BBCCBB
        10
    BBCCBB  
       2021-06-29 08:34:16 +08:00
    有了 spi, 一般情况下就不需要你手动指定 Class.forName("com.mysql.cj.jdbc.Driver") 了.
    qwerthhusn
        11
    qwerthhusn  
       2021-06-29 08:54:14 +08:00
    就像 Servlet 一样,Tomcat/Jetty 实现 SPI,开发者去用 API
    szq8014
        12
    szq8014  
       2021-06-29 10:10:15 +08:00   ❤️ 1
    > 为什么还要设计这样一种文件夹格式,其实你还是手动要在配置文件指定这个 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
        13
    0576coder  
    OP
       2021-06-29 10:25:38 +08:00
    其实我有点明白了
    这个 SPI 应该就是 JDK 自带的一种规范,顺便自带了代码层面的服务发现类似的功能,我只要按照 SPI 的规范去实现接口,他就能自动知道我这个包是 xx 接口的具体实现。所以使用的时候只需要配置文件指定下就行

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

    他们的本质都是为了面向接口编程 解耦
    MineDog
        14
    MineDog  
       2021-06-29 10:37:03 +08:00
    其实都是约定,有了约定,很多东西就可以标准化了
    GuuJiang
        15
    GuuJiang  
       2021-06-29 11:45:48 +08:00 via iPhone
    @0576coder 是的,你在主题中的疑惑应该来自于“反正都要手写 Class.forName,那么 spi 就没意义了”,事实上只要认识到 Class.forName 不但是多余的,甚至根本就是错误的,正确的做法是只需要把带有元信息的相应的实现放到 classpath 即可,当需要替换驱动时对代码完全无感,从这个角度看 spi 跟多态、DI 、服务发现等都是一回事,手写 Class.forName 反而依赖了具体实现
    Bromine0x23
        16
    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 之类的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1710 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:43 · PVG 00:43 · LAX 09:43 · JFK 12:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.