谈谈一个框架的扩展加载

springboot 中的扩展加载(SpringFactoriesLoader)

在 SpringBoot 启动类上都会标注@SpringBootApplication这个注解,其中的最重要的组成是@EnableAutoConfiguration, 再进入会发现是 @Import (AutoConfigurationImportSelector.class)这个注解在起作用。

自动加载配置的逻辑是:selectImports->selectImports->getAutoConfigurationEntry,不过这里不是分析 SpringBoot 启动流程,我们关心的是他如何加载配置的。可以在getAutoConfigurationEntry看到使用的是SpringFactoriesLoader,主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static List < String > loadFactoryNames(Class < ? > factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
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 {
// 扫描所有 jar 包类路径下 META-INF/spring.factories
Enumeration < URL > urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap < > ();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 把扫描到的这些文件的内容包装成 properties 对象
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())) {
// 从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中
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);
}
}

这样就把我们配置在 META-INF/spring.factories 中的配置类给找出来了,完成了自动配置的加载(spring 的 @import 机制会完成这个 bean 的注入过程)

Java SPI

SPI 全称 Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件,可以根据使用者的配置,来加载接口的具体实现类。

使用

先看看怎么使用。首先创建一个Repository接口,和两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Repository {
void connect();
}
public class MongoRepository implements Repository {
@Override
public void connect() {
System.out.println("mongo repository");
}
}
public class MysqlRepository implements Repository {
@Override
public void connect() {
System.out.println("mysql connected");
}
}

然后在META-INF/services下新建一个与接口同名的文件:com.java.Repository,里面的内容是具体实现类的类的全名:

1
2
com.java.impl.MongoRepository
com.java.MysqlRepository

运行方法:

1
2
3
4
5
6
7
8
@Test
public void jdkSpi() {
ServiceLoader<Repository> serviceLoader = ServiceLoader.load(Repository.class);
Iterator<Repository> iterable = serviceLoader.iterator();
while (iterable.hasNext()) {
System.out.println(iterable.next().getClass().getName());
}
}

可以在控制台上看到,输出了配置文件中的类,也就是代表了配置文件中的类已经被加载了。

原理

jdk 的这种机制,把约定(接口)和实现分离,我们当引入具体实现的时候,不会给服务使用方带来任何代码上的修改,并且只有服务方主动使用的时候才会真正的去初始化,完成动态的加载。

可以看到 jdk 是通过 ServiceLoader 来实现, 它的 load() 流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程上下文加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 如果传入 ClassLoader 为空则使用系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 访问权限判断
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 往下面看
reload();
}

上面的内容是获取当前类的加载器,传给成员变量loader,并进入reload流程, reload是初始化了一个LazyIterator对象, 看看它的next():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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);
}
}

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 {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

// 判断是否有,如果有则赋值给 nextName
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;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

可以看到,整个流程还是很清晰的,就是去找META-INF/service下对应的class全名的文件,然后返回出去。

Dubbo 中 扩展点的加载

Dubbo 文档中描述 Dubbo SPI 改进了 JDK SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了(不会上报真正失败的原因)。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

dubbo的扩展点加载具体以下特点:

  • 自动包装: ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。Wrapper类可以为扩展点之上做一些处理,类似于 AOP
  • 自动装配: 扩展点实现类的成员如果是其他扩展点,ExtensionLoader会自动注入依赖的扩展点。
  • 扩展点自适应: ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是哪一个扩展点实现。
  • 扩展点自动激活,如果扩展点有多个实现类,可以使用@Activate来自动激活,简化配置

使用

借用官网的例子,新建接口Robot和两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这里需要标注是一个 SPI 扩展
@SPI
public interface Robot {
void sayHello();
}

public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}

public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}

然后在 META-INF/dubbo/ 下新建与接口同名的文件com.code.service.Robot,并配置扩展实现类:

1
2
bumblebee = com.code.service.impl.Bumblebee
optimusPrime = com.code.service.impl.OptimusPrime

执行:

1
2
3
4
5
public static void main(String[] args) {
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
}

控制台输出:

1
Hello, I am Optimus Prime.

加载扩展对象

上面的使用例子可以看出,首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。

getExtensionLoader

getExtensionLoader()用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
// 从缓存中获取对应扩展点的 ExtensionLoader 实例
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
// 若缓存没有命中,则创建一个实例
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
getExtension

getExtension(String name)获取扩展类对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 获取默认的拓展实现类
return getDefaultExtension();
}
// Holder 用于持有目标对象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 双重检查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建拓展实例
instance = createExtension(name);
// 设置实例到 holder 中
holder.set(instance);
}
}
}
return (T) instance;
}

上述逻辑较为简单,尝试从缓存中获取对象,若缓存没有则创建扩展对象,创建对象createExtension(String name)流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private T createExtension(String name) {
// 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通过反射创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向实例中注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
// 所以最终返回的是一个 wrapper 类的实例
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}

这里主要分为四个步骤

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖(dubbo 的 IOC 机制)
  4. 将拓展对象包裹在相应的 Wrapper 对象中
获取所有扩展类

getExtensionClasses() 会根据配置文件解析出 扩展项->扩展类 的映射关系表(Map<名称, 拓展类>),流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
private Map<String, Class<?>> getExtensionClasses() {
// 从缓存中获取已加载的拓展类
Map<String, Class<?>> classes = cachedClasses.get();
// 双重检查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载拓展类
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}

// loadExtensionClasses 加载扩展类
private Map<String, Class<?>> loadExtensionClasses() {
// 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的(接口类型)
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 对 SPI 注解内容进行切分
String[] names = NAME_SEPARATOR.split(value);
// 检测 SPI 注解内容是否合法,不合法则抛出异常
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension...");
}
// 设置默认名称
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 加载指定文件夹下的配置文件
// META-INF/dubbo/internal/
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
// META-INF/dubbo/
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
// META-INF/services/
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}

// loadDirectory 加载指定文件夹配置
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
// fileName = 文件夹路径 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 根据文件名加载所有的同名文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// 加载资源, 这个方法做的事情比较多
// 1. 解析配置文件(=号分割)
// 2. 通过反射加载扩展实现类
// 3. 调用 loadClass 缓存各种类
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("...");
}
}

获取缓存的逻辑不复杂,就是有点长, 到这里基本就完成了上述的1、2步,获取了扩展实现类并通过反射创建了扩展对象

Dubbo IOC

Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 遍历目标类的所有方法
for (Method method : instance.getClass().getMethods()) {
// 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// 获取 setter 方法参数类型
Class<?> pt = method.getParameterTypes()[0];
try {
// 获取属性名,比如 setName 方法对应属性名 name
String property = method.getName().length() > 3 ?
method.getName().substring(3, 4).toLowerCase() +
method.getName().substring(4) : "";
// 从 ObjectFactory 中获取依赖对象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通过反射调用 setter 方法设置依赖
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method...");
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}

自适应扩展

Dubbo 的扩展都是通过 SPI 机制进行加载,有时候希望扩展被调用前,根据运行参数进行加载。这里感觉起来就很矛盾,既然没有被加载,怎么去调用呢? Dubbo 的自适应扩展机制解决了这个问题。

这里的代码有些复杂,主要的流程是通过@Adaptive注解标注在类或方法上,如果是标注在类上就会生成代理类(这种模式比较简单),如果标注在方法上,就会由框架自动生成加载扩展的逻辑。


谈谈一个框架的扩展加载
https://haobin.work/2019/12/25/架构源码/谈谈一个框架的扩展加载/
作者
Leo Hao
发布于
2019年12月25日
许可协议