Springboot/Spring 擴(kuò)展點(diǎn)(二): Springboot啟動(dòng)擴(kuò)展點(diǎn)

1、自動(dòng)裝配

當(dāng)項(xiàng)目啟動(dòng)的時(shí)候,會(huì)去從所有的spring.factories文件中讀取@EnableAutoConfiguration鍵對應(yīng)的值,拿到配置類,然后根據(jù)一些條件判斷,決定哪些配置可以使用,哪些不能使用。自動(dòng)裝配是SPI機(jī)制的一種運(yùn)用場景。

在SpringBoot中,@EnableAutoConfiguration是通過@SpringBootApplication來使用的。

自動(dòng)裝配以及SPI機(jī)制見 Spring Boot自動(dòng)配置

  • 調(diào)用時(shí)機(jī):項(xiàng)目啟動(dòng)時(shí)加載;

  • 使用場景:框架整合Springboot的時(shí)候,通過自動(dòng)裝配來實(shí)現(xiàn)項(xiàng)目啟動(dòng),框架就自動(dòng)啟動(dòng)的,比如Mybatis整合SpringBoot。
    image.png
  • 自定義實(shí)現(xiàn)
//第一步,寫個(gè)配置類:
@Configuration
public class UserAutoConfiguration {

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

}


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

//到這就已經(jīng)實(shí)現(xiàn)了自動(dòng)裝配的擴(kuò)展。


//接下來進(jìn)行測試:
@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);
    }

}

//運(yùn)行結(jié)果:
調(diào)用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@3406472c
獲取到的Bean為com.sanyou.spring.extension.User@3406472c
從運(yùn)行結(jié)果可以看出,自動(dòng)裝配起了作用,并且雖然往容器中注入的Bean的class類型為UserFactoryBean,但是最終會(huì)調(diào)用UserFactoryBean的getObject的實(shí)現(xiàn)獲取到User對象。

2、Import注解

@Import注解:導(dǎo)入的配置類的分類,在項(xiàng)目中可能不常見,但是下面這兩個(gè)注解肯定常見。

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

@EnableScheduling:開啟定時(shí)任務(wù);
@EnableAsync:開啟異步執(zhí)行;

通過這兩個(gè)注解可以看出,他們都使用了@Import注解,所以真正起作用的是@Import注解。并且在很多情況下,@EnbaleXXX這種格式的注解,都是通過@Import注解起作用的,代表開啟了某個(gè)功能。

@Import注解導(dǎo)入的配置類可以分為三種情況:

第一種:配置類實(shí)現(xiàn)了 ImportSelector 接口

public interface ImportSelector {

   String[] selectImports(AnnotationMetadata importingClassMetadata);

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

}

當(dāng)配置類實(shí)現(xiàn)了 ImportSelector 接口的時(shí)候,就會(huì)調(diào)用 selectImports 方法的實(shí)現(xiàn),獲取一批類的全限定名,并把這些類注冊到Spring容器中。

UserImportSelector實(shí)現(xiàn)了ImportSelector,selectImports方法返回User的全限定名,并把User這個(gè)類注冊容器中

public class UserImportSelector implements ImportSelector {

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

}

測試:
// @Import 注解導(dǎo)入 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));
    }

}

結(jié)果:
調(diào)用 UserImportSelector 的 selectImports 方法獲取一批類限定名
獲取到的Bean為com.sanyou.spring.extension.User@282003e1
所以可以看出,的確成功往容器中注入了User這個(gè)Bean

第二種:配置類實(shí)現(xiàn)了 ImportBeanDefinitionRegistrar 接口

public interface ImportBeanDefinitionRegistrar {

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

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

}

當(dāng)配置類實(shí)現(xiàn)了 ImportBeanDefinitionRegistrar 接口,你就可以自定義往容器中注冊想注入的Bean。這個(gè)接口相比與 ImportSelector 接口的主要區(qū)別就是,ImportSelector接口是返回類,你不能對這些類進(jìn)行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加屬性之類的。

來個(gè)demo:
實(shí)現(xiàn)ImportBeanDefinitionRegistrar接口

public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        //構(gòu)建一個(gè) BeanDefinition , Bean的類型為 User
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
                // 設(shè)置 User 這個(gè)Bean的屬性username的值為 TestName
                .addPropertyValue("username", "TestName")
                .getBeanDefinition();

        System.out.println("往Spring容器中注入U(xiǎn)ser");
        //把 User 這個(gè)Bean的定義注冊到容器中
        registry.registerBeanDefinition("user", beanDefinition);
    }

}

測試:

// 導(dǎo)入 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());
    }

}

結(jié)果:
往Spring容器中注入U(xiǎn)ser
獲取到的Bean為com.sanyou.spring.extension.User@6385cb26,屬性username值為:TestName

第三種:普通配置類

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

image.png

其實(shí)不論是什么樣的配置類,主要的作用就是往Spring容器中注冊Bean,只不過注入的方式不同罷了。
這種方式有什么好處呢?

ImportSelector和ImportBeanDefinitionRegistrar的方法是有入?yún)⒌模簿褪亲⒔獾囊恍傩缘姆庋b,所以就可以根據(jù)注解的屬性的配置,來決定應(yīng)該往容器中注入什么樣的類型的Bean,可以看一下 @EnableAsync 的實(shí)現(xiàn),看看是如何根據(jù)@EnableAsync注解的屬性來決定往容器中注入什么樣的Bean。

@Import的核心作用就是導(dǎo)入配置類,并且還可以根據(jù)配合(比如@EnableXXX)使用的注解的屬性來決定應(yīng)該往Spring中注入什么樣的Bean。

FeignClient接口在把FeignClientFactoryBean 注入到Spring時(shí),就是通過ImportBeanDefinitionRegistrar 來注入的。

image.png

3、ApplicationListener

準(zhǔn)確的說,這個(gè)應(yīng)該不算spring&springboot當(dāng)中的一個(gè)擴(kuò)展點(diǎn),ApplicationListener可以監(jiān)聽某個(gè)事件的event,觸發(fā)時(shí)機(jī)可以穿插在業(yè)務(wù)方法執(zhí)行過程中,用戶可以自定義某個(gè)業(yè)務(wù)事件。但是spring內(nèi)部也有一些內(nèi)置事件,這種事件,可以穿插在啟動(dòng)調(diào)用中。我們也可以利用這個(gè)特性,來自己做一些內(nèi)置事件的監(jiān)聽器來達(dá)到和前面一些觸發(fā)點(diǎn)大致相同的事情。

Spring內(nèi)置的事件:

  • ContextRefreshedEvent
    ApplicationContext 被初始化或刷新時(shí),該事件被發(fā)布。也會(huì)在調(diào)用ConfigurableApplicationContext 接口中的refresh()方法時(shí)發(fā)生。此處的初始化是指:所有的Bean被成功裝載,后處理Bean被檢測并激活,所有Singleton Bean 被預(yù)實(shí)例化,ApplicationContext容器已就緒可用。

  • ContextStartedEvent
    當(dāng)使用 ConfigurableApplicationContext (ApplicationContext子接口)接口中的 start() 方法啟動(dòng) ApplicationContext時(shí),該事件被發(fā)布。你可以連接你的數(shù)據(jù)庫,或者你可以在接受到這個(gè)事件后重啟任何停止的應(yīng)用程序。

  • ContextStoppedEvent
    當(dāng)使用 ConfigurableApplicationContext接口中的 stop()停止ApplicationContext 時(shí),發(fā)布這個(gè)事件。你可以在接受到這個(gè)事件后做必要的清理的工作

  • ContextClosedEvent
    當(dāng)使用 ConfigurableApplicationContext接口中的 close()方法關(guān)閉 ApplicationContext 時(shí),該事件被發(fā)布。一個(gè)已關(guān)閉的上下文到達(dá)生命周期末端;它不能被刷新或重啟

  • RequestHandledEvent
    這是一個(gè) web-specific 事件,告訴所有 bean HTTP 請求已經(jīng)被服務(wù)。只能應(yīng)用于使用DispatcherServlet的Web應(yīng)用。在使用Spring作為前端的MVC控制器時(shí),當(dāng)Spring處理用戶請求結(jié)束后,系統(tǒng)會(huì)自動(dòng)觸發(fā)該事件

在Spring容器啟動(dòng)的過程中,Spring會(huì)發(fā)布這些事件,如果你需要這Spring容器啟動(dòng)的某個(gè)時(shí)刻進(jìn)行什么操作,只需要監(jiān)聽對應(yīng)的事件即可。

Spring Event 補(bǔ)充

Spring Event 可以說是一種觀察者模式的實(shí)現(xiàn),主要是用來解耦合的。當(dāng)發(fā)生了某件事,只要發(fā)布一個(gè)事件,對這個(gè)事件的監(jiān)聽者(觀察者)就可以對事件進(jìn)行響應(yīng)或者處理。
Spring Event 事件,就是Spring實(shí)現(xiàn)了這種事件模型,你只需要基于Spring提供的API進(jìn)行擴(kuò)展,就可以完成事件的發(fā)布訂閱。

Spring Event api

  • ApplicationEvent:事件的父類,所有具體的事件都得繼承這個(gè)類,構(gòu)造方法的參數(shù)是這個(gè)事件攜帶的參數(shù),監(jiān)聽器就可以通過這個(gè)參數(shù)來進(jìn)行一些業(yè)務(wù)操作。
  • ApplicationListener:事件監(jiān)聽的接口,泛型是子類需要監(jiān)聽的事件類型,子類需要實(shí)現(xiàn)onApplicationEvent,參數(shù)就是事件類型,onApplicationEvent方法的實(shí)現(xiàn)就代表了對事件的處理,當(dāng)事件發(fā)生時(shí),Spring會(huì)回調(diào)onApplicationEvent方法的實(shí)現(xiàn),傳入發(fā)布的事件。
  • ApplicationEventPublisher:事件發(fā)布器,通過publishEvent方法就可以發(fā)布一個(gè)事件,然后就可以觸發(fā)監(jiān)聽這個(gè)事件的監(jiān)聽器的回調(diào)。
    ApplicationContext實(shí)現(xiàn)了ApplicationEventPublisher接口,所以通過ApplicationContext就可以發(fā)布事件
//第一步:創(chuàng)建一個(gè)火災(zāi)事件類

//火災(zāi)事件類繼承ApplicationEvent
// 火災(zāi)事件
public class FireEvent extends ApplicationEvent {

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

}

//第二步:創(chuàng)建火災(zāi)事件的監(jiān)聽器

//打119的火災(zāi)事件的監(jiān)聽器:
public class Call119FireEventListener implements ApplicationListener<FireEvent> {

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

}
//救人的火災(zāi)事件的監(jiān)聽器:
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {

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

}

//事件和對應(yīng)的監(jiān)聽都有了,接下來進(jìn)行測試:
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 事件監(jiān)聽器 注冊到容器中
        applicationContext.register(Call119FireEventListener.class);
        applicationContext.register(SavePersonFireEventListener.class);
        applicationContext.refresh();

        // 發(fā)布著火的事件,觸發(fā)監(jiān)聽
        applicationContext.publishEvent(new FireEvent("著火了"));
    }

}


//運(yùn)行結(jié)果:
打119
救人

3.1 在Mybatis中的使用

Mybatis的SqlSessionFactoryBean監(jiān)聽了ApplicationEvent,然后判斷如果是ContextRefreshedEvent就進(jìn)行相應(yīng)的處理,這個(gè)類還實(shí)現(xiàn)了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的運(yùn)用

在SpringCloud的中,當(dāng)項(xiàng)目啟動(dòng)的時(shí)候,會(huì)自動(dòng)往注冊中心進(jìn)行注冊,這個(gè)過程當(dāng)然也是基于事件來的。當(dāng)web服務(wù)器啟動(dòng)完成之后,就發(fā)布ServletWebServerInitializedEvent事件。

image.png

然后不同的注冊中心的實(shí)現(xiàn)都只需要監(jiān)聽這個(gè)事件,就知道web服務(wù)器已經(jīng)創(chuàng)建好了,那么就可以往注冊中心注冊服務(wù)實(shí)例了。如果你的服務(wù)沒往注冊中心,看看是不是web環(huán)境,因?yàn)橹挥衱eb環(huán)境才會(huì)發(fā)這個(gè)事件。

SpringCloud提供了一個(gè)抽象類 AbstractAutoServiceRegistration,實(shí)現(xiàn)了對WebServerInitializedEvent(ServletWebServerInitializedEvent的父類)事件的監(jiān)聽

image.png

一般不同的注冊中心都會(huì)去繼承這個(gè)類,監(jiān)聽項(xiàng)目啟動(dòng),實(shí)現(xiàn)往注冊中心服務(wù)端進(jìn)行注冊。

image.png

Spring Event事件在Spring內(nèi)部中運(yùn)用很多,是解耦合的利器。在實(shí)際項(xiàng)目中,你既可以監(jiān)聽SpringBoot內(nèi)置的一些事件,進(jìn)行相應(yīng)的擴(kuò)展,也可以基于這套模型在業(yè)務(wù)中自定義事件和相應(yīng)的監(jiān)聽器,減少業(yè)務(wù)代碼的耦合。

4、PropertySourceLoader

在SpringBoot環(huán)境下,外部化的配置文件支持properties和yaml兩種格式,這兩種配置文件格式是通過PropertySourceLoader來實(shí)現(xiàn)的。

對于PropertySourceLoader的實(shí)現(xiàn),SpringBoot兩個(gè)實(shí)現(xiàn)

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

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

   // 解析配置文件,讀出內(nèi)容,封裝成一個(gè)PropertySource<?>結(jié)合返回回去
   List<PropertySource<?>> load(String name, Resource resource) throws IOException;

}
image.png
image.png

PropertySourceLoader生效:
SpringBoot會(huì)先通過SPI機(jī)制加載所有PropertySourceLoader,然后遍歷每個(gè)PropertySourceLoader,判斷當(dāng)前遍歷的PropertySourceLoader,通過getFileExtensions獲取到當(dāng)前PropertySourceLoader能夠支持哪些配置文件格式的解析,讓后跟當(dāng)前需要解析的文件格式進(jìn)行匹配,如果能匹配上,那么就會(huì)使用當(dāng)前遍歷的PropertySourceLoader來解析配置文件。
PropertySourceLoader其實(shí)就屬于策略接口,配置文件的解析就是策略模式的運(yùn)用。

image.png
image.png

所以,如果我們要想實(shí)現(xiàn)json格式的支持,只需要自己實(shí)現(xiàn)可以用來解析json格式的配置文件的PropertySourceLoader就可以了。

第一步:自定義一個(gè)PropertySourceLoader

public class JsonPropertySourceLoader implements PropertySourceLoader {

    @Override
    public String[] getFileExtensions() {
        //這個(gè)方法表明這個(gè)類支持解析以json結(jié)尾的配置文件
        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());

        //將文件內(nèi)容讀到 ByteBuffer 中
        readableByteChannel.read(byteBuffer);
        //將讀出來的字節(jié)轉(zhuǎn)換成字符串
        String content = new String(byteBuffer.array());
        // 將字符串轉(zhuǎn)換成 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

//先創(chuàng)建一個(gè)application.json的配置文件
{
"sanxay.username":"三友的java日記”
}

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

//啟動(dòng)項(xiàng)目
@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();
    }

}

// 運(yùn)行結(jié)果:
獲取到的Bean為com.sanyou.spring.extension.User@481ba2cf,屬性username值為:三友的java日記
成功將json配置文件的屬性注入到User對象中。

Nacos對于PropertySourceLoader的實(shí)現(xiàn)

Nacos作為配置中心,不僅支持properties和yaml格式的文件,還支持json格式的配置文件,由于 SpringBoot已經(jīng)支持了properties和yaml格式的文件的解析,那么Nacos只需要實(shí)現(xiàn)SpringBoot不支持的json就可以了。

image.png

5、EnvironmentPostProcessor

EnvironmentPostProcessor在SpringBoot啟動(dòng)過程中,也會(huì)調(diào)用,也是通過SPI機(jī)制來加載擴(kuò)展的。

image.png

EnvironmentPostProcessor是用來處理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在這個(gè)對象的。
說這個(gè)類的主要原因,主要不是說擴(kuò)展,而是他的一個(gè)實(shí)現(xiàn)類很關(guān)鍵。

image.png

這個(gè)類的作用就是用來處理外部化配置文件的,也就是這個(gè)類是用來處理配置文件的,通過前面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment里面。

可以通過在EnvironmentPostProcessor的postProcessEnvironment上打斷點(diǎn)來查看配置文件的加載過程。尤其是在配置文件修改后,不生效的時(shí)候。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容