Springboot/Spring 扩展点(二): Springboot启动扩展点

1、自动装配

当项目启动的时候,会去从所有的spring.factories文件中读取@EnableAutoConfiguration键对应的值,拿到配置类,然后根据一些条件判断,决定哪些配置可以使用,哪些不能使用。自动装配是SPI机制的一种运用场景。

在SpringBoot中,@EnableAutoConfiguration是通过@SpringBootApplication来使用的。

自动装配以及SPI机制见 Spring Boot自动配置

  • 调用时机:项目启动时加载;

  • 使用场景:框架整合Springboot的时候,通过自动装配来实现项目启动,框架就自动启动的,比如Mybatis整合SpringBoot。
    image.png
  • 自定义实现
//第一步,写个配置类:
@Configuration
public class UserAutoConfiguration {

    @Bean
    public UserFactoryBean userFactoryBean() {
        return new UserFactoryBean();
    }

}


//第二步,往spring.factories文件配置一下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sanyou.spring.extension.springbootextension.UserAutoConfiguration

//到这就已经实现了自动装配的扩展。


//接下来进行测试:
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);

        User user = applicationContext.getBean(User.class);

        System.out.println("获取到的Bean为" + user);
    }

}

//运行结果:
调用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@3406472c
获取到的Bean为com.sanyou.spring.extension.User@3406472c
从运行结果可以看出,自动装配起了作用,并且虽然往容器中注入的Bean的class类型为UserFactoryBean,但是最终会调用UserFactoryBean的getObject的实现获取到User对象。

2、Import注解

@Import注解:导入的配置类的分类,在项目中可能不常见,但是下面这两个注解肯定常见。

@Import({SchedulingConfiguration.class})
public @interface EnableScheduling {
}
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
    //忽略
}

@EnableScheduling:开启定时任务;
@EnableAsync:开启异步执行;

通过这两个注解可以看出,他们都使用了@Import注解,所以真正起作用的是@Import注解。并且在很多情况下,@EnbaleXXX这种格式的注解,都是通过@Import注解起作用的,代表开启了某个功能。

@Import注解导入的配置类可以分为三种情况:

第一种:配置类实现了 ImportSelector 接口

public interface ImportSelector {

   String[] selectImports(AnnotationMetadata importingClassMetadata);

   @Nullable
   default Predicate<String> getExclusionFilter() {
      return null;
   }

}

当配置类实现了 ImportSelector 接口的时候,就会调用 selectImports 方法的实现,获取一批类的全限定名,并把这些类注册到Spring容器中。

UserImportSelector实现了ImportSelector,selectImports方法返回User的全限定名,并把User这个类注册容器中

public class UserImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("调用 UserImportSelector 的 selectImports 方法获取一批类限定名");
        return new String[]{"com.sanyou.spring.extension.User"};
    }

}

测试:
// @Import 注解导入 UserImportSelector

@Import(UserImportSelector.class)
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将 Application 注册到容器中
        applicationContext.register(Application.class);
        applicationContext.refresh();

        System.out.println("获取到的Bean为" + applicationContext.getBean(User.class));
    }

}

结果:
调用 UserImportSelector 的 selectImports 方法获取一批类限定名
获取到的Bean为com.sanyou.spring.extension.User@282003e1
所以可以看出,的确成功往容器中注入了User这个Bean

第二种:配置类实现了 ImportBeanDefinitionRegistrar 接口

public interface ImportBeanDefinitionRegistrar {

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {
       registerBeanDefinitions(importingClassMetadata, registry);
   }

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
   }

}

当配置类实现了 ImportBeanDefinitionRegistrar 接口,你就可以自定义往容器中注册想注入的Bean。这个接口相比与 ImportSelector 接口的主要区别就是,ImportSelector接口是返回类,你不能对这些类进行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加属性之类的

来个demo:
实现ImportBeanDefinitionRegistrar接口

public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        //构建一个 BeanDefinition , Bean的类型为 User
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
                // 设置 User 这个Bean的属性username的值为 TestName
                .addPropertyValue("username", "TestName")
                .getBeanDefinition();

        System.out.println("往Spring容器中注入User");
        //把 User 这个Bean的定义注册到容器中
        registry.registerBeanDefinition("user", beanDefinition);
    }

}

测试:

// 导入 UserImportBeanDefinitionRegistrar
@Import(UserImportBeanDefinitionRegistrar.class)
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将 Application 注册到容器中
        applicationContext.register(Application.class);
        applicationContext.refresh();

        User user = applicationContext.getBean(User.class);
        System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());
    }

}

结果:
往Spring容器中注入User
获取到的Bean为com.sanyou.spring.extension.User@6385cb26,属性username值为:TestName

第三种:普通配置类

比如上面的@EnableScheduling注解的SchedulingConfiguration.class配置类

image.png

其实不论是什么样的配置类,主要的作用就是往Spring容器中注册Bean,只不过注入的方式不同罢了。
这种方式有什么好处呢?

ImportSelector和ImportBeanDefinitionRegistrar的方法是有入参的,也就是注解的一些属性的封装,所以就可以根据注解的属性的配置,来决定应该往容器中注入什么样的类型的Bean,可以看一下 @EnableAsync 的实现,看看是如何根据@EnableAsync注解的属性来决定往容器中注入什么样的Bean。

@Import的核心作用就是导入配置类,并且还可以根据配合(比如@EnableXXX)使用的注解的属性来决定应该往Spring中注入什么样的Bean。

FeignClient接口在把FeignClientFactoryBean 注入到Spring时,就是通过ImportBeanDefinitionRegistrar 来注入的。

image.png

3、ApplicationListener

准确的说,这个应该不算spring&springboot当中的一个扩展点,ApplicationListener可以监听某个事件的event,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。但是spring内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。

Spring内置的事件:

  • ContextRefreshedEvent
    ApplicationContext 被初始化或刷新时,该事件被发布。也会在调用ConfigurableApplicationContext 接口中的refresh()方法时发生。此处的初始化是指:所有的Bean被成功装载,后处理Bean被检测并激活,所有Singleton Bean 被预实例化,ApplicationContext容器已就绪可用。

  • ContextStartedEvent
    当使用 ConfigurableApplicationContext (ApplicationContext子接口)接口中的 start() 方法启动 ApplicationContext时,该事件被发布。你可以连接你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序。

  • ContextStoppedEvent
    当使用 ConfigurableApplicationContext接口中的 stop()停止ApplicationContext 时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作

  • ContextClosedEvent
    当使用 ConfigurableApplicationContext接口中的 close()方法关闭 ApplicationContext 时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启

  • RequestHandledEvent
    这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件

在Spring容器启动的过程中,Spring会发布这些事件,如果你需要这Spring容器启动的某个时刻进行什么操作,只需要监听对应的事件即可。

Spring Event 补充

Spring Event 可以说是一种观察者模式的实现,主要是用来解耦合的。当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。
Spring Event 事件,就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以完成事件的发布订阅。

Spring Event api

  • ApplicationEvent:事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。
  • ApplicationListener:事件监听的接口,泛型是子类需要监听的事件类型,子类需要实现onApplicationEvent,参数就是事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件。
  • ApplicationEventPublisher:事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。
    ApplicationContext实现了ApplicationEventPublisher接口,所以通过ApplicationContext就可以发布事件
//第一步:创建一个火灾事件类

//火灾事件类继承ApplicationEvent
// 火灾事件
public class FireEvent extends ApplicationEvent {

    public FireEvent(String source) {
        super(source);
    }

}

//第二步:创建火灾事件的监听器

//打119的火灾事件的监听器:
public class Call119FireEventListener implements ApplicationListener<FireEvent> {

    @Override
    public void onApplicationEvent(FireEvent event) {
        System.out.println("打119");
    }

}
//救人的火灾事件的监听器:
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {

    @Override
    public void onApplicationEvent(FireEvent event) {
        System.out.println("救人");
    }

}

//事件和对应的监听都有了,接下来进行测试:
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将 事件监听器 注册到容器中
        applicationContext.register(Call119FireEventListener.class);
        applicationContext.register(SavePersonFireEventListener.class);
        applicationContext.refresh();

        // 发布着火的事件,触发监听
        applicationContext.publishEvent(new FireEvent("着火了"));
    }

}


//运行结果:
打119
救人

3.1 在Mybatis中的使用

Mybatis的SqlSessionFactoryBean监听了ApplicationEvent,然后判断如果是ContextRefreshedEvent就进行相应的处理,这个类还实现了FactoryBean接口。

public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (failFast && event instanceof ContextRefreshedEvent) {
        // fail-fast -> check all statements are completed
        this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
        }
    }
    
}

3.2 在SpringCloud的运用

在SpringCloud的中,当项目启动的时候,会自动往注册中心进行注册,这个过程当然也是基于事件来的。当web服务器启动完成之后,就发布ServletWebServerInitializedEvent事件。

image.png

然后不同的注册中心的实现都只需要监听这个事件,就知道web服务器已经创建好了,那么就可以往注册中心注册服务实例了。如果你的服务没往注册中心,看看是不是web环境,因为只有web环境才会发这个事件。

SpringCloud提供了一个抽象类 AbstractAutoServiceRegistration,实现了对WebServerInitializedEvent(ServletWebServerInitializedEvent的父类)事件的监听

image.png

一般不同的注册中心都会去继承这个类,监听项目启动,实现往注册中心服务端进行注册。

image.png

Spring Event事件在Spring内部中运用很多,是解耦合的利器。在实际项目中,你既可以监听SpringBoot内置的一些事件,进行相应的扩展,也可以基于这套模型在业务中自定义事件和相应的监听器,减少业务代码的耦合。

4、PropertySourceLoader

在SpringBoot环境下,外部化的配置文件支持properties和yaml两种格式,这两种配置文件格式是通过PropertySourceLoader来实现的。

对于PropertySourceLoader的实现,SpringBoot两个实现

  • PropertiesPropertySourceLoader:可以解析properties或者xml结尾的配置文件;
  • YamlPropertySourceLoader:解析以yml或者yaml结尾的配置文件
public interface PropertySourceLoader {

   //可以支持哪种文件格式的解析
   String[] getFileExtensions();

   // 解析配置文件,读出内容,封装成一个PropertySource<?>结合返回回去
   List<PropertySource<?>> load(String name, Resource resource) throws IOException;

}
image.png
image.png

PropertySourceLoader生效:
SpringBoot会先通过SPI机制加载所有PropertySourceLoader,然后遍历每个PropertySourceLoader,判断当前遍历的PropertySourceLoader,通过getFileExtensions获取到当前PropertySourceLoader能够支持哪些配置文件格式的解析,让后跟当前需要解析的文件格式进行匹配,如果能匹配上,那么就会使用当前遍历的PropertySourceLoader来解析配置文件。
PropertySourceLoader其实就属于策略接口,配置文件的解析就是策略模式的运用。

image.png
image.png

所以,如果我们要想实现json格式的支持,只需要自己实现可以用来解析json格式的配置文件的PropertySourceLoader就可以了。

第一步:自定义一个PropertySourceLoader

public class JsonPropertySourceLoader implements PropertySourceLoader {

    @Override
    public String[] getFileExtensions() {
        //这个方法表明这个类支持解析以json结尾的配置文件
        return new String[]{"json"};
    }

    @Override
    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {

        ReadableByteChannel readableByteChannel = resource.readableChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate((int) resource.contentLength());

        //将文件内容读到 ByteBuffer 中
        readableByteChannel.read(byteBuffer);
        //将读出来的字节转换成字符串
        String content = new String(byteBuffer.array());
        // 将字符串转换成 JSONObject
        JSONObject jsonObject = JSON.parseObject(content);

        Map<String, Object> map = new HashMap<>(jsonObject.size());
        //将 json 的键值对读出来,放入到 map 中
        for (String key : jsonObject.keySet()) {
            map.put(key, jsonObject.getString(key));
        }

        return Collections.singletonList(new MapPropertySource("jsonPropertySource", map));
    }

}

第二步:配置PropertySourceLoader

在spring.factories文件中配置一下就行了。

org.springframework.boot.env.PropertySourceLoader=\
com.xxx.JsonPropertySourceLoader

demo

//先创建一个application.json的配置文件
{
"sanxay.username":"三友的java日记”
}

//改造User
public class User {
    // 注入配置文件的属性
    @Value("${sanyou.username:}")
    private String username;
}

//启动项目
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);

        User user = applicationContext.getBean(User.class);

        System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());
    }


    @Bean
    public User user() {
        return new User();
    }

}

// 运行结果:
获取到的Bean为com.sanyou.spring.extension.User@481ba2cf,属性username值为:三友的java日记
成功将json配置文件的属性注入到User对象中。

Nacos对于PropertySourceLoader的实现

Nacos作为配置中心,不仅支持properties和yaml格式的文件,还支持json格式的配置文件,由于 SpringBoot已经支持了properties和yaml格式的文件的解析,那么Nacos只需要实现SpringBoot不支持的json就可以了。

image.png

5、EnvironmentPostProcessor

EnvironmentPostProcessor在SpringBoot启动过程中,也会调用,也是通过SPI机制来加载扩展的。

image.png

EnvironmentPostProcessor是用来处理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在这个对象的。
说这个类的主要原因,主要不是说扩展,而是他的一个实现类很关键。

image.png

这个类的作用就是用来处理外部化配置文件的,也就是这个类是用来处理配置文件的,通过前面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment里面。

可以通过在EnvironmentPostProcessor的postProcessEnvironment上打断点来查看配置文件的加载过程。尤其是在配置文件修改后,不生效的时候。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容