Front knowledge

What is the SPI

If you don’t know anything about SPI, you can read this article first and then read the following

preface

Assuming you already have some knowledge of SPI, those of you who have used the SPI provided by the JDK will find that the JDK’S SPI is not available for loading on demand. How to solve this problem?

There are two ideas, one is to implement a set of SPI, the other is a common way to implement components, that is, when the current component cannot meet, you can use other components or add a proxy layer. The implementation idea of this article is to use spring IOC, which is essentially a key-value map. The objects generated by JDK SPI are injected into spring IOC container, which indirectly has the key–>value mapping function

Implementation approach

  • When the project starts, spi is used to load classes and generate objects
  • Inject the generated objects into the Spring container
  • In the business project, the SPI generated bean object is referenced on demand using the @Autowired + @Qualifier annotation

Core code snippet

1. Spi loading implementation

 public Map<String,T> getSpiMap(Class<T> clz){
        listServicesAndPutMapIfNecessary(clz,true);
        return spiMap;
    }


    private List<T> listServicesAndPutMapIfNecessary(Class<T> clz,boolean isNeedPutMap){
        List<T> serviceList = new ArrayList();
        ServiceLoader<T> services = ServiceLoader.load(clz);
        Iterator<T> iterator = services.iterator();
        while(iterator.hasNext()){
            T service = iterator.next();
            serviceList.add(service);
            setSevices2Map(isNeedPutMap, service);
        }
        return serviceList;
    }

    @SneakyThrows
    private void setSevices2Map(boolean isNeedPutMap, T service) {
        if(isNeedPutMap){ String serviceName = StringUtils.uncapitalize(service.getClass().getSimpleName()); service = getProxyIfNecessary(service); spiMap.put(serviceName,service); }}Copy the code

2. Inject the Spring container

public class SpiRegister implements ImportBeanDefinitionRegistrar.BeanFactoryAware {


    private DefaultListableBeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registerSingleton(importingClassMetadata);

    }

    private void registerSingleton(AnnotationMetadata importingClassMetadata) { Class<? > spiInterface = getSpiInterface(importingClassMetadata);if(spiInterface ! =null){ Map<String,? > spiMap =new SpiFactory().getSpiMap(spiInterface);
            if(MapUtil.isNotEmpty(spiMap)){ spiMap.forEach((beanName,bean) -> { registerSpiInterfaceSingleton(spiInterface, bean); beanFactory.registerSingleton(beanName,bean); }); }}}private void registerSpiInterfaceSingleton(Class
        spiInterface, Object bean) {
        Spi spi = spiInterface.getAnnotation(Spi.class);
        String defalutSpiImplClassName = spi.defalutSpiImplClassName();
        if(StringUtils.isBlank(defalutSpiImplClassName)){
            defalutSpiImplClassName = spi.value();
        }

        String beanName = bean.getClass().getName();
        if(bean.toString().startsWith(SpiProxy.class.getName())){ SpiProxy spiProxy = (SpiProxy) Proxy.getInvocationHandler(bean);  beanName = spiProxy.getTarget().getClass().getName(); }if(beanName.equals(defalutSpiImplClassName)){ String spiInterfaceBeanName = StringUtils.uncapitalize(spiInterface.getSimpleName()); beanFactory.registerSingleton(spiInterfaceBeanName,bean); }}privateClass<? > getSpiInterface(AnnotationMetadata importingClassMetadata) { List<String> basePackages = getBasePackages(importingClassMetadata);for (String basePackage : basePackages) {
            Reflections reflections = newReflections(basePackage); Set<Class<? >> spiClasses = reflections.getTypesAnnotatedWith(Spi.class);if(! CollectionUtils.isEmpty(spiClasses)){for(Class<? > spiClass : spiClasses) {if(spiClass.isInterface()){
                        returnspiClass; }}}}return null;
    }

    private List<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> enableSpi = importingClassMetadata.getAnnotationAttributes(EnableSpi.class.getName());
        String[] spiBackagepackages = (String[]) enableSpi.get("basePackages");
        List<String> basePackages =  Arrays.asList(spiBackagepackages);
        if(CollectionUtils.isEmpty(basePackages)){
            basePackages = new ArrayList<>();
            basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
        }
        return basePackages;
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (DefaultListableBeanFactory)beanFactory; }}Copy the code

How are business items used

The sample

1. Define spi service interface

@Spi
public interface HelloService {

    String sayHello(String username);
}
Copy the code

Note: @SPI specifies which Spi service interfaces to inject into the Spring container, and @SPI also has a defalutSpiImplClassName attribute that specifies the DEFAULT Spi implementation class to inject

2. Define concrete implementation classes

public class HelloServiceCnImpl implements HelloService {

    @Override
    @InterceptorMethod(interceptorClasses = {HelloServiceCnInterceptor.class, HelloServiceCnOtherInterceptor.class})
    public String sayHello(String username) {
        return "Hello."+ username; }}Copy the code
public class HelloServiceEnImpl implements HelloService {


    @Override
    @InterceptorMethod(interceptorClasses = HelloServiceEnInterceptor.class)
    public String sayHello(String username) {
        return "hello:"+ username; }}Copy the code

Note: The @interceptorMethod annotation is used for method enhancement and is not relevant to this article

SRC /main/resources/ create/meta-inf /services directory and add a new file named after the interface

com.github.lybgeek.spi.HelloService
Copy the code

4. Enter the following information in the interface name file

com.github.lybgeek.spi.en.HelloServiceEnImpl
Copy the code
com.github.lybgeek.spi.cn.HelloServiceCnImpl
Copy the code

5. Write the service controller

@RestController
@RequestMapping("/test")
public class SpiTestController {


    @SpiAutowired("helloServiceCnImpl")
    private HelloService helloService;


    @GetMapping(value="/{username}")
    public String sayHello(@PathVariable("username") String username){
        returnhelloService.sayHello(username); }}Copy the code

Note: @spiautowired is a custom annotation that can be thought of as @autowired + @Qualifier

@enablespi (basePackages = “com.github. Lybgeek.spi “)

Note: basePackages is used to specify the packages to scan spi

7, test,

  • When @SpiAutowired(“helloServiceCnImpl”), the page renders as

  • When @SpiAutowired(“helloServiceEnImpl”), the page renders as

  • When specifying the @autowired @ Spi (” com. Making. Lybgeek. Spi. Cn. HelloServiceCnImpl “)

The page is rendered as

Note:I don’t use @spiautowired here because @Spiautowired needs to specify a name

conclusion

This article relies on Spring for on-demand loading based on SPI, and to some extent coupling with Spring. If there is an opportunity, how to implement custom key-value pairs of SPI

The demo link

Github.com/lyb-geek/sp…