1

Pre-knowledge

What is SPI

I have written an article before --> java spi mechanism introduction friends who do not know spi, you can check this article to understand, and then read the following

Preface

Assuming that you already have a certain understanding of SPI, friends who have used the SPI provided by the JDK should find that the SPI of the JDK cannot be loaded on demand. How to solve this shortcoming problem?

Here are two ways of thinking, one is to implement a set of SPI by yourself, and the other is a very common way to implement components, that is, when the current component cannot be satisfied, you can use other components or add a proxy layer. The idea of this article is to use spring's IOC, spring's ioc is essentially a key-value pair map, injecting the objects generated by jdk spi into the spring ioc container, and indirectly also has the key-->value mapping function

Realization ideas

  • When the project starts, use spi to load classes and generate objects
  • Inject the generated object into the spring container
  • In business projects, use @Autowired +
    @Qualifier annotation, reference the bean object generated by SPI on demand

Core code snippet

1. Implementation of spi loading
 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);
        }
    }
2. Inject into 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);
        }
    }

    private Class<?> getSpiInterface(AnnotationMetadata importingClassMetadata) {
        List<String> basePackages = getBasePackages(importingClassMetadata);
        for (String basePackage : basePackages) {
            Reflections reflections = new Reflections(basePackage);
            Set<Class<?>> spiClasses = reflections.getTypesAnnotatedWith(Spi.class);
            if(!CollectionUtils.isEmpty(spiClasses)){
                for (Class<?> spiClass : spiClasses) {
                    if(spiClass.isInterface()){
                        return spiClass;
                    }
                }
            }
        }

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

How to use business items

Example

1. Define the spi service interface
@Spi
public interface HelloService {

    String sayHello(String username);
}

Note: @Spi is used to specify which spi service interfaces need to be injected into the spring container, and @Spi also has a defalutSpiImplClassName attribute to specify the default injection spi implementation class

2. Define the specific implementation class
public class HelloServiceCnImpl implements HelloService {

    @Override
    @InterceptorMethod(interceptorClasses = {HelloServiceCnInterceptor.class, HelloServiceCnOtherInterceptor.class})
    public String sayHello(String username) {
        return "你好:" + username;
    }
}
public class HelloServiceEnImpl implements HelloService {


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

Note: @InterceptorMethod This annotation is used for method enhancement, which has little to do with this article and can be ignored

3. Create a /META-INF/services directory under src/main/resources/ and add a file named after the interface
com.github.lybgeek.spi.HelloService
4. Fill in the file named interface with the following content
com.github.lybgeek.spi.en.HelloServiceEnImpl
com.github.lybgeek.spi.cn.HelloServiceCnImpl
5. Write business controller
@RestController
@RequestMapping("/test")
public class SpiTestController {


    @SpiAutowired("helloServiceCnImpl")
    private HelloService helloService;


    @GetMapping(value="/{username}")
    public String sayHello(@PathVariable("username") String username){
        return helloService.sayHello(username);
    }
}

Note: @SpiAutowired is a custom annotation, which can be regarded as @Autowired + @Qualifier

6. Add @EnableSpi(basePackages = "com.github.lybgeek.spi") to the startup class

Note: basePackages is used to specify the package for scanning spi

7. Test
  • When @SpiAutowired("helloServiceCnImpl"), the page is rendered as

在这里插入图片描述

  • When @SpiAutowired("helloServiceEnImpl"), the page is rendered as

在这里插入图片描述

  • When @Autowired @Spi("com.github.lybgeek.spi.cn.HelloServiceCnImpl") is specified

At this point the page is rendered as

在这里插入图片描述
Note: does not use @SpiAutowired here, because @SpiAutowired needs to specify the name

Summarize

This article is based on the spi on-demand loading is dependent on spring, to some extent coupled with spring, if there is a chance, let me talk about how to implement a custom key-value pair SPI

demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-ioc


linyb极客之路
344 声望193 粉丝