• notice
  • Congratulations on the launch of the Sought Tech site

How spi implemented based on jdk integrates with spring to achieve dependency injection

Pre-knowledge

What is SPI

I have written an article before -> [Introduction to java spi mechanism] Friends who don’t know spi can check this article first 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 annotations to reference bean objects 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

@Spipublic 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, which is used 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: The annotation @InterceptorMethod 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

Insert picture description here

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

Insert picture description here

  • 当指定@Autowired @Spi(“com.github.lybgeek.spi.cn.HelloServiceCnImpl”)

At this point the page is rendered as

Insert picture description here
Note: @SpiAutowired is not used 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


Tags

Technical otaku

Sought technology together

Related Topic

0 Comments

Leave a Reply

+