spring中所涉及到的SPI机制

一、什么是SPI

  SPI ,全称为 Service Provider Interface,是一种服务发现机制,这一机制被广大厂商和插件所使用。 SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。在我们的系统中有各个抽象的模块,而往往该模块会有很多不同的实现方案,比如:

  1. 日志模块:加载不同的日志实现框架
  2. jdbc模块:加载不同类型的数据库驱动
  3. spring:spring-web加载不同的web容器(servlet3.0所规范的)
    TypeConversion SPI
    springboot的自动配置(spring SPI)
  4. dubbo(dubbo SPI)

  它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类的,在面向对象的设计中,我们一般推荐模块之间基于接口编程,而且模块之间不会对实现类进行硬编码,因为一旦代码里涉及到具体的实现类,就会违反了可插拔的原则。所以为了实现在模块装配的时候不用再程序里动态指明,这就需要SPI这一种服务发现机制。

  这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC、Web容器(Tomcat、Jetty)、Spring中都使用到了SPI机制,不过Dubbo和Spring不是使用的Java原生的SPI机制,而是将其重写封装或增强成为他们自己的SPI。而在了解SPI的过程中,我们会碰到打破双亲委派机制的做法,也正是因为打破了双亲委派机制,我们才能够做到SPI。若想详细了解如何打破双亲委派机制,请看之前的文章。

疑问一:springmvc怎样才能不使用springmvc.xml配置文件,从而配置springmvc呢?
            答:通过JavaConfig方式,即通过注解@Configuration及编程式方式来配置。
疑问二:springmvc怎样才能不使用web.xml文件,从而配置web容器呢?
            答:通过Java的SPI机制,实现 WebApplicationInitializer接口,将servlet-mapping、filter-mapping、listener在实现类里配置。

  那么通过JavaConfig方式和Java的SPI机制就能够实现springmvc的零配置

二、以下是spring-web对接tomcat时所使用到的SPI

  tomcat web容器 遵循servlet规范,所以成为了web容器。
  在servlet规范3.0里, 为了支持可以不使用web.xml,提供了ServletContainerInitializer接口,描述了在项目根目录下的META-INF文件夹下的services文件夹下的javax.servlet.ServletContainerInitializer文件里的 类的全限定名,该类只需要实现ServletContainerInitializer,就可以实现零配置启动。这种就是使用了java的SPI机制。

  Tomcat所用的SPI类是WebappServiceLoader

如图一与图二所示

在这里插入图片描述

图一

在这里插入图片描述
在这里插入图片描述

最后两张为图二

  因为这个类@HandlesTypes注解的是WebApplicationInitializer.class,Servlet3.0容器会自动的扫描classpath下面所有的WebApplicationInitializer接口的实现类,并提供给SpringServletContainerInitializer的onStartup()方法

三、如何使用SPI机制呢?

使用Java的原生SPI机制的步骤有三:
	要使用Java的原生SPI主要得用到ServiceLoader类

在这里插入图片描述

① 项目一:创建一个类LogInvoke【调用方】来使用ServiceLoader类,并创建一个接口Log【标准服务接口】。
关键:ServiceLoader.load(Log.class),该Log.class表示的就是需要交给其他开发者去扩展的接口

扫描二维码关注公众号,回复: 12489529 查看本文章
public class LogInvoke {
    
    
    public static Log getLog(){
    
    
        Log log = null;
        ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
        Iterator<Log> iterator = serviceLoader.iterator();
        if(iterator.hasNext()){
    
    
            log = iterator.next();
        }
        return log;
    }
}

public interface Log {
    
    
    public void info(String msg);
}

② 项目二:创建服务提供方,创建一个类MyLog实现Log接口,并在META-INF/services下创建一个以Log接口的类的全限定名为名的文件,在其文件内配置MyLog实现类的全限定名。

public class MyLog implements Log {
    
    
@Override
    public void info(String msg) {
    
    
        System.out.println("我实现的日志"+msg);
    }
}

③ 将②所在的项目打成jar包,放到①项目的依赖里。调用①的调用方就能动态加载②的服务实现方;
若想调用其他的服务实现方,则只需要将其他服务实现方的jar包放到①项目就可以了,这样就实现了模块的插拔。

在这里插入图片描述

我们来看看JDK是如何获取数据库驱动的:
  JDK定义了一套接口规范—Driver接口,由此来获取数据库驱动

数据库驱动目前常用的有两种:

  1. mysql(mysql-connector.jar)

  2. oracle(oracle-connector.jar)

  通过上面的Java原生SPI机制可知,数据库驱动的jar包里肯定是实现了Driver接口的,并且其实现类的全限定名肯定写在了META-INF/services/java.sql.Driver的文件里。
  所以在这里只要你想要用oracle的数据库驱动就可以添加oracle-connector依赖,想用mysql数据库启动就添加mysql-connector.jar依赖,从而实现了可插拔。

  jdk在调用数据库的时候最终会用到 DriverManager.getConnection(url,username,password)
而对于DriverManager类,在初始化的时候就会调用到数据库驱动了,看静态代码块。

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;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
    
            public Void run() {
    
    
                //ServiceLoader的使用
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
    
            //这里的driversIterator.hasNext()和driversIterator.next()就已经把Driver的实现类【即服务提供者】给加载进来了【通过Class.forName,用来加载的加载器还是Thread.currentThread().getContextClassLoader()】。主要看LazyIterator。 这里的getContextClassLoader()是当前线程的类加载器方法,如果当前线程没有设置的话默认是AppClassLoader加载器,而不是当前类加载器getClassLoader()。
getClassLoader是使用双亲委派模型来加载类的,而getContextClassLoader就是为了打破双亲委派模型的加载方式的,也就是说现在是在DriverManager类里,而DriverManager是由bootstrapclassloader加载器加载的,所以在DriverManager里面加载其他类的话也会是会先用加载DriverManager类的加载器来加载————也就是bootstrapclassloader,那么使用bootstrapclassloader来加载Driver的实现类是不可能的,因为Driver的实现类根本就不在jdk的lib里,所以只能通过Thread.currentThread().getContextClassLoader()的方式来获取到AppClassLoader(Thread的ContextClassLoader没设置类加载器的话默认存的是AppClassLoader引用),而Driver的实现类我们是通过jar包的方式添加进我们的根路径ClassPath,所以AppClassLoader能够扫描加载到Driver的实现类,那么这个时候本来应该由bootstrapclassloader来加载的类变成了AppClassLoader来加载了,这就打破了双亲委派机制【基础类被bootstrapclassloader加载,而我们写的类里包含了基础类,所以正常的双亲委派就是将需要加载的类交给parent加载器去加载,parent加载不了的才轮到自己加载,这是一种至上而下的,而在本例子里,当加载我们Driver的实现类的时候当前自身的加载器是bootstrapclassloader(因为DriverManager是基础类由bootstrapclassloader加载,而我们现在正处于DriverManager类里准备去加载Driver的实现类,所以当前自身的加载器就是bootstrapclassloader),其parent为null,所以它自身去加载Driver的实现类,但发现加载不了,因为其位置不在自己范围之内,所以扫描不到,这个时候还是符合双亲委派机制的,但是这样我们的Driver的实现类就扫描不到了需要做的事也做不到了,所以我们通过Thread.currentThread().getContextClassLoader()取出AppClassLoader这个做法,使得双亲委派被破坏了,因为这样变成了Driver实现类是被AppClassLoader加载的,而不是当前的bootstrapclassloader,也不是bootstrapclassloader的parent(因为bootstrapclassloader的parent为Null),所以双亲委派是需要在当前的加载器的parent去加载类 或者自身去加载,而不是在当前位置往下传去给son(这里的son是为了容易理解)们去加载】
                    while(driversIterator.hasNext()) {
    
    
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                // Do nothing
                }
                return null;
            }
        });


        println("DriverManager.initialize: jdbc.drivers = " + drivers);


        if (drivers == null || drivers.equals("")) {
    
    
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
    
    
            try {
    
    
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
    
    
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

在这里插入图片描述
  若上面所提到的双亲委派机制的解释觉得有点乱的话,可以去如何打破双亲委派机制看一下,里面有详解讲解。

猜你喜欢

转载自blog.csdn.net/gwokgwok137/article/details/113924744