Springboot的SPI机制

418 阅读4分钟

这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

介绍

SpringbootSpring进行了大量的约定大于配置的优化,使得用户在使用Springboot应用是可以在不进行那么多配置的情况下启动服务,其中Springboot添加了一种解耦的扩展机制Spring Factories。这是Springboot提供的一种SPI方式,对于模块化应用的解耦有非常大的益处,比如将应用的底层业务模块分为非关系型相关模块、调度相关模块、关系型数据库相关模块、用户权限相关模块等,然后业务主模块根据不同场景的需要引入对应的xxx-starter模块进行使用,解耦度非常高。并且通过这种模式的模块化,对于一些新项目非常友好,它只需要引入需要的模块,添加一些配置项,就可以集成一个项目中的很多通用功能。

分析

这种方式是经过了Springboot的自动装配EnableAutoConfiguration注解中的@Import去解析了配置类AutoConfigurationImportSelectorselectImports方法,在该方法中读取自动装配的配置类列表后逐个进入装配解析类中执行相应的业务逻辑。

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

然后这个Selector类的selectImports方法会去读取出每个模块中META-INF的目录下的spring.factories中的每个auto配置类。

List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
			getBeanClassLoader());

核心逻辑:FACTORIES_RESOURCE_LOCATION = META-INF/spring.factories从这里获取到了所有的xx.factories文件中的自动启动列表。

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    result = new LinkedMultiValueMap<>();
}

实践

  1. 创建一个服务或者模块,只需要引用一些本自动装配模块需要使用到的依赖即可,比如我这边只引用了lombok的相关依赖。

  2. resources文件夹下创建META-INF/spring.factories,并将对应的auto配置类引入,我这边由于一些特殊情况需要对这个配置类配置两个自动处理,一个是为了自动装配的配置类,一个是为了将容器上下文对象注入到自动装配类中。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.com.xiaocainiaoya.common.CustomizeConfiguration
org.springframework.context.ApplicationContextInitializer=cn.com.xiaocainiaoya.common.CustomizeConfiguration
  1. 创建一个auto配置类,我这边的场景是由于项目原来依赖于tk.mybatis先新项目想引入mybatis-plus,所以想在项目中进行兼容,在不修改的情况下,将资源配置项中的mybatis.mapper-locationsmybatis-plus.mapper-locations赋为一样的值。所以这个操作需要在应用启动后的相对前置位置进行操作。这个地方由于我使用了BeanDefinitionRegistryPostProcessor在进入这个方法的时候由于还没有到达属性注入的解析,本来可以通过@Autowired的方式注入applicationContext然后再获取对应的environment进行处理,所以这里只能通过实现上下文包装器ApplicationContextInitializer来获取对应的上下文信息。一般情况下,由于auto类散落在每个不同的模块中,为了保证加载的顺序或者前后之间的关联或者是否进行本配置类的加载,需要配合一些条件注解使用,比如@AutoConfigureBefore本配置类执行前需要执行某个配置类、@AutoConfigureAfter本配置类执行后需要执行某个配置类、ConditionalOnProperty存在某个配置时加载本配置类、ConditionalOnBean容器存在某个bean对象时加载本配置类等条件注解结合进行处理。
@Configuration
@ComponentScan(basePackages = {"cn.com.xiaocainiaoya.common.*"})
public class CustomizeConfiguration implements BeanDefinitionRegistryPostProcessor, ApplicationContextInitializer {

  private static ConfigurableEnvironment environment;

  public void modifyProperty() {

    if (CollUtil.isNotEmpty(mapperLocations)) {
      // 动态刷新配置
      OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource(CUSTOM_PROPERTY, mapProp);
      propertySources.addFirst(source);
    }
  }

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    environment = context.getEnvironment();
  }
}
  1. 主服务引用模块。在主服务的pom.xml文件中添加这个starter

  2. 启动服务,主服务打印xx-starter服务中相关信息,说明自动装配设置成功。

c.c.x.common.CustomizeConfiguration      : classpath*:mapper/*.xml

总结

实际上springboot这种factoriesSPI机制提供了比较灵活的可拔插处理,但是在使用上需要掌握spring的一些原理,就比如我这个例子,我本来是使用@Autowired方式注入应用上下文信息,但是发现怎么都注入不进去。