首页 > 其他分享 >SpringBoot3.x中spring.factories SPI 服务发现机制的改变

SpringBoot3.x中spring.factories SPI 服务发现机制的改变

时间:2023-05-11 22:25:12浏览次数:32  
标签:spring SpringApplication SPI SpringBoot3 context new factories ex

目录

一、基础背景

以Spring Boot 2.x与Spring Boot 3.x为背景做变化描述,顺带勾勒启动与注册流程;

二、服务发现接口

1.@SpringBootApplication启用@EnableAutoConfiguration

2.@EnableAutoConfiguration引入并初始化@Import(AutoConfigurationImportSelector.class)

AutoConfigurationImportSelector类就此被加载并初始化,它的核心加载方法在哪被调用呢?

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList<>(
          SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
    ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations,
          "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

spring.factories

spring.factories文件被SpringFactoriesLoader加载

spring.factories其实是SpringBoot提供的SPI机制,底层实现是基于SpringFactoriesLoader检索ClassLoader中所有jar(包括ClassPath下的所有模块)引入的META-INF/spring.factories文件。

基于文件中的接口(或者注解)加载对应的实现类并且注册到IOC容器。

这种方式对于@ComponentScan不能扫描到的并且想自动注册到IOC容器的使用场景十分合适,基本上绝大多数第三方组件甚至部分spring-projects中编写的组件都是使用这种方案。

三、服务发现机制调用

1.启动SpringApplication

作为SpringBoot启动入口类,位于Spring-boot-project->spring-boot。

常见启动类编写如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Application {
 
    public static void main(String[] args) {
       SpringApplication.run(Application.class, args);
    }
}

2.加载SpringApplication.run

SpringApplication的静态方法run被调用,开始启动Spring Boot应用程序。

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime();
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
       ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
       ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
       Banner printedBanner = printBanner(environment);
       context = createApplicationContext();
        //1展开说明
       context.setApplicationStartup(this.applicationStartup);
        //2展开说明
       prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        //3展开说明
       refreshContext(context);
       afterRefresh(context, applicationArguments);
       Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
       if (this.logStartupInfo) {
          new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
       }
       listeners.started(context, timeTakenToStartup);
       callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
       if (ex instanceof AbandonedRunException) {
          throw ex;
       }
       handleRunFailure(context, ex, listeners);
       throw new IllegalStateException(ex);
    }
    try {
       if (context.isRunning()) {
          Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
          listeners.ready(context, timeTakenToReady);
       }
    }
    catch (Throwable ex) {
       if (ex instanceof AbandonedRunException) {
          throw ex;
       }
       handleRunFailure(context, ex, null);
       throw new IllegalStateException(ex);
    }
    return context;
}

1.SpringApplication.createApplicationContext

创建Context上下文,加载SPI配置

SpringApplication中的createApplicationContext方法被调用,创建一个ApplicationContext实例。

通常未做拓展或者配置的情况下为ApplicationContextFactory接口中的

ApplicationContextFactory DEFAULT = new DefaultApplicationContextFactory();
 
protected ConfigurableApplicationContext createApplicationContext() {
    //WebApplicationType是一个枚举,在SpringApplication构造方法中
    //通过WebApplicationType.deduceFromClasspath确定应用是servlet亦或reactive
    return this.applicationContextFactory.create(this.webApplicationType);
}
 
class DefaultApplicationContextFactory implements ApplicationContextFactory {
...
@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
    try {
       return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
             this::createDefaultApplicationContext);
    }
    catch (Exception ex) {
       throw new IllegalStateException("Unable create a default ApplicationContext instance, "
             + "you may need a custom ApplicationContextFactory", ex);
    }
}
 
private <T> T getFromSpringFactories(WebApplicationType webApplicationType,
       BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {
    //SpringFactoriesLoader在spring-context中,用于加载spring.factories指定工厂类在classpath中所有可用的实现类的实例列表。
    for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
          getClass().getClassLoader())) {
       T result = action.apply(candidate, webApplicationType);
       if (result != null) {
          return result;
       }
    }
    return (defaultResult != null) ? defaultResult.get() : null;
}
...
}

2.SpringApplication.prepareContext

为应用程序上下文准备必要的配置信息,并将自动配置的组件注册到上下文中,以完成应用程序的初始化工作。

在prepareContext中load方法继续执行,加载所有的ApplicationListener实例,并注册到ApplicationContext中。

说到这里肯定会有人问:什么是上下文?

GenericWebApplicationContext类实现:ConfigurableWebApplicationContext接口
ServletWebServerApplicationContext类继承:GenericWebApplicationContext类
具体实现类有:
ServletWebServerApplicationContext、ReactiveWebServerApplicationContext、XmlServletWebServerApplicationContext等。

ServletWebServerApplicationContext封装了WebServer、ServletConfig,对外暴露统一的配置工厂注册接口,屏蔽从Servlet获取资源信息的复杂性
适配对接不同的WebServer对象比如netty、jetty、tomcat、unbertow

3.SpringApplication.refreshContext

刷新应用程序上下文,以完成 Bean 的加载、依赖解析、实例化等一系列初始化操作,并执行一些后置处理操作,如注册 ShutdownHook 钩子、输出 Banner 等。

SpringApplication中的run方法继续执行,调用refreshContext方法,启动ApplicationContext上下文并刷新应用程序。

在refreshContext方法中,会调用load方法,加载所有的自动配置类。

4.AutoConfigurationImportSelector在什么时候被调用呢?

通过AbstractApplicationContext#refresh()

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors

(ConfigurationClassPostProcessor)registryProcessor.postProcessBeanDefinitionRegistry(registry)

ConfigurationClassPostProcessor.processConfigBeanDefinitions();

ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);

parser.parse(candidates);

ConfigurationClassParser. processConfigurationClass()

ConfigurationClassParser.doProcessConfigurationClass

ConfigurationClassParser.processImports

ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,this.environment, this.resourceLoader, this.registry);
selector.selectImports(currentSourceClass.getMetadata());

spring-boot-autoconfigure-> AutoConfigurationImportSelector.fireAutoConfigurationImportEvents()

AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods(listener);
 listener.onAutoConfigurationImportEvent(event);
}

ConditionEvaluationReportAutoConfigurationImportListener.onAutoConfigurationImportEvent

public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
if (this.beanFactory != null) {
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.beanFactory);
 report.recordEvaluationCandidates(event.getCandidateConfigurations());
 report.recordExclusions(event.getExclusions());
 }
}

AutoConfigurationImportSelector.getCandidateConfigurations
 
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
          getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
          + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

三、服务发现实现核心

spring-core包中

public class SpringFactoriesLoader {
private final Map<String, List<String>> factories;
//构造方法被保护,被公开的静态方法forResourceLocation调用
protected SpringFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
    this.classLoader = classLoader;
    this.factories = factories;
}
...
//初始化factories
public static SpringFactoriesLoader forResourceLocation(String resourceLocation, @Nullable ClassLoader classLoader) {
    Assert.hasText(resourceLocation, "'resourceLocation' must not be empty");
    ClassLoader resourceClassLoader = (classLoader != null ? classLoader :
          SpringFactoriesLoader.class.getClassLoader());
    Map<String, SpringFactoriesLoader> loaders = cache.computeIfAbsent(
          resourceClassLoader, key -> new ConcurrentReferenceHashMap<>());
    return loaders.computeIfAbsent(resourceLocation, key ->
          new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)));
}
 
protected static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
    Map<String, List<String>> result = new LinkedHashMap<>();
    try {
       Enumeration<URL> urls = classLoader.getResources(resourceLocation);
       while (urls.hasMoreElements()) {
          UrlResource resource = new UrlResource(urls.nextElement());
          Properties properties = PropertiesLoaderUtils.loadProperties(resource);
          properties.forEach((name, value) -> {
             List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
             Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
                   .map(String::trim).forEach(implementations::add);
          });
       }
       result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
    }
    catch (IOException ex) {
       throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
    }
    return Collections.unmodifiableMap(result);
}
 
private static List<String> toDistinctUnmodifiableList(String factoryType, List<String> implementations) {
    return implementations.stream().distinct().toList();
}
...
//服务构建构建层做事
private List<String> loadFactoryNames(Class<?> factoryType) {
    return this.factories.getOrDefault(factoryType.getName(), Collections.emptyList());
}
}

四、服务发现变化

AutoConfigurationImportSelector.getCandidateConfigurations
//原本通过spring-core->SpringFactoriesLoader 去加载META-INF/spring.factories
//现在改用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
       .getCandidates();
    Assert.notEmpty(configurations,
          "No auto configuration classes found in "
                + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

五、周边生态支持适配变化

Spring Boot 2.x升级到Spring Boot 3.0其实是一个"破坏性"升级,目前来看相对较大的影响是:

  • 必须使用JDK17
  • Jakarta EE的引入,导致很多旧的类包名称改变
  • 部分类被彻底移除
  • spring-data模块的所有配置属性必须使用spring.data前缀,例如spring.redis.host必须更变为spring.data.redis.host
  • spring.factories功能在Spring Boot 2.7已经废弃,在Spring Boot 3.0彻底移除

1.替代方案

替代方案比较简单,就是在类路径下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
文件的内容是:
每个实现类的全类名单独一行。例如:
对于使用了(低版本还没适配Spring Boot 3.0)mybatis-plus、dynamic-datasource组件的场景,可以在项目某个模块的resources目录下建立META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,输入以下内容:

com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

标签:spring,SpringApplication,SPI,SpringBoot3,context,new,factories,ex
From: https://www.cnblogs.com/snifferhu/p/17392410.html

相关文章

  • Spring项目的相关准备
    导入jar包只需要导入这个核心jar包其余包也就能够正常使用啦!简单练习再spring官网找到xml配置文件的框架,复制到我们自己的项目里面网址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction写入bean配置然后在test的java......
  • SpringCloud之Zookeeper作为注册中心
    SpringCloudZookeeper通过自动配置和绑定到Spring环境,为SpringBoot应用程序提供ApacheZookeepper集成。通过一些简单的注释,可以快速启用和配置应用程序中的常见模式,并使用Zookeeper构建大型分布式系统。提供的模式包括服务发现和分布式配置。 特征服务发现:可以向Zookeepe......
  • springcache + redis 配置支持缓存ttl失效
    packagetst;importcom.fasterxml.jackson.annotation.JsonAutoDetect;importcom.fasterxml.jackson.annotation.JsonTypeInfo;importcom.fasterxml.jackson.annotation.PropertyAccessor;importcom.fasterxml.jackson.databind.DeserializationFeature;importcom.......
  • SpringMVC18_文件上传6
    十四、文件上传-客户端表单实现三要素文件上传客户端表单需要满足:表单项type=“file”表单的提交方式是post表单的enctype属性是多部分表单形式,及enctype=“multipart/form-data”form.xml<%@pagecontentType="text/html;charset=UTF-8"language="java"%><html......
  • Spring配置yml配置映射为HashMap
    核心代码spring:cache:type:REDISredis:time-to-live:28800000custom-ttl:{"xxx":"1000000","yyy":"6000000"} @Data@Component@Configuration@ConfigurationProperties(prefix="spr......
  • 【Spring实战】第4章 面向切面的Spring
    POM依赖<dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>4.0.7.RELEASE</version></dependency><!--SpringAOP依赖AspectJ,不然会报ReflectionWorldExc......
  • Spring MVC官方文档学习笔记(一)之Web入门
    注:该章节主要为原创内容,为后续的SpringMVC内容做一个先行铺垫1.Servlet的构建使用(1)选择Maven->webapp来构建一个web应用(2)构建好后,打开pom.xml文件,一要注意打包方式为war包,二导入servlet依赖,如下<!--打war包--><packaging>war</packaging><!--导入servlet依赖......
  • SpringBoot 接口并发限制(Semaphore)
    可以使用JMeter辅助测试 https://blog.csdn.net/weixin_45014379/article/details/124190381@RestController@RequestMapping({"/Test"})publicclasstest{Loggerlogger=LoggerFactory.getLogger(this.getClass());//使用Semaphore并发限制3个超过阻......
  • ssm springboot
    IOC解析Config.class得到扫描路径遍历路径下所有的java类,存在Component注解就存在专用map中(BeanDefinitionMap)根据相应的规则生成BeanName为key,类作为value核心类BeanDefinition类型作用域懒加载初始化方法销毁方法BeanDefinitionReaderBeanFactoryApplicationCon......
  • 这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问
    问题现象vSwitchId、uShape、iPhone...这类字段名,有什么特点?很容易看出来吧,首字母小写,第二个字母大写。它们看起来确实是符合Java中对字段所推崇的“小驼峰命名法”,即第一个单词小写,后面的单词首字母大写。但是,如果你在项目中给POJO类的字段以这种形式进行命名的话,那么可能......