7.Spring 扩展点

1. ApplicationContextInitializer

前言
一提到Spring、Springoboot,很多人马上就会想到依赖注入、控制反转、自动装配、约定大于配置、使开发变得简单等等。但是如果仅仅会使用Springboot、SpringMVC完成一些增删改查,解决一些bug,那么实际上你并没有真的懂Spring、Springboot。Spring的核心是容器,Springboot更是封装了Spring,把复杂隐藏在内部,让其在使用上更简单,同时又预留了很多的扩展。所以我认为学会Springboot的简单使用只是一个开始,对业务开发更有参考和学习意义的是Springboot如何把复杂变得简单、预留的扩展接口又是如何使用的。

1.1 ApplicationContextInitializer的功能作用

在Spring容器初始化开始的时候,ApplicationContextInitializer接口的所有实现在类会被实例化;

在Spring容器刷新前,所有实现类的org.springframework.context.ApplicationContextInitializer#initialize方法会被调用,initialize方法的形参类型是ConfigurableApplicationContext,因此可以认为ApplicationContextInitializer实际上是Spring容器初始化前ConfigurableApplicationContext的回调接口,可以对上下文环境作一些操作,如运行环境属性注册、激活配置文件等

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
 
   /**
    * Initialize the given application context.
    * @param applicationContext the application to configure
    */
   void initialize(C applicationContext);
 
}

1.2. 三种实现方式

下面通过一个具体实现类实现ApplicationContextInitializer接口,来分享一下其具体的实现方式。Springboot的扩展点ApplicationContextInitializer接口的实现主要分为两步:

  • 1、实现ApplicationContextInitializer接口;

MyApplicationContextInitializer实现ApplicationContextInitializer接口,并打印一下系统相关属性的key、value

public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        Map<String, Object> systemProperties = applicationContext.getEnvironment().getSystemProperties();
        System.out.println("---------------start------------------");
        systemProperties.forEach((key,value)->{
            System.out.println("key:"+key+",value:"+value);
        }); System.out.println("---------------end------------------");
    }
}
  • 2、把实现类注册到Spring容器中

把实现类MyApplicationContextInitializer注册到Spring容器中,主要有三种方式:

1.2.1 spring.factories

在resources目录新建/META-INFI/spring.factories文件,并预置以下内容,即可完成自定义MyApplicationContextInitializer的注册;

org.springframework.context.ApplicationContextInitializer=com.fanfu.config.MyApplicationContextInitializer

1.2.2 application.properties

在application.properties文件中预置以下配置内容,即可完成自定义MyApplicationContextInitializer的注册;

context.initializer.classes=com.fanfu.config.MyApplicationContextInitializer

1.2.3 springApplication.addInitializers()

在springboot的启动类中,使用springApplication.addInitializers(new MyApplicationContextInitializer()),完成自定义MyApplicationContextInitializer的注册;

@SpringBootApplication
public class FanfuApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(FanfuApplication.class);
        springApplication.addInitializers(new MyApplicationContextInitializer());
        springApplication.run(args);
    }
}

执行结果如下:(user.dir=项目的根目录)

1.3. 初始化时机

ApplicationContextInitializer接口的实现类的初始化,是在SpringApplication类的构造函数中,即spring容器初始化开始前,先通过 getSpringFactoriesInstances(...)得到所有实现类的集合,然后通过 setInitializers(...)注入到SpringApplication类的initializersn属性中;

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;
   Assert.notNull(primarySources, "PrimarySources must not be null");
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
   setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   this.mainApplicationClass = deduceMainApplicationClass();
}

1.4. 执行时机

ApplicationContextInitializer接口的实现类的执行时机是在org.springframework.boot.SpringApplication#prepareContext-->org.springframework.boot.SpringApplication#applyInitializers中,即spring容器正式刷新前,准备上下文环境时;

getInitializers()得到,SpringApplication类的构造函数中通过 setInitializers(...)注入到SpringApplication类的initializersn属性中的所有ApplicationContextInitializer接口的实现类;
遍历这些实现类,并调用initialize()方法;

protected void applyInitializers(ConfigurableApplicationContext context) {
   for (ApplicationContextInitializer initializer : getInitializers()) {
      Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
            ApplicationContextInitializer.class);
      Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
      initializer.initialize(context);
   }
}

1.5. 内置的实现类

Springboot内部也有一些内置的实现类,用于辅助Spring相关功能的实现,常见的实现类如下:

1.5.1 DelegatingApplicationContextInitializer

ApplicationContextInitializer的第二种实现方式(application.properties),在application.properties文件中配置context.initializer.classes=com.fanfu.config.MyApplicationContextInitializer,DelegatingApplicationContextInitializer的作用就是找到application.properties文件中配置的实现类实例化,并执行initialize()方法

1.5.2 ContextIdApplicationContextInitializer

ContextIdApplicationContextInitializer用于设置 Spring 应用上下文 ID,如果在application.properties中未设置spring.application.name,则默认为“application”

1.5.3 ConfigurationWarningsApplicationContextInitializer

ConfigurationWarningsApplicationContextInitializer用于报告 Spring 容器的一些常见的错误配置,可以看出,该初始化器为 context 增加了一个 Bean 的后置处理器。这个处理器是在注册 BeanDefinition 实例之后生效的,用于处理注册实例过程中产生的告警信息,其实就是通过日志打印出告警信息。

1.5.4 ServerPortInfoApplicationContextInitializer

ServerPortInfoApplicationContextInitializer除了实现了ApplicationContextInitializer接口外,还实现了ApplicationListener接口,ServerPortInfoApplicationContextInitializer作用就是把自己作为一个监听器注册到Spring的上下文环境中;

1.5.5 SharedMetadataReaderFactoryContextInitializer

实例化了一个org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer.CachingMetadataReaderFactoryPostProcessor对象并注册到了Spring的上下文环境中,CachingMetadataReaderFactoryPostProcessor是SharedMetadataReaderFactoryContextInitializer的一个内部类,这里就不具体展开了,有兴趣的小伙伴可以继续跟一下org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer.CachingMetadataReaderFactoryPostProcessor#postProcessBeanDefinitionRegistry方法了解一下具体的内容;

1.5.6 ConditionEvaluationReportLoggingListener

ConditionEvaluationReportLoggingListener从名字看是一个监听器,实际上不是,其内部类org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener.ConditionEvaluationReportListener才是实际的监听器,而ConditionEvaluationReportLoggingListener的作用就是把内部类ConditionEvaluationReportListener注册到Spring上下文环境中;

1.6. 总结

通过这篇文章,可以了解到:

第一,在Spring容器被刷新前,可以通过实现ApplicationContextInitializer接口对Spring上下文环境作一些配置或操作;

第二,ApplicationContextInitializer接口的实现方式有三种,可以根据项目需要选择合适的;

第三,了解了ApplicationContextInitializer接口实现类的初始化时机和执行时机,以及Springboot内置的具体实现类,可以学习到spring本身是如何利用扩展接口实现一些功能的,对实际的项目开发具有一定的参考意义。

2.BeanDefinitionRegistryPostProcessor

前言:通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级的后置处理器会在Spring容器初始化后、刷新前这个时间执行一次,Bean级的重置处理器,则是在每一个Bean实例化前后都会执行。

2.1. 功能特性

postProcessBeanDefinitionRegistry()方法可以通过BeanDefinitionRegistry对BeanDefintion进行增删改查;

继承了BeanFactoryPostProcessor,BeanFactoryPostProcessor是容器级别的扩展接口,org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory方法在容器实例化后、刷新容器前被执行,即在容器刷新前还可以对BeanDefintion再作一些操作;

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
   void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
@FunctionalInterface
public interface BeanFactoryPostProcessor {
   void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

总结起来就是,在所有的BeanDefinition加载完成之后,Bean真正被实例化之前,可以通过实现BeanDefinitionRegistryPostProcessor接口,对BeanDefinition再做一些定制化的操作,比如修改某个bean的BeanDefinition的属性、手动注册一些复杂的Bean。

对于Spring原理不太熟悉的小伙伴心里看到这可能有点晕了,BeanDefinition是什么?BeanDefinitionRegistry又是什么?ConfigurableListableBeanFactory又又是什么?别着急,这里拐个弯简单的解释一下,对整篇文章的理解会更顺畅。

2.1.1 BeanDefinition

大家都知道,Spring的核心之一是IOC(控制反转),Spring之所以可以实现bean控制权的反转,是因为Spring的容器功能,在bean纳入Spring容器管理前,所有bean会被抽象封装成一个BeanDefinition实例,然后会在不同的时机根据BeanDefinition实例信息对bean进行实例化。

简单说,Dog.java描述狗这一类动物的属性和行为,BeanDefinition描述Dog.java这个类。

2.1.2 BeanDefinitionRegistry

BeanDefinitionRegistry从字面意思看是bean的定义信息的注册登记,其实这个类的功能和字面意思一样,就是对BeanDefinition进行管理(增删改查);

public interface BeanDefinitionRegistry extends AliasRegistry {
    //注册beanDefinition
   void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
         throws BeanDefinitionStoreException;
    //移除指定的beanDefinition
   void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
    //根据beanName查询beanDefinition
   BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
    //判断某个beanDefinition是否已经注册
   boolean containsBeanDefinition(String beanName);
    //获取所有已注册的beanDefinition
   String[] getBeanDefinitionNames();
    //获取所有已注册的beanDefinition的数量
   int getBeanDefinitionCount();
    //判断某个beanDefinition是否已经被使用
   boolean isBeanNameInUse(String beanName);
}

2.1.3 ConfigurableListableBeanFactory

上面提到了Spring的容器,Spring的核心之一是IOC,那么Spring的容器设计就是核心中的核心了。Spring的容器有多种形态,最基础的形态就是BeanFactory,ConfigurableListableBeanFactory间接继承了BeanFactory,因此ConfigurableListableBeanFactory实现类除了有Spring基础版本容器的功能外,还有一些高级的功能,Springboot默认的实际实现是DefaultListableBeanFactory,有兴趣的小伙伴可以以此为入口深入探究一番,这里不展开细说了。

2.2 自定义实现

2.2.1 MyBeanDefinitionRegistryPostProcessor

下面通过一个具体类MyBeanDefinitionRegistryPostProcessor实现BeanDefinitionRegistryPostProcessor接口,来探究BeanDefinitionRegistryPostProcessor实现类的初始化和执行过程。

在postProcessBeanDefinitionRegistry()方法被调用的时候手工在Spring中注册了Dog类的BeanDefinition信息;

在postProcessBeanFactory()方法被调用的时候,从Spring容器中取出Dog类的BeanDefinition信息和Dog类的实例;

@Data
public class Dog {
    private String name;
    private String color;
}
@Component
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        //手工定义一个beanDefinition实例
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        //给beanDefinition填充属性
        beanDefinition.setBeanClass(Dog.class);
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        PropertyValue propertyValue1 = new PropertyValue("name", "旺财");
        PropertyValue propertyValue2 = new PropertyValue("color", "黑色");
        propertyValues.addPropertyValue(propertyValue1);
        propertyValues.addPropertyValue(propertyValue2);
        beanDefinition.setPropertyValues(propertyValues);
        //注册手工定义的beanDefinition
        registry.registerBeanDefinition("dog", beanDefinition);
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("-----------start------------");
        //根据类名取出手工注册的beanDefinition
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("dog");
        System.out.println(beanDefinition.getBeanClassName());
        //根据类从容器中取出手工注册的beanDefinition所描述的实例bean
        Dog dog = beanFactory.getBean(Dog.class);
        System.out.println(dog.getName());
        System.out.println(dog.getColor());
        System.out.println("-----------end------------");
    }
}

单元测试

@Test
public void test(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.fanfu");
    Dog dog = ((Dog) context.getBean("dog"));
    System.out.println(dog.getName());
    System.out.println(dog.getColor());
}

2.2.2 UML类图

通过BeanDefinitionRegistryPostProcessorUML类图可以看出BeanDefinitionRegistryPostProcessor继承了BeanFactoryPostProcessor,postProcessBeanDefinitionRegistry()方法属于BeanDefinitionRegistryPostProcessor,postProcessBeanFactory()属于BeanFactoryPostProcessor,所有实现了BeanDefinitionRegistryPostProcessor接口的实现类都需要实现这两个方法,而作为Springboot的扩展点之一,其扩展的逻辑也在这两个方法中。

2.3. 初始化和执行时机

通过自定义的MyBeanDefinitionRegistryPostProcessor类,实现BeanDefinitionRegistryPostProcessor接口,从项目启动开始,其执行过程如下:

  • 1.执行项目的主类,org.springframework.boot.SpringApplication#run被调用;

  • 2.进入boot.SpringApplication#run方法后,刚开始是一些Spring容器初始化的配置操作,直到执行到org.springframework.boot.SpringApplication#refreshContext,开始容器刷新,进入了关键阶段;

  • 3.在SpringApplication#refreshContext,实际的刷新逻辑是在org.springframework.context.support.AbstractApplicationContext#refresh方法中;

  • 4.AbstractApplicationContext#refresh方法中,调用org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors开始初始化和执行实现BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry()和postProcessBeanFactory();

  • 5.进入AbstractApplicationContext#invokeBeanFactoryPostProcessors方法,发现又调用了org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors();

  • 6.在PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()方法中,并不是直接就初始化和执行postProcessBeanDefinitionRegistry()和postProcessBeanFactory(),而是又进行了一系列的判断,其判断顺序是:1、通过AbstractApplicationContext#addBeanFactoryPostProcessor提前注册的BeanDefinitionRegistryPostProcessor实现类;2、实现了PriorityOrdered接口;3、是否实现了Ordered;4、剩下的其他BeanDefinitionRegistryPostProcessor实现类;自定义的MyBeanDefinitionRegistryPostProcessor就属于第4类,所以是所有实现里较晚才被执行的,如果想要提前被执行,可以考虑前面三种方式;

  • 7.在PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()方法中执行完MyBeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry方法后,紧接着就开始执行MyBeanDefinitionRegistryPostProcessor#postProcessBeanFactory方法了;从整个调用过程看postProcessBeanDefinitionRegistry()是早于postProcessBeanFactory()方法执行;

下面是我根据整个调用过程画的一个时序图,过程确实比较复杂,但是逻辑比较清晰,因此并不难理解,想要真的搞清楚整个过程,最好的方法就是照着这个图,亲自执行一遍,通过debug观察每一个关键节点的执行过程。

2.4. 内部实现类

spring-boot-starter-web中内置的实现类有CachingMetadataReaderFactoryPostProcessor、ConfigurationClassPostProcessor、ConfigurationWarningsPostProcessor、EmbeddedDataSourceBeanFactoryPostProcessor、ImportsCleanupPostProcessor、TestRestTemplateRegistrar、WebTestClientRegistrar、WsdlDefinitionBeanFactoryPostProcessor,观察一下每个实现类会发现:都比较类似,这些内置实现类都是Springboot中的内部类,通过这些BeanDefinitionRegistryPostProcessor内部实现类向Spring容器中注册了一些特殊的BeanDefinition,如果展开详细再说一说这些Bean,怕是一天一夜也说不完,有兴趣的小伙伴可以深入了解一下,这里就不再展开了。

3.BeanFactoryPostProcessor

前言
通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级的后置处理器会在Spring容器初始化后、刷新前这个时间执行一次,Bean级的重置处理器,则是在每一个Bean实例化前后都会执行。

CommandLineRunner接口介绍
  CommandLineRunner接口是在容器启动成功后的最后一步回调(类似开机自启动)。

1. CommandLineRunner接口使用

写一个类,实现CommandLineRunner接口,将该类注入到Spring容器中;

可以通过@Order注解(属性指定数字越小表示优先级越高)或者Ordered接口来控制执行顺序。

例如,我们自定义两个类,实现CommandLineRunner接口,实现run方法,在run方法中添加处理逻辑。

@Order(5)
@Component
public class AppStartReport implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("AppStartReport : 项目成功启动------------------");
    }
}
@Order(2)
@Component
public class AppStartReport2 implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("AppStartReport2 : 项目成功启动------------------");
    }
}

项目启动效果如下,AppStartReport2 较AppStartReport 先执行:

2. ApplicationRunner与CommandLineRunner

ApplicationRunner与CommandLineRunner功能类似,区别在于方法参数不同:

  • CommandLineRunner的方法参数是原始的参数,未做任何处理;
  • ApplicationRunner的参数为ApplicationArguments对象,是对原始参数的进一步封装。ApplicationArguments是对参数(主要针对main方法)做了进一步的处理,可以解析--name=value的参数,我们就可根据name获取value。
@Component
public class AppStartReport3 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("AppStartReport3 : 项目成功启动,参数为: " + Arrays.asList(args.getSourceArgs()));
    }
}

由于我们使用的是main方法的args参数,而这个参数未指定过,因此这里获取值为空;

我们接下来进行参数值的手动指定,再查看效果:

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(App.class);
        ConfigurableApplicationContext context = application.run("aaa", "bbb");
        context.close();
    }
}

以上是自行指定的参数值,未使用main方法的系统参数值,效果如下:

那我们如何指定系统参数值呢?可以在Program arguments选项框设置初始值,“–key=value“形式,如“–argname=ccc”,如下图所示:

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(App.class);
        ConfigurableApplicationContext context = application.run(args);
        context.close();
    }
}

启动器传入参数为main方法的参数args,效果如下:

我们发现获取到的是我们设置的整个参数信息,那如何根据key获取具体的value呢,接下来就需要看一下ApplicationArguments的源码了。

3. ApplicationArguments 源码分析

public interface ApplicationArguments {

   String[] getSourceArgs();

   Set<String> getOptionNames();

   boolean containsOption(String name);

   List<String> getOptionValues(String name);

   List<String> getNonOptionArgs();

}

我们可以看到5个方法。

从前面的例子中我们可以看出,资源参数分为两种,一种是在调用run方法时传入的(”aaa”,”bbb”),一种是配置的系统参数即main方法的args参数(”ccc”)。

首先getSourceArgs方法,可以获取到所有参数,可以是自己传入的参数,也可以是配置的系统参数;

getNonOptionArgs方法即可获得我们传入的参数(”aaa”,”bbb”)。

其余3个方法,可获得系统参数。实际用法如下:

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(App.class);
        ConfigurableApplicationContext context = application.run(args);
        ApplicationArguments arguments = context.getBean(ApplicationArguments.class);
        System.out.println("参数个数:" + arguments.getSourceArgs().length);
        System.out.println("Non OptionArgs:" + arguments.getNonOptionArgs());
        System.out.println("Option Names:" + arguments.getOptionNames());
        System.out.println("获取key为argname的value值:" + arguments.getOptionValues("argname"));
        System.out.println("是否包含key为argname的参数:" + arguments.containsOption("argname"));
        context.close();
    }
}

输出效果如图:

https://blog.csdn.net/fox9916/article/details/128538625

posted @ 2023-08-11 18:02  让你上瘾的三哥  阅读(150)  评论(0)    收藏  举报