[ Java ] 到底什么是 SPI ?

[ Java ] 到底什么是 SPI ?,第1张

           

        昨天在和我的小伙伴探讨 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() {
            @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() {
                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() {
            public Void run() {

                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator 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 loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator 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 机制有了一个整体的了解,而他的好处,我刚才也说了,关键就在解耦,概括起来如下:

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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/923785.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)