系列目录
spi 05-dubbo adaptive extension 自适应拓展
回顾
学习了 java 的 SPI 和 dubbo 的 SPI 实现之后,希望实现一个属于自己的 SPI 框架。
希望具有如下特性:
(1)类似 dubbo 的真正的惰性加载,而不是遍历一堆一需要的实例
(2)并发安全考虑
(3)支持基于名称获取实现类,后期可以添加更多的特性支持。类似 spring 的 IOC
(4)尽可能的简化实现
使用演示
类实现
- Say.java
[java]
1
2
3
4
5
6@SPI
public interface Say {
void say();
}
- SayBad.java
[java]
1
2
3
4
5
6
7
8public class SayBad implements Say {
@Override
public void say() {
System.out.println("bad");
}
}
- SayGood.java
[java]
1
2
3
4
5
6
7
8public class SayGood implements Say {
@Override
public void say() {
System.out.println("good");
}
}
文件配置
在 META-INF/services/
文件夹下定义文件 com.github.houbb.spi.bs.spi.Say
内容如下:
[plaintext]
1
2good=com.github.houbb.spi.bs.spi.impl.SayGood
bad=com.github.houbb.spi.bs.spi.impl.SayBad
测试案例
[java]
1
2
3
4
5
6
7ExtensionLoader<Say> loader = SpiBs.load(Say.class);
Say good = loader.getExtension("good");
Say bad = loader.getExtension("bad");
good.say();
bad.say();
日志输出:
[plaintext]
1
2good
bad
整体目录
[plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17├─annotation
│ SPI.java
│
├─api
│ │ IExtensionLoader.java
│ │
│ └─impl
│ ExtensionLoader.java
│
├─bs
│ SpiBs.java
│
├─constant
│ SpiConst.java
│
└─exception
SpiException.java
源码分析
annotation 注解
@SPI
类似于 dubbo 的注解,标识一个接口为 SPI 接口。
这样严格控制,便于后期拓展和管理,降低代码复杂度。
[java]
1
2
3
4
5@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface SPI {
}
bs 引导类
- SpiBs.java
核心引导类
[java]
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
38public final class SpiBs {
private SpiBs(){}
/**
* 拓展加载类 map
* @since 0.0.1
*/
private static final Map<Class, ExtensionLoader> EX_LOADER_MAP = new ConcurrentHashMap<>();
/**
* 加载实例
* @param clazz 类
* @param <T> 泛型
* @return 结果
* @since 0.0.1
*/
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> load(Class<T> clazz) {
ArgUtil.notNull(clazz, "clazz");
ExtensionLoader extensionLoader = EX_LOADER_MAP.get(clazz);
if(EX_LOADER_MAP.get(clazz) != null) {
return extensionLoader;
}
// DLC
synchronized (EX_LOADER_MAP) {
extensionLoader = EX_LOADER_MAP.get(clazz);
if(extensionLoader == null) {
extensionLoader = new ExtensionLoader(clazz);
}
}
return extensionLoader;
}
}
api 核心实现
- ExtensionLoader.java
[java]
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202import com.github.houbb.heaven.annotation.CommonEager;
import com.github.houbb.heaven.annotation.ThreadSafe;
import com.github.houbb.heaven.response.exception.CommonRuntimeException;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.houbb.spi.annotation.SPI;
import com.github.houbb.spi.api.IExtensionLoader;
import com.github.houbb.spi.constant.SpiConst;
import com.github.houbb.spi.exception.SpiException;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 默认实现
* @author binbin.hou
* @since 0.0.1
*/
@ThreadSafe
public class ExtensionLoader<T> implements IExtensionLoader<T> {
/**
* 接口定义
* @since 0.0.1
*/
private final Class<T> spiClass;
/**
* 类加载器
* @since 0.0.1
*/
private final ClassLoader classLoader;
/**
* 缓存的对象实例
* @since 0.0.1
*/
private final Map<String, T> cachedInstances = new ConcurrentHashMap<>();
/**
* 实例别名 map
* @since 0.0.1
*/
private final Map<String, String> classAliasMap = new ConcurrentHashMap<>();
public ExtensionLoader(Class<T> spiClass, ClassLoader classLoader) {
spiClassCheck(spiClass);
ArgUtil.notNull(classLoader, "classLoader");
this.spiClass = spiClass;
this.classLoader = classLoader;
// 初始化配置
this.initSpiConfig();
}
public ExtensionLoader(Class<T> spiClass) {
this(spiClass, Thread.currentThread().getContextClassLoader());
}
/**
* 获取对应的拓展信息
*
* @param alias 别名
* @return 结果
* @since 0.0.1
*/
@Override
public T getExtension(String alias) {
ArgUtil.notEmpty(alias, "alias");
//1. 获取
T instance = cachedInstances.get(alias);
if(instance != null) {
return instance;
}
// DLC
synchronized (cachedInstances) {
instance = cachedInstances.get(alias);
if(instance == null) {
instance = createInstance(alias);
cachedInstances.put(alias, instance);
}
}
return instance;
}
/**
* 实例
* @param name 名称
* @return 实例
* @since 0.0.1
*/
@SuppressWarnings("unchecked")
private T createInstance(String name) {
String className = classAliasMap.get(name);
if(StringUtil.isEmpty(className)) {
throw new SpiException("SPI config not found for spi: " + spiClass.getName()
+" with alias: " + name);
}
try {
Class clazz = Class.forName(className);
return (T) clazz.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new SpiException(e);
}
}
/**
* 参数校验
*
* 1. 不能为 null
* 2. 必须是接口
* 3. 必须指定 {@link com.github.houbb.spi.annotation.SPI} 注解
* @param spiClass spi 类
* @since 0.0.1
*/
private void spiClassCheck(final Class<T> spiClass) {
ArgUtil.notNull(spiClass, "spiClass");
if(!spiClass.isInterface()) {
throw new SpiException("Spi class is not interface, " + spiClass);
}
if(!spiClass.isAnnotationPresent(SPI.class)) {
throw new SpiException("Spi class is must be annotated with @SPI, " + spiClass);
}
}
/**
* 初始化配置文件名称信息
*
* 只加载当前类的文件信息
* @since 0.0.1
*/
private void initSpiConfig() {
// 文件的读取
String fullName = SpiConst.JDK_DIR+this.spiClass.getName();
try {
Enumeration<URL> urlEnumeration = this.classLoader
.getResources(fullName);
// 没有更多元素
if(!urlEnumeration.hasMoreElements()) {
throw new SpiException("SPI config file for class not found: "
+ spiClass.getName());
}
// 获取第一个元素
URL url = urlEnumeration.nextElement();
List<String> allLines = readAllLines(url);
// 构建 map
if(CollectionUtil.isEmpty(allLines)) {
throw new SpiException("SPI config file for class is empty: " + spiClass.getName());
}
for(String line : allLines) {
String[] lines = line.split(SpiConst.SPLITTER);
classAliasMap.put(lines[0], lines[1]);
}
} catch (IOException e) {
throw new SpiException(e);
}
}
/**
* 读取每一行的内容
* @param url url 信息
* @return 结果
* @since 0.0.1
*/
@CommonEager
private List<String> readAllLines(final URL url) {
ArgUtil.notNull(url, "url");
List<String> resultList = new ArrayList<>();
try(InputStream is = url.openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
// 按行读取信息
String line;
while ((line = br.readLine()) != null) {
resultList.add(line);
}
} catch (IOException e) {
throw new CommonRuntimeException(e);
}
return resultList;
}
}
其他
其他的只是一些常量定义等。
完整代码见:
进步一思考
这里只实现了最基本的功能,可以后续添加更多丰富的特性。
还有一个问题就是,我们能不能像使用 jdk spi 那样,去使用 google auto,直接一个注解,帮我们省略掉文件创建的过程呢?
当然是可以的,下一节,我们就来实现一个这样的功能。