[ Java ] 到底什么是 SPI ?

Posted 削尖的螺丝刀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ Java ] 到底什么是 SPI ?相关的知识,希望对你有一定的参考价值。

           

        昨天在和我的小伙伴探讨 Druid 源码的时候,他提出了一个问题,问题是这样描述的:DruidDataSource#getConnection 中的 init 执行 DruidDriver.getInstance 的时候是如何把其他驱动注册的,也就是下面这里:

 public void init() throws SQLException 
        if (inited) 
            return;
        

        // bug fixed for dead lock, for issue #2980
        DruidDriver.getInstance();

        到底是为什么呢? 那么我们就以这个问题作为钥匙,开启我们对SPI学习的大门。 先跟进去看一下  ( 确实如果不是有人提醒我根本没在意,这里面有几个关键的静态代码块 )

   public static DruidDriver getInstance() 
        return instance;
    

DruidDriver的静态代码块:

static 
        AccessController.doPrivileged(new PrivilegedAction<Object>() 
            @Override
            public Object run() 
                registerDriver(instance);
                return null;
            
        );
    

  public static boolean registerDriver(Driver driver) 
        try 
            DriverManager.registerDriver(driver);

            try 
                MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();

                ObjectName objectName = new ObjectName(MBEAN_NAME);
                if (!mbeanServer.isRegistered(objectName)) 
                    mbeanServer.registerMBean(instance, objectName);
                
             catch (Throwable ex) 
                if (LOG == null) 
                    LOG = LogFactory.getLog(DruidDriver.class);
                
                LOG.warn("register druid-driver mbean error", ex);
            

            return true;
         catch (Exception e) 
            if (LOG == null) 
                LOG = LogFactory.getLog(DruidDriver.class);
            
            
            LOG.error("registerDriver error", e);
        

        return false;
    

        再跟进看一下 DriverManager.registerDriver(driver) 的内部也有个静态代码块:

  /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the @code ServiceLoader mechanism
     */
    static 
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    

  private static void loadInitialDrivers() 
        String drivers;
        try 
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() 
                public String run() 
                    return System.getProperty("jdbc.drivers");
                
            );
         catch (Exception ex) 
            drivers = null;
        
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() 
            public Void run() 

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try
                    while(driversIterator.hasNext()) 
                        driversIterator.next();
                    
                 catch(Throwable t) 
                // Do nothing
                
                return null;
            
        );

可以看到关键方法:

 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

        

        我们都知道静态代码块是执行在最前面,而这里对其他驱动的加载正是在静态代码块里实现的,可是问题是为什么其他的驱动也会被加载呢?

       这 一切都要从Java类的加载机制 以及 这个问题的最关键原理 —— SPI 说起,而它通过驱动加载的方式,也算是打破类在加载过程中双亲委派机制 的一个关键手段 !

(  如果你要问为什么要打破? 原因就在于驱动属于Java核心类库,而核心类库的 Class文件 只能由启动类加载器加载,如果对加载过程和双亲委派原理掌握的不是很透彻的同学,可以通过这篇文章了解一下 : [ Java ] 一文说透所谓的双亲委派  )

那么 SPI 到底是个什么东西呢 ? 又为什么要有它 ?

说到底,最关键的就为了两个字 —— 解耦

如果你一定让我多说两个字,那就是为了方便在不改动原始代码的基础上 ——  扩展

        我们仔细想想,如果你是一个厂家,你生产了一个 jar 包给别人用,但是里面有些方法你觉得可以让别人自定义,但是又不想修改 jar 包怎么办? 这就是 SPI 的关键作用。他可以不通过修改原厂jar包实现客户自己定义关键方法。

        再来解释下 SPI , 它的全称是: Service Provider Interface , 是一种面向接口变成的思想规范,它是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。


有了上面的理论基础,我们这里写一个 Demo 来自己实现一套 SPI 看看:

1.首先我们建立一个普通Maven项目,模拟自己是一个SDK的提供者,我们再里面提供一个可以自定义的SPI接口

2.然后我们建立一个该接口的实现类

3.接下来我们建立一个测试工具,这个工具就模拟了SDK要提供服务的整理流程,中间夹杂了自定义方法:

4.指定 SPI 加载类

        敲黑板,关键点来了: SPI 的规范就是 ,要在资源包下 建立一个 META-INF.services 的文件夹,然后再这个文件夹里建立一个文件,千万注意的是,文件名必须是你要提供的服务类的全限定类名:那么我这里就是 spi.SPIService ,然后再这个文件里,写入你对这个服务实现类的全限定类名。

5.那么我们最终建立的内容结构就是这个样子了:


6.我们给这个项目 打成一个 jar 包,然后再另外创建一个maven项目来模拟我们的真实项目,然后引入刚才的jar包。

7.接下来我们建立一个SPIUser类,来加载并使用这个jar包提供的服务。

8.执行结果,可以看到我们jar包提供的服务生效了,且执行了默认的方法。

9.这个时候我们再运用 SPI 的机制,自定义一套方法,逻辑和上面一样,只不过是在当前项目中完成,相当于”重写“ 的概念:

10.同样,定义SPI路径

11.再次 执行刚才的方法,可以看到结果变了:

上面完整描述了 SPI 的实现过程 , 这会我们再来看看 Druid 源码中的内容,同样是不是也看到了 SPI 的身影 :

        至此,我们对 SPI 机制有了一个整体的了解,而他的好处,我刚才也说了,关键就在解耦,概括起来如下:

  • 不需要改动源码就可以实现扩展,解耦。
  • 实现扩展对原来的代码几乎没有侵入性。
  • 只需要添加配置就可以实现扩展,符合开闭原则。

以上是关于[ Java ] 到底什么是 SPI ?的主要内容,如果未能解决你的问题,请参考以下文章

SPI机制到底是什么?中间件又是如何使用的?

java 的SPI机制

不懂Java SPI机制,怎么进大厂

来撸一撸Dubbo之SPI机制源码,SPI到底解决了什么问题?

带你打开-接口测试的大门

不懂Java SPI机制,怎么进大厂