Java的Spi机制研究

 摘要:之前研究过dubbo、spring源码。然后发现大量使用一个叫做spi的东西,发现这确实是一个很好的实现方法,特别是在设计框架中。

一、SPI机制概念

      SPI的全称是Service Provider Interface。简单来说,SPI机制提供了一个表达接口和其具体实现类之间的绑定关系的方案。具体是在JAR包的"META-INF/services/"目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名。

      SPI可以理解是为接口寻找服务实现类。现在公司的系统都是进行了模块的划分,系统抽象为多个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。于是就有了SPI这种服务发现机制。

二、JDBC中的SPI机制

  在java中,Java.sql.Driver接口是Java对外公开的一个加载驱动接口,Java并未实现,至于实现这个接口由各个Jdbc厂商去实现就行了,好处是解藕,使得更具有灵活性,当然这也是面向对象的好处之一。真正的实现是不同提供商提供的。

[java]  view plain  copy
  1. Class.forName("com.mysql.jdbc.Driver");  
  2. Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root""123456");  
  3. Statement stmt = conn.createStatement();  
  4. ResultSet rs = stmt.executeQuery("select * from Users");  
Class.forName("com.mysql.jdbc.Driver");这里虽是加载mysql的driver,但是无论是oracle还是其它的jdbc驱动包,它们的原理都是spi机制。

首先看java.sql.driver

[java]  view plain  copy
  1. package java.sql;  
  2.   
  3. import java.util.logging.Logger;  
  4. public interface Driver {  
  5.     boolean acceptsURL(String url) throws SQLException;  
  6.   
  7.     DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)  
  8.                          throws SQLException;  
  9.   
  10.     int getMinorVersion();  
  11.   
  12.     boolean jdbcCompliant();  
  13.   
  14.     public Logger getParentLogger() throws SQLFeatureNotSupportedException;  
  15. }  

这个类是一个接口类, 在整个JDK包中都没有它的实现类,它的实现要由各个jdbc的开发产商去实现,但是我们发现DriverManager这个类中还是有去加载Driver类,那它是怎么发现其它开发商实现的Driver类?

再来看看DriverManager

这里将driver封成一个driverinfo对象,然后在DriverManager 这个类加载时就初始化一次,那初始化它做什么呢?其实就是其发现driver类的实现类,并加载到当前类中。注意看loadInitialDrivers方法。

[java]  view plain  copy
  1. public class DriverManager {  
  2.   
  3.   
  4. // List of registered JDBC drivers  
  5. private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();  
  6.   
  7. static {  
  8. loadInitialDrivers();  
  9. println("JDBC DriverManager initialized");  
  10. }  
  11.   
  12. private static void loadInitialDrivers() {  
  13. String drivers;  
  14. try {  
  15. drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {  
  16. public String run() {  
  17. return System.getProperty("jdbc.drivers");  
  18. }  
  19. });  
  20. catch (Exception ex) {  
  21. drivers = null;  
  22. }  
  23.   
  24.   
  25. AccessController.doPrivileged(new PrivilegedAction<Void>() {  
  26. public Void run() {  
  27.   
  28.   
  29. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);  
  30. Iterator driversIterator = loadedDrivers.iterator();  
  31.   
  32. try{  
  33. while(driversIterator.hasNext()) {  
  34. driversIterator.next();  
  35. }  
  36. catch(Throwable t) {  
  37. // Do nothing  
  38. }  
  39. return null;  
  40. }  
  41. });  
  42.   
  43.   
  44. println("DriverManager.initialize: jdbc.drivers = " + drivers);  
  45.   
  46.   
  47. if (drivers == null || drivers.equals("")) {  
  48. return;  
  49. }  
  50. String[] driversList = drivers.split(":");  
  51. println("number of Drivers:" + driversList.length);  
  52. for (String aDriver : driversList) {  
  53. try {  
  54. println("DriverManager.Initialize: loading " + aDriver);  
  55. Class.forName(aDriver, true,  
  56. ClassLoader.getSystemClassLoader());  
  57. catch (Exception ex) {  
  58. println("DriverManager.Initialize: load failed: " + ex);  
  59. }  
  60. }  
  61. }  
  62. ...  
  63. }  

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);这一句就是真实的发现driver类的实现类。

在mysql-connector-java-.jar包下面META-INF.services包下有个java.sql.Driver文件打开文件有下面两行




com.mysql.jdbc.Driver这样就实现了在我们程序运行时引入mysql-connector-java-.jar这个包。然后在JDK源码中,当我们调用到

DriverManager.getConnection的方法,就会将程序中使用到的Driver接口类用mysql驱动包的Driver实现类来使用。

这样做有什么好处?

1、不用在JDK里实现Driver实现类的硬编码,然后每次使用JDK里的DriverManager 类时,都会自动去发现Driver类的实现类,并根据这个实现类来做数据库连接。

2、可以满足不同的产商实现各不相同,但对外暴露一样的接口。使用方只要按照JDK的标准方法来调用即可,即实现了接口的可拔插。

三、使用实例

        看完介绍,不自己写一个实例总是理解不深刻,下面来看看笔者自己写的一个实例。

本文示例工程下载

1、接口工程


接口类:

[java]  view plain  copy
  1. package com.lin.service;  
  2.   
  3. public interface Service {  
  4.   
  5.     void say();  
  6.   
  7. }  

2、实现工程1


实现类:

[java]  view plain  copy
  1. package com.lin;  
  2.   
  3. import com.lin.service.Service;  
  4.   
  5. public class Service1 implements Service {  
  6.   
  7.     public void say() {  
  8.         System.out.println("say hello from ServiceImpl1");  
  9.           
  10.     }  
  11.   
  12. }  
引用接口工程:

[java]  view plain  copy
  1. <dependencies>  
  2.     <dependency>  
  3.         <groupId>com.lin</groupId>  
  4.         <artifactId>SpiService</artifactId>  
  5.         <version>0.0.1-SNAPSHOT</version>  
  6.     </dependency>  
  7. </dependencies>  

并配置Spi:


3、实现工程2



实现类:

[java]  view plain  copy
  1. package com.lin;  
  2.   
  3. import com.lin.service.Service;  
  4.   
  5. public class Service2 implements Service {  
  6.   
  7.     public void say() {  
  8.         System.out.println("say hello from ServiceImpl2");  
  9.           
  10.     }  
  11.   
  12. }  
引用接口:

[java]  view plain  copy
  1. <dependencies>  
  2.     <dependency>  
  3.         <groupId>com.lin</groupId>  
  4.         <artifactId>SpiService</artifactId>  
  5.         <version>0.0.1-SNAPSHOT</version>  
  6.     </dependency>  
  7. </dependencies>  

并配置spi:



4、使用工程

引入上面三个工程

[java]  view plain  copy
  1.     <dependencies>  
  2.     <dependency>  
  3.         <groupId>com.lin</groupId>  
  4.         <artifactId>SpiService</artifactId>  
  5.         <version>0.0.1-SNAPSHOT</version>  
  6.     </dependency>  
  7.     <dependency>  
  8.         <groupId>com.lin</groupId>  
  9.         <artifactId>SpiServiceImpl1</artifactId>  
  10.         <version>0.0.1-SNAPSHOT</version>  
  11.     </dependency>  
  12.     <dependency>  
  13.         <groupId>com.lin</groupId>  
  14.         <artifactId>SpiServiceImpl2</artifactId>  
  15.         <version>0.0.1-SNAPSHOT</version>  
  16.     </dependency>  
  17. </dependencies>  

java代码:

[java]  view plain  copy
  1. package com.lin;  
  2.   
  3. import java.util.ServiceLoader;  
  4.   
  5. import com.lin.service.Service;  
  6.   
  7. public class UseDemo {  
  8.   
  9.     public static void main(String[] args) throws Exception {  
  10.         ServiceLoader<Service> services = ServiceLoader.load(Service.class);  
  11.         for (Service service : services) {  
  12.             service.say();  
  13.         }  
  14.   
  15.     }  
  16.   
  17. }  
输出结果:

say hello from ServiceImpl1
say hello from ServiceImpl2

如果只引入实现1:

[java]  view plain  copy
  1.     <dependencies>  
  2. <dependency>  
  3.     <groupId>com.lin</groupId>  
  4.     <artifactId>SpiService</artifactId>  
  5.     <version>0.0.1-SNAPSHOT</version>  
  6. </dependency>  
  7. <dependency>  
  8.     <groupId>com.lin</groupId>  
  9.     <artifactId>SpiServiceImpl1</artifactId>  
  10.     <version>0.0.1-SNAPSHOT</version>  
  11. </dependency>  
  12. lt;/dependencies>  
那么就只会发现实现1,打印结果如下:

如果只引入接口包,那什么都不会打印。

[java]  view plain  copy
  1.     <dependencies>  
  2.     <dependency>  
  3.         <groupId>com.lin</groupId>  
  4.         <artifactId>SpiService</artifactId>  
  5.         <version>0.0.1-SNAPSHOT</version>  
  6.     </dependency>  
  7. </dependencies>  

本文示例工程下载
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值