Analysis of the use and principles of SPI extension points in business | JD Logistics Technical Team

1 What is SPI

The full name of SPI is Service Provider Interface. In interface-oriented programming, we will abstract different interfaces according to different businesses, and then establish classes with different rules according to different business implementations. Therefore, an interface will implement multiple implementation classes. During the specific calling process, the corresponding implementation class is specified. , when the business changes, a new implementation class will be added, or an existing class will become obsolete, and the calling code needs to be changed, which is intrusive to a certain extent.
The overall mechanism diagram is as follows:

Java SPI is actually a dynamic loading mechanism implemented by a combination of "interface-based programming + strategy mode + configuration file".

2 Use of SPI in Jingxi’s business

2.1 Introduction

At present, the cooperation between the warehousing center and Jingxi BP is mainly through SPI expansion points. The advantage is that it is closed for modification and open for expansion. The middle office does not need to care about the business implementation details of BP and can achieve personalization by configuring the interfaces of extension points for different BPs. At present, Jingxi BP mainly provides two methods of interface implementation, one is the jar package method, and the other is the jsf interface.
The definition and implementation of the two methods are introduced below.

2.2 jar package method

2.2.1 Description and examples

The extension point interface inherits IDomainExtension. This interface is a plug-in interface in the dddplus package. The implementation class must use Extension (io.github.dddplus.annotation) annotation to mark the BP business party and interface identification name for personalized distinction. accomplish.
Taking the inventory inventory extension point in the library as an example, the interface is defined in the jar provided by the caller and is defined as follows:

public interface IProfitLossEnrichExt extends IDomainExtension {
    @Valid
    @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail.putExtendAttr(key, value)"})
    List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> var1);
}

The implementation class is defined in the service provider's jar, as follows:

实现类:/**
 * ProfitLossEnrichExtImpl
 * 批量盘盈亏数据丰富扩展
 *
 * @author jiayongqiang6
 * @date 2021-10-15 11:30
 */
@Extension(code = IPartnerIdentity.JX_CODE, value = "jxProfitLossEnrichExt")
@Slf4j
public class ProfitLossEnrichExtImpl implements IProfitLossEnrichExt {
    private SkuInfoQueryService skuInfoQueryService;

    @Override
    public @Valid @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail" +
            ".putExtendAttr(key, value)"}) List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> list) {
        ...
        return list;
    }

    @Autowired
    public void setSkuInfoQueryService(SkuInfoQueryService skuInfoQueryService) {
        this.skuInfoQueryService = skuInfoQueryService;
    }
}

This implementation class will rely on the jsf service SkuQueryService of the main data, and SkuInfoQueryService performs rpc encapsulation calls to SkuQueryService. Injected through Autowired, the consumer needs to be defined in the xml file, which is the same as the jsf consumer we usually introduce. An example is as follows: jx/spring-jsf-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jsf="http://jsf.jd.com/schema/jsf"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://jsf.jd.com/schema/jsf
       http://jsf.jd.com/schema/jsf/jsf.xsd"
       default-lazy-init="false" default-autowire="byName">
    <jsf:consumer id="skuQueryService" interface="com.jdwl.wms.masterdata.api.sku.SkuQueryService"
                  alias="${jsf.consumer.masterdata.alias}" protocol="jsf" check="false" timeout="10000"  retries="3"/>
</beans>

The user of the jar package can directly load the consumer resource file, or directly add the dependent services to the project directory manually. The first method is more convenient, but can easily cause conflicts. The second method, although troublesome, can avoid conflicts.

2.2.2 Testing of extension points

Because the extension point depends on Jeff's relationship, you need to add the configuration of the registration center and the related configuration of the dependent service in the configuration file. An example is as follows: application-config.properties

jsf.consumer.masterdata.alias=wms6-test
jsf.registry.index=i.jsf.jd.com

By loading the consumer resource file and configuration file in the unit test and loading all relevant dependencies, the through-call test of the interface can be achieved. As shown in the following code:

package com.zhongyouex.wms.spi.inventory;

import com.alibaba.fastjson.JSON;
import com.jdwl.wms.inventory.spi.difference.entity.ProfitLossBatchDetailExt;
import com.zhongyouex.wms.spi.inventory.service.SkuInfoQueryService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:jx/spring-jsf-consumer.xml"})
@PropertySource(value = {"classpath:application-config.properties"})
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = {"com.zhongyouex.wms"})
public class ProfitLossEnrichExtImplTest {
    @Resource
    SkuInfoQueryService skuInfoQueryService;

    ProfitLossEnrichExtImpl profitLossEnrichExtImpl = new ProfitLossEnrichExtImpl();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testEnrich() throws Exception {
        profitLossEnrichExtImpl.setSkuInfoQueryService(skuInfoQueryService);
        ProfitLossBatchDetailExt ext = new ProfitLossBatchDetailExt();
        ext.setSku("100008483105");
        ext.setWarehouseNo("6_6_618");
        ProfitLossBatchDetailExt ext1 = new ProfitLossBatchDetailExt();
        ext1.setSku("100009847591");
        ext1.setWarehouseNo("6_6_618");
        List<ProfitLossBatchDetailExt> list = new ArrayList<>();
        list.add(ext);
        list.add(ext1);
        profitLossEnrichExtImpl.enrich(list);
        System.out.write(JSON.toJSONBytes(list));
    }
}

//Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme

2.3 jsf interface method

The extension point implementation in jsf method is the same as the jar package method. The difference is that this method does not need to rely on the jar implemented by the service provider and does not need to load specific implementation classes. Identify the extension point and call the extension point by configuring the Jeff alias of the jsf interface.

3 SPI principle analysis

3.1dddplus

ExtensionDef in the dddplus-runtime package is mainly used to load extension point beans to InternalIndexer:

public void prepare(@NotNull Object bean) {
    this.initialize(bean);
    InternalIndexer.prepare(this);
}

private void initialize(Object bean) {
    Extension extension = (Extension)InternalAopUtils.getAnnotation(bean, Extension.class);
    this.code = extension.code();
    this.name = extension.name();
    if (!(bean instanceof IDomainExtension)) {
        throw BootstrapException.ofMessage(new String[]{bean.getClass().getCanonicalName(), " MUST implement IDomainExtension"});
    } else {
        this.extensionBean = (IDomainExtension)bean;
        Class[] var3 = InternalAopUtils.getTarget(this.extensionBean).getClass().getInterfaces();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Class extensionBeanInterfaceClazz = var3[var5];
            if (extensionBeanInterfaceClazz.isInstance(this.extensionBean)) {
                this.extClazz = extensionBeanInterfaceClazz;
                log.debug("{} has ext instance:{}", this.extClazz.getCanonicalName(), this);
                break;
            }
        }

    }
}

3.2 java spi

Through the simple demo above, we can see that the most critical implementation is the ServiceLoader class. You can take a look at the source code of this class, as follows:

public final class ServiceLoader<S> implements Iterable<S> {
 2 3 4     //扫描目录前缀 5     private static final String PREFIX = "META-INF/services/";
 6 7     // 被加载的类或接口 8     private final Class<S> service;
 910     // 用于定位、加载和实例化实现方实现的类的类加载器11     private final ClassLoader loader;
1213     // 上下文对象14     private final AccessControlContext acc;
1516     // 按照实例化的顺序缓存已经实例化的类17     private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
1819     // 懒查找迭代器20     private java.util.ServiceLoader.LazyIterator lookupIterator;
2122     // 私有内部类,提供对所有的service的类的加载与实例化23     private class LazyIterator implements Iterator<S> {
24         Class<S> service;
25         ClassLoader loader;
26         Enumeration<URL> configs = null;
27         String nextName = null;
2829         //...30         private boolean hasNextService() {
31             if (configs == null) {
32                 try {
33                     //获取目录下所有的类34                     String fullName = PREFIX + service.getName();
35                     if (loader == null)
36                         configs = ClassLoader.getSystemResources(fullName);
37                     else38                         configs = loader.getResources(fullName);
39                 } catch (IOException x) {
40                     //...41                 }
42                 //....43             }
44         }
4546         private S nextService() {
47             String cn = nextName;
48             nextName = null;
49             Class<?> c = null;
50             try {
51                 //反射加载类52                 c = Class.forName(cn, false, loader);
53             } catch (ClassNotFoundException x) {
54             }
55             try {
56                 //实例化57                 S p = service.cast(c.newInstance());
58                 //放进缓存59                 providers.put(cn, p);
60                 return p;
61             } catch (Throwable x) {
62                 //..63             }
64             //..65         }
66     }
67 }

The above code only posts some key implementations. Interested readers can study it by themselves. The more intuitive main process of spi loading is posted below for reference:

4 Summary

The two methods of providing SPI have their own advantages and disadvantages. The jar package method has low deployment cost and many dependencies, which increases the configuration cost of the caller; the jsf interface method has high deployment cost, but the caller has few dependencies and only needs to identify different BPs through aliases.

Summarize the benefits that spi can bring:

  • Extension and decoupling can be achieved without changing the source code.
  • Implementing extensions is barely intrusive to the original code.
  • You only need to add configurations to achieve expansion, which is consistent with the opening and closing principle.

Author: JD Logistics Jia Yongqiang

Source: JD Cloud Developer Community Ziyuanqishuo Tech Please indicate the source when reprinting

Spring Boot 3.2.0 is officially released. The most serious service failure in Didi’s history. Is the culprit the underlying software or “reducing costs and increasing laughter”? Programmers tampered with ETC balances and embezzled more than 2.6 million yuan a year. Google employees criticized the big boss after leaving their jobs. They were deeply involved in the Flutter project and formulated HTML-related standards. Microsoft Copilot Web AI will be officially launched on December 1, supporting Chinese PHP 8.3 GA Firefox in 2023 Rust Web framework Rocket has become faster and released v0.5: supports asynchronous, SSE, WebSockets, etc. Loongson 3A6000 desktop processor is officially released, the light of domestic production! Broadcom announces successful acquisition of VMware
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10313986