文章目录
一.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被依赖进来。
- 这些SPI的接口是由
Java SPI本质上其实就是“基于接口编程+策略模式+配置文件
”组合实现的动态加载机制
。
通过下图来看,完成spi的实现,需要哪些操作,需要遵循哪些规范?
SPI机制使用规范:
- 当服务的提供者编写
服务规范接口
的一种实现,在Jar包的META-INF/Services/
目录中同时创建一个以服务规范接口命名的文件
。该文件里存放实现该服务接口的具体实现类
。而当外部程序装配这个实现模块
的时候,就能通过该模块Jar包META-INF/Services/的配置文件找到具体的实现类名。并装载实例化
,完成模块的注入。基于这样的一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。
那么问题来了,
SPI的接口
是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)
来加载的。SPI的实现类
是由系统类加载器(System ClassLoader)
来加载的。
- 引导类加载器在加载时是
无法找到SPI的实现类的
,因为双亲委派模型
中规定,引导类加载器BootstrapClassloader无法委派系统类加载器AppClassLoader来加载。这时候,该如何解决此问题?线程上下文类加载
由此诞生,它的出现也破坏了类加载器的双亲委派模型,使得程序可以进行逆向类加载
。
2.实现一个自定义的SPI?
项目结构
- 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
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.规范接口的实现
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目录
下新建一个名为标准服务接口全限定名
的文件,并将具体实现类全名写入。 - 该文件的名称是
服务接口的全限定类名
,内容则是服务接口实现类的全限定类名
,*如果是多个实现类则用换行符分割
文件内容
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.规范接口的实现
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目录
下新建一个名为标准服务接口全限定名
的文件,并将具体实现类全名写入。 - 该文件的名称是
服务接口的全限定类名
,内容则是服务接口实现类的全限定类名
,*如果是多个实现类则用换行符分割
文件内容
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.动态调用规范接口实现,如果没有找到对应的实现则抛出异常
- 如果调用
ApiFactory.invoker()
没有找到具体实现抛出异常 - 如果发现多个实现,则调用实现的
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>
测试结果
b. 引入spi-provider1,执行测试方法
<dependency>
<groupId>com.demo</groupId>
<artifactId>spi-provider1</artifactId>
<version>1.0</version>
</dependency>
c. 引入spi-provider2,执行测试方法
<dependency>
<groupId>com.demo</groupId>
<artifactId>spi-provider2</artifactId>
<version>1.0</version>
</dependency>
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>
3.JDBC数据库驱动包中SPI机制分析
mysql-connector-java-xxx.jar
就有一个/META-INF/services/java.sql.Driver
里面内容是 com.mysql.jdbc.Driver
在引入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
实现思路:
- 读取配置文件,获取实现类的全名称字符串;
- 使用 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();
}
}
执行结果
5.JavaSPI源码浅析
源码主要加载流程如下:
-
应用程序调用
ServiceLoader.load()
方法 创建一个新的ServiceLoader,并实例化该类中的成员变量
;- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- ookupIterator(实现迭代器功能)
-
应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载。
- 读取
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); }
- 通过反射方法
Class.forName()
加载类对象,并用instance()
方法将类实例化。 - 把实例化后的类缓存到
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类的实例是
不安全
的。
- 虽然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包中。
- 在SpringBoot的自动装配过程中,最终会通过
1.2.对比ServiceLoader
- 一个是加载
META-INF/services/
目录下的配置;一个是加载META-INF/spring.factories
固定文件的配置。思路都一样。 - 两个都是利用
ClassLoader
和ClassName
来完成操作的。不同的是Java的ServiceLoader
加载配置和实例化都是自己来实现,并且不能按需加载;SpringFactoriesLoader
既可以单独加载配置
然后按需实例化也可以实例化全部。
2.源码浅析
分析org.springframework.boot.SpringBootApplication.run()
方法 发现 SpringBoot的启动包含new SpringApplication
和执行run方法
两个过程,new的时候有这么个逻辑:(getSpringFactoriesInstances
)
getSpringFactoriesInstances() 主要做2件事情: 1. 加载类的全限定名列表。2. 根据类名通过反射实例化。
重点在于: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的相关要点。
这个是SpringBoot的@SpringBootApplication
注解,里面还有一个 @EnableAutoConfiguration
注解,开启自动配置的。
可以发现这个自动配置注解
在另一个工程,而这个工程里也有个spring.factories
文件,如下图:
SpringBoot的目录下有个spring.factories
文件,里面都是一些接口的具体实现类
,可以由SpringFactoriesLoader加载
。
说明实现一个starter必须要在classpath下面创建
META-INF/spring.factories
文件,通过org.springframework.boot.autoconfigure.EnableAutoConfiguration
标明需要自动配置
的类的全限定名
3.2.custom-spring-boot-starter
新建springBoot项目:custom-spring-boot-starter
目录结构
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
,进行打包测试
目录结构
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,查看是否打印方法耗时(默认开启
)
6.修改全局配置文件application.properties
,关闭方法耗时打印
aspect-log.enable=false
aspectLog.enable=false可以看到有自动提示了,这是因为引入的jar中包含了元数据文件,详细见下图
- springboot 的Jar包含
元数据文件
,提供所有支持的配置属性的详细信息。用于IDE开发人员在用户使用application.properties 或application.yml文件时提供上下文帮助
和自动补全 。
- 主要的元数据文件是在编译器通过处理所有被
@ConfigurationProperties注解
的节点来自动生成的。- 配置元数据位于jar文件中的
META-INF/spring-configuration-metadata.json
,它们使用一个具有”groups
”或”properties
”分类节点的简单JSON格式。
6. 分别请求/sayHello、/sayByeBye,查看是否打印方法耗时