【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

文章目录

一.Java SPI

1.描述

SPI 全称为Service Provider Interface,JDK内置的一种动态的服务提供发现机制SPI 的本质是将接口实现类全限定名(包名+类名)配置在文件中,并由服务加载器ServiceLoader读取配置文件来加载实现类实现在运行时动态为接口替换实现类。可以理解为 运行时动态加载接口的实现类。实际上就是“基于接口的编程+策略模式+配置文件”组合实现的一种动态加载机制

举个栗子

  • 你有一个接口 A。A1/A2/A3 分别是接口A的不同实现。你通过配置 接口 A = 实现 A2,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。

使用场景

  • 一般用于插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 spi 思想就用上了。

Java提供的SPI

  • Java提供了很多服务提供者接口(SPI),允许第三方为这些接口提供实现。常见的SPI有JDBC、JCE、JNDI、JAXP和JBI等。
    • 这些SPI的接口是由Java核心库来提供,而SPI的实现则是作为Java应用所依赖的jar包被包含进类路径(CLASSPATH)中。例如:JDBC的实现mysql就是通过maven被依赖进来。

Java SPI本质上其实就是“基于接口编程+策略模式+配置文件”组合实现的动态加载机制
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

通过下图来看,完成spi的实现,需要哪些操作,需要遵循哪些规范?
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

SPI机制使用规范:

  • 当服务的提供者编写服务规范接口的一种实现,在Jar包的META-INF/Services/目录中同时创建一个以服务规范接口命名的文件该文件里存放实现该服务接口的具体实现类。而当外部程序装配这个实现模块的时候,就能通过该模块Jar包META-INF/Services/的配置文件找到具体的实现类名。并装载实例化,完成模块的注入。基于这样的一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。

那么问题来了,SPI的接口 是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的。SPI的实现类 是由系统类加载器(System ClassLoader)来加载的。

  • 引导类加载器在加载时是无法找到SPI的实现类的,因为双亲委派模型中规定,引导类加载器BootstrapClassloader无法委派系统类加载器AppClassLoader来加载。这时候,该如何解决此问题?
  • 线程上下文类加载由此诞生,它的出现也破坏了类加载器的双亲委派模型,使得程序可以进行逆向类加载

2.实现一个自定义的SPI?

项目结构
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

  • spi-interface: 是针对厂商定义的接口项目,只提供接口,不提供实现
  • spi-provider1/spi-provider2: (服务提供方)分别是两个厂商对interface的不同实现,所以他们会依赖于interface项目
  • spi-core: 是提供给用户使用的核心jar文件, 同样依赖于interface项目, 用户使用时需要引入spi-core.jar和厂商具体实现的jar
  • spi-test:用来模拟用户测试(服务调用方), 依赖spi-core和spi-boy/spi-gril(至少一个实现,否则会报错)

2.1.父项目-spi

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo</groupId>
    <artifactId>spi</artifactId>
    <version>1.0</version>

    <modules>
        <module>spi-provider1</module>
        <module>spi-provider2</module>
        <module>spi-core</module>
        <module>spi-interface</module>
        <module>spi-test</module>
    </modules>
    <packaging>pom</packaging>
</project>

2.2.spi-interface

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

package com.demo;
public interface ApiService {
    void execute();
}

2.3.spi-provider1

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spi</artifactId>
        <groupId>com.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>spi-provider1</artifactId>
    <name>spi-provider1</name>

    <dependencies>
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-interface</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>
</project>

2.规范接口的实现
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

package com.demo;
public class SpiProvider1 implements ApiService {
    @Override
    public void execute() {
        System.out.println(this.getClass().getName()+"=>hello provider ");
    }
}

3.classpath下面的META-INF/services创建规范接口对应文件

  • 服务提供方实现标准服务接口后,在自己Jar包的META-INF/services目录下新建一个名为标准服务接口全限定名的文件,并将具体实现类全名写入。
  • 该文件的名称是服务接口的全限定类名,内容则是服务接口实现类的全限定类名,*如果是多个实现类则用换行符分割

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
文件内容

com.demo.SpiProvider1

2.4.spi-provider2

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spi</artifactId>
        <groupId>com.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>spi-provider2</artifactId>
    <name>spi-provider2</name>

    <dependencies>
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-interface</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>
</project>

2.规范接口的实现
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

package com.demo;
public class SpiProvider2 implements ApiService {
    @Override
    public void execute() {
        System.out.println(this.getClass().getName()+"=>hello provider ");
    }
}

3.classpath下面的META-INF/services创建规范接口对应文件

  • 服务提供方实现标准服务接口后,在自己Jar包的META-INF/services目录下新建一个名为标准服务接口全限定名的文件,并将具体实现类全名写入。
  • 该文件的名称是服务接口的全限定类名,内容则是服务接口实现类的全限定类名,*如果是多个实现类则用换行符分割
    【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

文件内容

com.demo.SpiProvider2

2.5.spi-core

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spi</artifactId>
        <groupId>com.demo</groupId>
        <version>1.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>spi-core</artifactId>
    <name>spi-core</name>

    <dependencies>
		<dependency>
			<groupId>com.demo</groupId>
			<artifactId>spi-interface</artifactId>
			<version>1.0</version>
		</dependency>
	</dependencies>
</project>
package com.demo;
public class SpiProvider2 implements ApiService {
    @Override
    public void execute() {
        System.out.println(this.getClass().getName()+"=>hello provider ");
    }
}

2.动态调用规范接口实现,如果没有找到对应的实现则抛出异常
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

  1. 如果调用ApiFactory.invoker()没有找到具体实现抛出异常
  2. 如果发现多个实现,则调用实现的execute(),分别打印this.getClass().getName()+hello provider
package com.demo;
import java.util.Iterator;
import java.util.ServiceLoader;

public class ApiFactory {
    public static void invoker() {
        ServiceLoader<ApiService> load = ServiceLoader.load(ApiService.class);

        Iterator<ApiService> it = load.iterator();

        boolean notFound = true;
        while (it.hasNext()) {
            notFound = false;
            ApiService service = it.next();
            service.execute();
        }

        if (notFound) {
            throw new RuntimeException("未发现ApiService的具体实现");
        }
    }
}

2.6.spi-test

用于模拟服务调用方,调用通过ServiceLoader.load加载服务接口的实现类实例

//测试代码
public class App {
    public static void main(String[] args) {
       ApiFactory.invoker();
    }
}

a. 无厂商实现jar引入

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.demo</groupId>
    <artifactId>spi-test</artifactId>
    <version>1.0</version>
    <name>spi-test</name>

    <dependencies>
        <!--spi-core : 是提供给用户使用的核心jar文件, 同样依赖于interface项目, 用户使用时需要引入spi-core.jar和厂商具体实现的jar-->
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-core</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>
</project>

测试结果
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

b. 引入spi-provider1,执行测试方法

        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-provider1</artifactId>
            <version>1.0</version>
        </dependency>

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

c. 引入spi-provider2,执行测试方法

        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-provider2</artifactId>
            <version>1.0</version>
        </dependency>

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

d. 同时引入spi-provider1、spi-provider2,执行测试方法

        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-provider1</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>spi-provider2</artifactId>
            <version>1.0</version>
        </dependency>

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

3.JDBC数据库驱动包中SPI机制分析

mysql-connector-java-xxx.jar就有一个/META-INF/services/java.sql.Driver里面内容是 com.mysql.jdbc.Driver

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
在引入mysql驱动包后jdbc连接代码java.sql.DriverManager,就会使用SPI机制来加载具体的jdbc实现,关键源码如下:

public class 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<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                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);
            }
        }
    }
}

4.使用反射简单写一个ServiceLoader

实现思路:

  1. 读取配置文件,获取实现类的全名称字符串;
  2. 使用 Java 反射机制来构造服务实现类的实例。可以使用泛型方法,避免获取的时候做类型转换。

不过 JDK 自带的 ·java.util.ServiceLoader· 实现得更加严谨一些,使用了ClassLoader 来加载类,并使用迭代器来获取服务实现类。思路大体相同。

spi-core: 新增简单的spi读取实现类

/**
 * 简单的实例化规范接口
 */
public class SimpleServiceLoader {
    /**
     * 规范接口的相对路径,位于classpath下面的/META-INF/services/
     */
    private static final String PREFIX = "/META-INF/services/";

    /**
     * 读取/META-INF/services/规范接口命名文件内容,并反射实例化规范接口文件内的实现类
     *
     * @param clazz 规范接口
     * @param <T>
     * @return
     */
    public static <T> List<T> load(Class<T> clazz) {
        //读取/META-INF/services/规范接口命名文件内容
        List<String> implClasses = readServiceFile(clazz);

        //存储实例化的实现类
        List<T> implList = new ArrayList<T>();

        for (String implClass : implClasses) {
            try {
                //实例化规范接口的实现类并添加到集合中
                Class<T> matchClazz = (Class<T>) Class.forName(implClass);
                implList.add(matchClazz.newInstance());
            } catch (Exception e) {
                return new ArrayList<T>();
            }
        }
        return implList;
    }

    /**
     * 读取/META-INF/services/规范接口命名文件内容
     *
     * @param clazz 规范接口
     * @return
     */
    private static List<String> readServiceFile(Class<?> clazz) {
        //保存文件内容
        List<String> implClasses = new ArrayList<String>();
        //获取规范接口的全限定名
        String infName = clazz.getCanonicalName();
        //获取 /META-INF/services + 规范的全限定名 的绝对路径
        String fileName = clazz.getResource(PREFIX + infName).getPath();

        //以行读取的方式 读取/META-INF/services下面文件内的内容
        try {
            BufferedReader br = new BufferedReader(new FileReader(new File(fileName)));
            String line = "";
            while ((line = br.readLine()) != null) {
                implClasses.add(line);
            }
            return implClasses;
        } catch (FileNotFoundException fnfe) {
            System.out.println("File not found: " + fileName);
            return new ArrayList<String>();
        } catch (IOException ioe) {
            System.out.println("Read file failed: " + fileName);
            return new ArrayList<String>();
        }
    }
}

spi-core: 新增初始化实现类工程

public class MyApiFactory {
    public static void invoker() {
        List<ApiService> load = SimpleServiceLoader.load(ApiService.class);

        Iterator<ApiService> it = load.iterator();
        
        boolean notFound = true;
        while (it.hasNext()) {
            notFound = false;
            ApiService service = it.next();
            service.execute();
        }

        if (notFound) {
            throw new RuntimeException("未发现ApiService的具体实现");
        }
    }
}

spi-test: 调用MyApiFactory.invoker()方法

  • 引入spi-core、spi-provider1,然后调用 MyApiFactory.invoker()
public class App {
    public static void main(String[] args) {
        MyApiFactory.invoker();
    }
}

执行结果
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

5.JavaSPI源码浅析

源码主要加载流程如下:

  • 应用程序调用ServiceLoader.load()方法 创建一个新的ServiceLoader,并实例化该类中的成员变量

    1. loader(ClassLoader类型,类加载器)
    2. acc(AccessControlContext类型,访问控制器)
    3. providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
    4. ookupIterator(实现迭代器功能)
  • 应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载。

    1. 读取META-INF/services/下的配置文件,获得所有能被实例化的类的全限定名,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件
       try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    
    1. 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
    2. 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。

ServiceLoader源码

public final class ServiceLoader<S> implements Iterable<S>{
    // 加载具体实现类信息的前缀
    private static final String PREFIX = "META-INF/services/";
    //需要加载的服务类或接口
    private final Class<S> service;
    // 用于加载的类加载器
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    // 用于缓存已经加载的接口实现类,其中key为实现类的完整类名,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 用于延迟加载接口的实现类(内部类)
    private LazyIterator lookupIterator;

    
    public void reload() {
        //先清空
        providers.clear();
        //实例化内部类
        lookupIterator = new LazyIterator(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //访问控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //reload方法
        reload();
    }

    private static void fail(Class<?> service, String msg, Throwable cause)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                            cause);
    }

    private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }

    //具体解析资源文件中的每一行内容
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
        	//-1表示解析完成
            return -1;
        }
        // 如果存在'#'字符,截取第一个'#'字符串之前的内容,'#'字符之后的属于注释内容
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
        	//不合法的标识:' '、'\t'
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            //判断第一个 char 是否一个合法的 Java 起始标识符
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
           	//判断所有其他字符串是否属于合法的Java标识符
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            //不存在则缓存
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }
    
    private Iterator<String> parse(Class<?> service, URL u)  throws ServiceConfigurationError {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }

    private class LazyIterator    implements Iterator<S>  {
        Class<S> service;
        ClassLoader loader;
        // 加载资源的URL集合
	    Enumeration<URL> configs = null; 
	    // 需加载的实现类的全限定类名的集合
	    Iterator<String> pending = null;
	    // 下一个需要加载的实现类的全限定类名
	    String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                // 资源名称,META-INF/services + 全限定名
                    String fullName = PREFIX + service.getName();
                     // 加载资源
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            // 从资源中解析出需要加载的所有实现类的全限定名
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                 // 解析的返回值是一个 Iterator<String> 类型,其中的String代表文件里配置的实现类全限定名,比如:com.mysql.jdbc.Driver
                pending = parse(service, configs.nextElement());
            }
            //下一个需要加载的实现类全限定名
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            //反射构造Class实例
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            // 类型判断,校验实现类必须与当前加载的类/接口的关系是派生或相同,否则抛出异常终止
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
            	//强转
                S p = service.cast(c.newInstance());
                 // 实例完成,添加缓存,Key:实现类全限定类名,Value:实现类实例
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    
  public Iterator<S> iterator() {
    // 这里是个Iterator的匿名内部类,重写了一些方法
    return new Iterator<S>() {

        // 已存在的提供者
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 先检查缓存
            if (knownProviders.hasNext())
                return true;
            // 缓存没有,走 java.util.ServiceLoader.LazyIterator#hasNext 方法
            return lookupIterator.hasNext();
        }

        public S next() {
            // 同样先检查缓存
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 缓存没有,走 java.util.ServiceLoader.LazyIterator#next 方法
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
  }
    
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    // 返回ServiceLoader的实例
        return new ServiceLoader<>(service, loader);
    }
    
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }
 
    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }
}
  • java会根据定义的路径去扫描可能存在的接口的实现。放在config中,然后使用parse()方法将配置文件中的接口实现全限定名放在pending中,并取得第一个实现类(变量nextName),然后使用类加载器加载,加载需要调用的类,然后调用实现的方法

优缺点

S优点

  • 使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。使得业务组件插件化

缺点

  • ServiceLoader在加载实现类的时候会全部加载并实例化,无论你是否需要使用到它。而且只能通过迭代去获取实现,无法通过关键字来获取。
    • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类会全部加载并实例化一遍如果你并不想用某些实现类,它也被加载并实例化到内存中,这就造成了资源浪费。获取某个实现类的方式不够灵活,只能通过遍历获取,不能根据某个参数来获取对应的实现类。
    • 多线程使用ServiceLoader类的实例是不安全的。

二.Spring SPI

1.描述

1.1.自动装配是什么

  • 对于Spring的SPI机制主要体现在SpringBoot的自动装配机制上面
    • 在SpringBoot的自动装配过程中,最终会通过SpringFactoriesLoade加载META-INF/spring.factories文件,从classpath下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析核心配置文件properties、yaml文件,找到指定名称的配置后返回。

      需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。

1.2.对比ServiceLoader

  • 一个是加载 META-INF/services/目录下的配置;一个是加载META-INF/spring.factories固定文件的配置。思路都一样。
  • 两个都是利用ClassLoaderClassName来完成操作的。不同的是Java的ServiceLoader加载配置和实例化都是自己来实现,并且不能按需加载;SpringFactoriesLoader既可以单独加载配置然后按需实例化也可以实例化全部。

2.源码浅析

分析org.springframework.boot.SpringBootApplication.run()方法 发现 SpringBoot的启动包含new SpringApplication执行run方法两个过程,new的时候有这么个逻辑:(getSpringFactoriesInstances
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析getSpringFactoriesInstances() 主要做2件事情: 1. 加载类的全限定名列表。2. 根据类名通过反射实例化。

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
重点在于:SpringFactoriesLoader.loadFactoryNames(type, classLoader)

// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    // 获取类的全限定名
    String factoryClassName = factoryClass.getName();
    // 1. 执行loadSpringFactories,这里只传入了类加载器,肯定是要获取全部配置
    // 2. getOrDefault,获取指定接口的实现类名称列表,如果没有则返回一个空列表
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 先检查缓存
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }
    try {
        // 获取类路径下所有META-INF/spring.factories的URL
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        //遍历所有的URL,把加载的配置转换成Map<String, List<String>>格式
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                 // 组装数据,并返回
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

它还提供了实例化的方法:SpringFactoriesLoader.loadFactories(factoryClass, classLoader)利用反射实现,理解起来也不困难,不做解释了

public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
        Assert.notNull(factoryClass, "'factoryClass' must not be null");
        ClassLoader classLoaderToUse = classLoader;
        if (classLoaderToUse == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }
        List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
        if (logger.isTraceEnabled()) {
            logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
        }
        List<T> result = new ArrayList<>(factoryNames.size());
        for (String factoryName : factoryNames) {
            result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
        }
        AnnotationAwareOrderComparator.sort(result);
        return result;
    }

3.如何实现一个自定义的starter?

Spring Boot 将常见的开发功能,分成了一个个的starter,这样我们开发功能的时候只需要引入对应的starter,而不需要去引入一堆依赖了!starter可以理解为一个依赖组,其主要功能就是完成引入依赖和初始化配置。

  • Spring 官方提供的starter 命名规范为 spring-boot-starter-xxx ,第三方提供的starter命名规范为xxx-spring-boot-starter

3.1.SpringBoot的相关要点。

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
这个是SpringBoot的@SpringBootApplication注解,里面还有一个 @EnableAutoConfiguration 注解,开启自动配置的。

【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
可以发现这个自动配置注解在另一个工程,而这个工程里也有个spring.factories文件,如下图:
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
SpringBoot的目录下有个spring.factories文件,里面都是一些接口的具体实现类,可以由SpringFactoriesLoader加载
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

说明实现一个starter必须要在classpath下面创建META-INF/spring.factories文件,通过org.springframework.boot.autoconfigure.EnableAutoConfiguration 标明需要自动配置的类的全限定名

3.2.custom-spring-boot-starter

新建springBoot项目:custom-spring-boot-starter

目录结构
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

1. pom 文件引入 jar 包

引入spring-boot-starter、spring-boot-autoconfigure、spring-boot-configuration-processor
这些Jar在编写自动配置类、注解、生成配置元数据处理等功能依赖的jar包。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <groupId>com.demo</groupId>
    <artifactId>custom-spring-boot-starter</artifactId>
    <version>1.0</version>
    <name>custom-spring-boot-starter</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2.自动配置注解

类型 注解
@ConditionalOnClass 当前classpath下有指定类才加载
@ConditionalOnMissingClass 当前classpath下无指定类才加载
@ConditionalOnBean 当前容器内有指定bean才加载
@ConditionalOnMissingBean 当期容器内无指定bean才加载
@ConditionalOnProperty refix 前缀name 名称havingValue 用于匹配配置项值matchIfMissing 没找指定配置项时的默认值
@ConditionalOnResource 有指定资源才加载
@ConditionalOnWebApplication 是web才加载
@ConditionalOnNotWebApplication 不是web才加载
@ConditionalOnExpression 符合SpEL 表达式才加载
  • 本次我们就选用 @ConditionalOnProperty 。即全局配置文件中有aspectLog.enable=true,才加载我们的配置类。

3.@ConfigurationProperties 与 @EnableConfigurationProperties 作用

  • @ConfigurationProperties注解,主要是用来把properties或者yml配置文件转化为bean来使用的
  • @EnableConfigurationProperties注解的作用是使@ConfigurationProperties注解生效
  • 如果只配置@ConfigurationProperties注解,在IOC容器中是获取不到properties配置文件转化的bean的,当然在@ConfigurationProperties加入注解的类上加@Component也可以使交于springboot管理。
  • 说白了@EnableConfigurationProperties 相当于把使用 @ConfigurationProperties 的类进行了一次注入。

.4.定义AspectLog注解,该注解用于标注需要打印执行时间的方法。

/**
 * 用于标记哪些方法需要记录耗时时间
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface   AspectLog {
}

6.定义计算方法执行耗时的切面类

//标注当前为一个切面类
@Aspect
//开启切面自动代理
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
//保证事务等切面先执行,也可以实现 PriorityOrdered接口,重写getOrder方法
@Order(Integer.MAX_VALUE)
public class AspectLogProcess {

    protected Logger logger = LoggerFactory.getLogger(getClass());

	//拦截所有标注了@AspectLog注解的方法
    @Around("@annotation(com.demo.AspectLog) ")
    public Object isOpen(ProceedingJoinPoint joinPoint) throws Throwable {
        long time = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        logger.info("method:{} run :{} ms", joinPoint.getSignature().toString(), (System.currentTimeMillis() - time));
        return result;
    }
}

5.定义配置信息对应类

@ConfigurationProperties(prefix = "aspectLog")
public class AspectLogProperties {
    private boolean enable;
    public boolean isEnable() { return enable; }
    public void setEnable(boolean enable) {  this.enable = enable;}
}

6.定义自动配置类

//标注当前类为一个配置类
@Configuration
//使用 @ConfigurationProperties 注解的类生效。
@EnableConfigurationProperties(AspectLogProperties.class)
public class AspectLogAutoConfiguration {

    @Bean
    //如果全局配置aspectLog.enable 为true才加载当前类到容器中,如果不为true,默认不加载到spring容器中
    @ConditionalOnProperty(prefix = "aspect-log", name = "enable",
            havingValue = "true", matchIfMissing = false)
    public AspectLogProcess aspectLogProcess() {
        return new AspectLogProcess();
    }
}

7.定义是否启用当前组件注解

/**
 * 用于控制是否启用自动配置
 **/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AspectLogAutoConfiguration.class)
public @interface EnableAspectLog {
}

8. 在 classpath(resources目录)下创建 META-INF/spring.factories文件

  • META-INF/spring.factories是spring这个文件中定义的类,都会被自动加载。多个配置使用逗号分割,换行用\
# key 对应springboot定义好的 org.springframework.boot.autoconfigure.EnableAutoConfiguration
# val 对应的是自己编写的 Configuration 配置类
# val 可以是多个,多个最后要加 \ 符号
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.AspectLogAutoConfiguration

9.打包发布组件,将当前项目install到maven本地仓库。


3.3.custom-spring-boot-spi-test

创建springBoot项目: custom-spring-boot-spi-test,进行打包测试

目录结构
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

1. pom.xml引入 custom-spring-boot-spi-starter

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.demo</groupId>
    <artifactId>custom-spring-boot-spi-test</artifactId>
    <version>1.0</version>
    <name>custom-spring-boot-spi-test</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.demo</groupId>
            <artifactId>custom-spring-boot-starter</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.编写controller

public class HelloController {
    @GetMapping("/sayHello")
    @AspectLog
    public String test1() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);//休眠2s
        return "Hello Hello !";
    }

    @GetMapping("/sayBye")
    @AspectLog
    public String test2() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);//休眠3s
        return "Bye Bye !";
    }
}

3.启动类使用@@EnableAspectLog启用custom-spring-boot-starter组件自动配置

@SpringBootApplication
@EnableAspectLog
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

4.配置文件application.properties设置aspect-log.enable=true开启方法执行时间打印

aspect-log.enable=true//默认关闭的

5.分别请求/sayHello、/sayByeBye,查看是否打印方法耗时(默认开启)
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析
6.修改全局配置文件application.properties ,关闭方法耗时打印

aspect-log.enable=false

aspectLog.enable=false可以看到有自动提示了,这是因为引入的jar中包含了元数据文件,详细见下图

  1. springboot 的Jar包含元数据文件,提供所有支持的配置属性的详细信息。用于IDE开发人员在用户使用application.properties 或application.yml文件时提供上下文帮助自动补全 。
  2. 主要的元数据文件是在编译器通过处理所有被@ConfigurationProperties注解的节点来自动生成的。
  3. 配置元数据位于jar文件中的META-INF/spring-configuration-metadata.json,它们使用一个具有”groups”或”properties”分类节点的简单JSON格式。
    【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

6. 分别请求/sayHello、/sayByeBye,查看是否打印方法耗时
【Java基础】JavaSPI机制及SpringSPI机制的使用及源码浅析

上一篇:JAVA jdk SPI调用其他jar包的方法


下一篇:【转】mysql数据库中实现内连接、左连接、右连接