avatar

目录
如何跨项目使用SPI扩展点

​ 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接口;凡是接口有该注解的,我们单独先把它拎出来;

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
import java.lang.annotation.*;

/**
* 标记一个 dubbo 接口是否为 SPI 接口
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPIInterface {
/**
* SPI所属应用
*
* @return
*/
String appName();

/**
* SPI所属的功能
*
* @return
*/
String function();

/**
* SPI描述
*
* @return
*/
String desc();
}

2、识别了那些事SPI接口,我们自然会想到该如何识别那些类是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
import org.springframework.stereotype.Component;
import java.lang.annotation.*;

/**
* 标记一个实现类为SPI的扩展实现
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface SPIProvider {
/**
* SPI实现类的描述
*
* @return
*/
String desc();

/**
* SPI服务提供方式:jar or rpc
*
* @return
*/
SPIProviderMode providerMode() default SPIProviderMode.JAR;

/**
* SPI分组标签
*
* @return
*/
String label();

/**
* 是否为默认的SPI实现
*
* @return
*/
boolean defaultProvider() default false;
}

凡是实现类有该注解的,我们会把它看做是某一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
    39
    import 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
    83
    import 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;

    @Component
    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;
    }
    }

    }
    }
    }

    @Autowired
    public void setSpiCollector(SPICollector spiCollector) {
    this.spiCollector = spiCollector;
    }

    @Autowired
    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
    65
    import 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;

    @Slf4j
    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);
    }
    }
    }
    }
文章作者: 海东青
文章链接: https://haohaogit.github.io/2020/06/04/%E5%A6%82%E4%BD%95%E8%B7%A8%E9%A1%B9%E7%9B%AE%E4%BD%BF%E7%94%A8SPI%E6%89%A9%E5%B1%95%E7%82%B9/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hexo
打赏
  • 微信
    微信
  • 支付宝
    支付宝