SPI-服务扩展点可以使得项目结构更加解耦,比如我司在支付方式模块中,全平台都可以使用在线支付,单由于业务场景要求,不同的业务实例会有定制化的支付方式,如非国有性质的采购人可以申请使用账期支付等。原先的技术实现是有订单模块代码中根据业务实例写死一堆的if-else业务实现,随着接入业务的增加,代码变得越来越臃肿和不可维护。为了该模块更具扩展性和易维护性,我们希望通过由订单中心提供支付方式SPI扩展接口,不同的业务线可以更灵活的配置各自的支付方式。
在一个项目中定义一个扩展接口,然后写不同的实现,并在项目中的META-INF/services路径下写好扩展点实现路径,然后项目在启动的时候回加载这些实现;现在的问题是如何在不同的项目中使用SPI,也就是项目A定义了一个SPI接口,项目B和项目C在实现了其接口后,项目A怎么找到这些实现?这里大概有两种方式:
- 通过JAR包,项目B和项目C实现项目A的SPI接口后,打包成JAR后放到项目A中;
- 通过dubbo注册中心机制,项目B和项目C在实现项目A的SPI接口后,向注册中心注册其实现,然后项目A再通过注册中心获取具体实现类和方法;
由于小菜不想每次修改实现或有新接入方时都要重新打包,并复制给SPI提供方,来来回回,不仅繁琐,而且中间也容易出错。所以这里主要探讨第二种方案是怎么做的。
注解和反射技术是做这件事的一大利器。
1、首先,应该定义一个注解,用于标识那些接口是SPI接口;凡是接口有该注解的,我们单独先把它拎出来;
1 | import java.lang.annotation.*; |
2、识别了那些事SPI接口,我们自然会想到该如何识别那些类是SPI接口的实现类;同理:
1 | import org.springframework.stereotype.Component; |
凡是实现类有该注解的,我们会把它看做是某一SPI接口的实现类;
3、假设项目B和项目C实现项目A的SPI接口,实现完之后,我们希望该实现类能够已dubbo服务的形式上传到注册中心;
4、项目A通过注册中心找到对应的服务类,具体由SPIProvider的label标签区分不同的业务线实现;
代码示例
具体操作上我司并不是直接通过dubbo注册中心实现,而是自己开发的一套工具,通过将SPI服务上传到指定的“注册中心”中,SPI提供者再到这里获取相应的实现类;
- 根据SPIProvider注解,加载项目B和项目C的扩展点实现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
39import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
public class SPIClientLoader extends SPILoaderTemplate {
private static final Logger log = LoggerFactory.getLogger(SPIClientLoader.class);
public SPIClientLoader(ApplicationContext applicationContext) {
super(applicationContext);
}
protected void loadAll() {
Map<String, Object> beansWithAnnotation = this.getApplicationContext().getBeansWithAnnotation(SPIProvider.class);
Optional.ofNullable(beansWithAnnotation).ifPresent((t) -> {
t.forEach((name, bean) -> {
Class<?> clazz = ClassUtils.getUserClass(bean.getClass());
SPIProvider spiProviderAnnotation = (SPIProvider)clazz.getAnnotation(SPIProvider.class);
Class<?>[] interfaces = clazz.getInterfaces();
Class[] var5 = interfaces;
int var6 = interfaces.length;
for(int var7 = 0; var7 < var6; ++var7) {
Class<?> interfaceClazz = var5[var7];
if (interfaceClazz.isAnnotationPresent(SPIInterface.class) && !spiProviderAnnotation.defaultProvider() && !SPIProviderMode.JAR.equals(spiProviderAnnotation.providerMode())) {
SPIIdentifier spiIdentifier = SPIIdentifier.builder().defaultProvider(spiProviderAnnotation.defaultProvider()).mode(spiProviderAnnotation.providerMode().name()).label(spiProviderAnnotation.defaultProvider() ? null : spiProviderAnnotation.label()).interfaceName(interfaceClazz.getName()).desc(spiProviderAnnotation.desc()).build();
SPIClientManager.getSpiProviderMap().put(spiIdentifier, bean);
}
}
});
});
log.info("[SPIClientLoader@TRADE-SPI] load total {} beans with SPIProvider annotation", beansWithAnnotation.size());
}
} - 获取SPI实现类后,把他们上传到指定的“注册中心”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
83import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
public class SPIClientManager implements ApplicationContextAware, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(SPIClientManager.class);
private static Map<SPIIdentifier, Object> spiProviderMap = new ConcurrentHashMap();
private ApplicationContext applicationContext;
private SPICollector spiCollector;
private ClientCollectorConfiguration clientCollectorConfiguration;
public SPIClientManager() {
}
public static Map<SPIIdentifier, Object> getSpiProviderMap() {
return spiProviderMap;
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void afterPropertiesSet() {
SPILoaderTemplate spiClientLoader = new SPIClientLoader(this.applicationContext);
spiClientLoader.load();
(new Thread(() -> {
this.report();
})).start();
}
private void report() {
if (this.clientCollectorConfiguration == null) {
log.warn("[SPIClientManager@TRADE-SPI] No collector config found, ignore report spi info");
} else {
log.info("[SPIClientManager@TRADE-SPI] Start to report spi provider on client side");
List<SPIProviderModel> implementationModelList = (List)getSpiProviderMap().keySet().stream().filter((spiIdentifier) -> {
return !spiIdentifier.isDefaultProvider();
}).map((spiIdentifier) -> {
return SPIProviderModel.fromSPIIdentifier(this.clientCollectorConfiguration.getApplicationName(), spiIdentifier);
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(implementationModelList)) {
int retry = 0;
while(retry < 3) {
try {
this.spiCollector.report(this.clientCollectorConfiguration.getCollectorDomain() + "/api/star-chain/spiProvider/implementation/report", implementationModelList);
break;
} catch (Throwable var4) {
if (retry == 2) {
log.error("[SPIClientManager@TRADE-SPI] Report spi provider on client side failed, ignore it!!!");
}
++retry;
}
}
}
}
}
public void setSpiCollector(SPICollector spiCollector) {
this.spiCollector = spiCollector;
}
public void setClientCollectorConfiguration(ClientCollectorConfiguration clientCollectorConfiguration) {
this.clientCollectorConfiguration = clientCollectorConfiguration;
}
} - 具体上传类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
65import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
4j
public class SPICollector {
public void report(String url, Object payload) throws IOException {
String json = JSON.toJSONString(payload);
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
log.info("[SPICollector@TRADE-SPI] Start to report spi info with url:{} and request body:{}", url, json);
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
conn.setReadTimeout(10000);
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(json);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
log.info("[SPICollector@TRADE-SPI] report spi info result:{}", result);
} catch (Throwable e) {
log.warn("[SPICollector@TRADE-SPI] 发送 POST 请求出现异常:{}", e);
throw e;
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
log.warn("[SPICollector@TRADE-SPI] close stream occur exception:{}",
ex);
}
}
}
}