一文讲透Java核心技术之高可扩展利器SPI

Posted 冰 河

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文讲透Java核心技术之高可扩展利器SPI相关的知识,希望对你有一定的参考价值。

大家好,我是冰河~~

SPI的概念

JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制

SPI的使用场景

Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。

大量的框架使用了Java的SPI技术,如下:

(1)JDBC加载不同类型的数据库驱动
(2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
(3)Spring中大量使用了SPI

  • 对servlet3.0规范
  • 对ServletContainerInitializer的实现
  • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

(4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现

SPI的使用

当服务的提供者,提供了接口的一种实现后,需要在Jar包的**META-INF/services/**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。

当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的**META/Services/**目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。

SPI中查找接口的实现类是通过java.util.ServiceLoader,而在java.util.ServiceLoader类中有一行代码如下:

// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";

这也就是说,我们必须将接口的配置文件写到Jar包的**META/Services/**目录下。

SPI实例

这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。

注意:实例是基于Java8进行开发的。

1.创建Maven项目

在IDEA中创建Maven项目spi-demo,如下:

编辑

2.编辑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">

	<artifactId>spi-demo</artifactId>
	<groupId>io.binghe.spi</groupId>
    <packaging>jar</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <modelVersion>4.0.0</modelVersion>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.创建类加载工具类

在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。

package io.binghe.spi.loader;

import java.util.ServiceLoader;

/**
 * @author binghe
 * @version 1.0.0
 * @description 类加载工具
 */
public class MyServiceLoader 

    /**
     * 使用SPI机制加载所有的Class
     */
    public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) 
        return ServiceLoader.load(clazz);
    

4.创建接口

在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:

package io.binghe.spi.service;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定义接口
 */
public interface MyService 

    /**
     *  打印信息
     */
    void print(String info);

5.创建接口的实现类

(1)创建第一个实现类MyServiceA

在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示:

package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;

/**
 * @author binghe
 * @version 1.0.0
 * @description 接口的第一个实现
 */
public class MyServiceA implements MyService 
    @Override
    public void print(String info) 
        System.out.println(MyServiceA.class.getName() + " print " + info);
    

(2)创建第二个实现类MyServiceB

在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示:

package io.binghe.spi.service.impl;

import io.binghe.spi.service.MyService;

/**
 * @author binghe
 * @version 1.0.0
 * @description 接口第二个实现
 */
public class MyServiceB implements MyService 
    @Override
    public void print(String info) 
        System.out.println(MyServiceB.class.getName() + " print " + info);
    

6.创建接口文件

在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:

io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB

7.创建测试类

在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示:

package io.binghe.spi.main;

import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;

import java.util.ServiceLoader;
import java.util.stream.StreamSupport;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试的main方法
 */
public class Main 

    public static void main(String[] args)
        ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
        StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
    

8.测试实例

运行Main类中的main()方法,打印出的信息如下所示:

io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World

Process finished with exit code 0

通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。

源码解析

这里,主要是对SPI的加载流程涉及到的java.util.ServiceLoader的源码的解析。

进入java.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。

public final class ServiceLoader<S>  implements Iterable<S> 

说明ServiceLoader类是可以遍历迭代的。

java.util.ServiceLoader类中定义了如下的成员变量:

// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";

// 需要加载的接口
private final Class<S> service;

// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;

// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;

// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;

可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的**META-INF/services/**目录下创建。

从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示:

//根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) 
	//获取当前线程的类加载器
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	//动态加载指定的类,将类加载到ServiceLoader中
	return ServiceLoader.load(service, cl);

方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示:

//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
	return new ServiceLoader<>(service, loader);

可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:

//构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) 
	//如果传入的Class对象为空,则判处空指针异常
	service = Objects.requireNonNull(svc, "Service interface cannot be null");
	//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
	loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
	acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
	reload();

继续跟**reload()**方法,如下所示。

//重新加载
public void reload() 
	//清空保存加载的实现类的LinkedHashMap
	providers.clear();
	//构造延迟加载的迭代器
	lookupIterator = new LazyIterator(service, loader);

继续跟进懒加载迭代器的构造函数,如下所示。

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

可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。

当我们在程序中迭代获取对象实例时,首先在成员变量**providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator**延迟加载迭代器进行加载。

迭代器进行逻辑判断的代码如下所示:

//迭代ServiceLoader的方法
public Iterator<S> iterator() 
	return new Iterator<S>() 
		//获取保存实现类的LinkedHashMap<String,S>的迭代器
		Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
		//判断是否有下一个元素
		public boolean hasNext() 
			//如果knownProviders存在元素,则直接返回true
			if (knownProviders.hasNext())
				return true;
			//返回延迟加载器是否存在元素
			return lookupIterator.hasNext();
		
		//获取下一个元素
		public S next() 
			//如果knownProviders存在元素,则直接获取
			if (knownProviders.hasNext())
				return knownProviders.next().getValue();
			//获取延迟迭代器lookupIterator中的元素
			return lookupIterator.next();
		

		public void remove() 
			throw new UnsupportedOperationException();
		

	;

LazyIterator加载类的流程如下代码所示

//判断是否拥有下一个实例
private boolean hasNextService() 
	//如果拥有下一个实例,直接返回true
	if (nextName != null) 
		return true;
	
	//如果实现类的全名为null
	if (configs == null) 
		try 
			//获取全文件名,文件相对路径+文件名称(包名+接口名)
			String fullName = PREFIX + service.getName();
			//类加载器为空,则通过ClassLoader.getSystemResources()方法获取
			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()) 
		//如果configs中没有更过的元素,则直接返回false
		if (!configs.hasMoreElements()) 
			return false;
		
		//解析包结构
		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 
		//加载类对象
		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 
		//通过c.newInstance()生成对象实例
		S p = service.cast(c.newInstance());
		//将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
		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);
	

最后,给出整个java.util.ServiceLoader的类,如下所示:

package java.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;


public final class ServiceLoader<S>  implements Iterable<S> 
    // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    private static final String PREFIX = "META-INF/services/";

    // 需要加载的接口
    private final Class<S> service;

    // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文环境
    private final AccessControlContext accSPI:Java的高可扩展利器

一文讲透 Tomcat 的类加载机制!揭秘类加载核心

一文搞懂Java/框架中的SPI机制

一文讲透贪心算法

一文讲透支付宝沙箱的基本应用

一文讲透java弱引用以及使用场景