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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。