<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          三萬字盤點Spring/Boot的常用擴展點!!

          共 36387字,需瀏覽 73分鐘

           ·

          2022-10-08 16:45

          近期文章精選?:

          Spring對于每個Java后端程序員來說肯定不陌生,日常開發(fā)和面試必備的。本文就來盤點Spring/SpringBoot常見的擴展點,同時也來看看常見的開源框架是如何基于這些擴展點跟Spring/SpringBoot整合的。

          話不多說,直接進入正題。

          FactoryBean

          提起FactoryBean,就有一道“著名”的面試題“說一說FactoryBean和BeanFactory的區(qū)別”。其實這兩者除了名字有點像,沒有半毛錢關(guān)系。。

          c9576c87894fca8b83300de829d9afce.webp

          BeanFactory是Bean的工廠,可以幫我們生成想要的Bean,而FactoryBean就是一種Bean的類型。當往容器中注入class類型為FactoryBean的類型的時候,最終生成的Bean是用過FactoryBean的getObject獲取的。

          927c3c1143e11a11b3e9fcab51eec3ab.webp

          來個FactoryBean的Demo

          定義一個UserFactoryBean,實現(xiàn)FactoryBean接口,getObject方法返回一個User對象

                
                public?class?UserFactoryBean?implements?FactoryBean<User>?{

          ????@Override
          ????public?User?getObject()?throws?Exception?{
          ????????User?user?=?new?User();
          ????????System.out.println("調(diào)用?UserFactoryBean?的?getObject?方法生成?Bean:"?+?user);
          ????????return?user;
          ????}

          ????@Override
          ????public?Class<?>?getObjectType()?{
          ????????//?這個?FactoryBean?返回的Bean的類型
          ????????return?User.class;
          ????}

          }

          測試類:

                
                public?class?Application?{

          ????public?static?void?main(String[]?args)?{
          ????????AnnotationConfigApplicationContext?applicationContext?=?new?AnnotationConfigApplicationContext();
          ????????//將?UserFactoryBean?注冊到容器中
          ????????applicationContext.register(UserFactoryBean.class);
          ????????applicationContext.refresh();

          ????????System.out.println("獲取到的Bean為"?+?applicationContext.getBean(User.class));
          ????}

          }

          結(jié)果:

                
                調(diào)用?UserFactoryBean?的?getObject?方法生成?Bean:com.sanyou.spring.extension.User@396e2f39
          獲取到的Bean為com.sanyou.spring.extension.User@396e2f39

          從結(jié)果可以看出,明明注冊到Spring容器的是UserFactoryBean,但是卻能從容器中獲取到User類型的Bean,User這個Bean就是通過UserFactoryBean的getObject方法返回的。

          FactoryBean在開源框架中的使用

          1、 在Mybatis中的使用

          Mybatis在整合Spring的時候,就是通過FactoryBean來實現(xiàn)的,這也就是為什么在Spring的Bean中可以注入Mybatis的Mapper接口的動態(tài)代理對象的原因。

          代碼如下,省略了不重要的代碼。

                
                public?class?MapperFactoryBean<T>?extends?SqlSessionDaoSupport?implements?FactoryBean<T>?{
          ??
          ??//?mapper的接口類型
          ??private?Class<T>?mapperInterface;
          ?
          ??@Override
          ??public?T?getObject()?throws?Exception?{
          ????//?通過SqlSession獲取接口的動態(tài)搭理對象
          ????return?getSqlSession().getMapper(this.mapperInterface);
          ??}
          ??
          ??@Override
          ??public?Class<T>?getObjectType()?{
          ????return?this.mapperInterface;
          ??}
          ??
          }

          getObject方法的實現(xiàn)就是返回通過SqlSession獲取到的Mapper接口的動態(tài)代理對象。

          而@MapperScan注解的作用就是將每個接口對應的MapperFactoryBean注冊到Spring容器的。

          2、在OpenFeign中的使用

          FeignClient接口的動態(tài)代理也是通過FactoryBean注入到Spring中的。

                
                class?FeignClientFactoryBean?implements?FactoryBean<Object>,?InitializingBean,?ApplicationContextAware?{
          ????
          ????//?FeignClient接口類型
          ????private?Class<?>?type;
          ????
          ????@Override
          ???public?Object?getObject()?throws?Exception?{
          ??????return?getTarget();
          ???}
          ????
          ????@Override
          ???public?Class<?>?getObjectType()?{
          ??????return?type;
          ???}
          }

          getObject方法是調(diào)用getTarget方法來返回的動態(tài)代理。

          @EnableFeignClients注解的作用就是將每個接口對應的FeignClientFactoryBean注入到Spring容器的。

          一般來說,F(xiàn)actoryBean 比較適合那種復雜Bean的構(gòu)建,在其他框架整合Spring的時候用的比較多。

          @Import注解

          @Import注解在項目中可能不常見,但是下面這兩個注解肯定常見。

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

          @EnableScheduling和@EnableAsync兩個注解,一個是開啟定時任務,一個是開啟異步執(zhí)行。通過這兩個注解可以看出,他們都使用了@Import注解,所以真正起作用的是@Import注解。并且在很多情況下,@EnbaleXXX這種格式的注解,都是通過@Import注解起作用的,代表開啟了某個功能。

          @Import注解導入的配置類的分類

          @Import注解導入的配置類可以分為三種情況:

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

                
                public?interface?ImportSelector?{

          ???String[]?selectImports(AnnotationMetadata?importingClassMetadata);

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

          }

          當配置類實現(xiàn)了 ImportSelector 接口的時候,就會調(diào)用 selectImports 方法的實現(xiàn),獲取一批類的全限定名,最終這些類就會被注冊到Spring容器中。

          UserImportSelector實現(xiàn)了ImportSelector,selectImports方法返回User的全限定名,代表吧User這個類注冊容器中

                
                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?注解導入?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這個Bean

          第二種:配置類實現(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)?{
          ???}

          }

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

          來個demo:

          實現(xiàn)ImportBeanDefinitionRegistrar接口

                
                public?class?UserImportBeanDefinitionRegistrar?implements?ImportBeanDefinitionRegistrar?{

          ????@Override
          ????public?void?registerBeanDefinitions(AnnotationMetadata?importingClassMetadata,?BeanDefinitionRegistry?registry,?BeanNameGenerator?importBeanNameGenerator)?{
          ????????//構(gòu)建一個?BeanDefinition?,?Bean的類型為?User
          ????????AbstractBeanDefinition?beanDefinition?=?BeanDefinitionBuilder.rootBeanDefinition(User.class)
          ????????????????//?設置?User?這個Bean的屬性username的值為三友的java日記
          ????????????????.addPropertyValue("username",?"三友的java日記")
          ????????????????.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());
          ????}

          }

          結(jié)果:

                
                往Spring容器中注入User
          獲取到的Bean為com.sanyou.spring.extension.User@6385cb26,屬性username值為:三友的java日記

          第三種:配置類什么接口都沒實現(xiàn)

          這種就不演示了,就是一個普普通通的類。

          總結(jié)

          24096b476dbc71e86d3b6577eb3eb609.webp@Import注解作用示意圖

          其實不論是什么樣的配置類,主要的作用就是往Spring容器中注冊Bean,只不過注入的方式不同罷了。

          這種方式有什么好處呢?

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

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

          Bean的生命周期

          第一節(jié)講的FactoryBean是一種特殊的Bean的類型,@Import注解是往Spring容器中注冊Bean。其實不論是@Import注解,還是@Component、@Bean等注解,又或是xml配置,甚至是demo中的register方法,其實主要都是做了一件事,那就是往Spring容器去注冊Bean。

          cd5dcce62da53373f05096a2b8a3d737.webpBean注冊示意圖

          為什么需要去注冊Bean?

          當然是為了讓Spring知道要為我們生成Bean,并且需要按照我的要求來生成Bean,比如說,我要@Autowired一個對象,那么你在創(chuàng)建Bean的過程中,就得給我@Autowired一個對象,這就是一個IOC的過程。所以這就涉及了Bean的創(chuàng)建,銷毀的過程,也就是面試常問的Bean的生命周期。我之前寫過 Spring bean到底是如何創(chuàng)建的?(上)Spring bean到底是如何創(chuàng)建的?(下)兩篇文章,來剖析Bean的生命周期的源碼,有需要的小伙伴可以看一下。

          本節(jié)來著重看一下,一個Bean在創(chuàng)建的過程中,有哪些常見的操作Spring在Bean的創(chuàng)建過程中給我們完成,并且操作的順序是什么樣的。

          話不多說,直接測試,基于結(jié)果來分析。

          Bean生命周期的回調(diào)

          先來測試

          創(chuàng)建LifeCycle類

          創(chuàng)建了一個LifeCycle,實現(xiàn)了 InitializingBean、ApplicationContextAware、DisposableBean接口,加了@PostConstruct、@PreDestroy注解,注入了一個User對象。

                
                public?class?LifeCycle?implements?InitializingBean,?ApplicationContextAware,?DisposableBean?{

          ????@Autowired
          ????private?User?user;

          ????public?LifeCycle()?{
          ????????System.out.println("LifeCycle對象被創(chuàng)建了");
          ????}

          ????/**
          ?????*?實現(xiàn)的?Aware?回調(diào)接口
          ?????*
          ?????*?@param?applicationContext
          ?????*?@throws?BeansException
          ?????*/

          ????@Override
          ????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{
          ????????System.out.println("Aware接口起作用,setApplicationContext被調(diào)用了,此時user="?+?user);
          ????}

          ????@PostConstruct
          ????public?void?postConstruct()?{
          ????????System.out.println("@PostConstruct注解起作用,postConstruct方法被調(diào)用了");
          ????}

          ????/**
          ?????*?實現(xiàn)?InitializingBean?接口
          ?????*
          ?????*?@throws?Exception
          ?????*/

          ????@Override
          ????public?void?afterPropertiesSet()?throws?Exception?{
          ????????System.out.println("InitializingBean接口起作用,afterPropertiesSet方法被調(diào)用了");
          ????}

          ????/**
          ?????*?通過?{@link?Bean#initMethod()}來指定
          ?????*
          ?????*?@throws?Exception
          ?????*/

          ????public?void?initMethod()?throws?Exception?{
          ????????System.out.println("@Bean#initMethod()起作用,initMethod方法被調(diào)用了");
          ????}

          ????@PreDestroy
          ????public?void?preDestroy()?throws?Exception?{
          ????????System.out.println("@PreDestroy注解起作用,preDestroy方法被調(diào)用了");
          ????}

          ????/**
          ?????*?通過?{@link?Bean#destroyMethod()}來指定
          ?????*
          ?????*?@throws?Exception
          ?????*/

          ????public?void?destroyMethod()?throws?Exception?{
          ????????System.out.println("@Bean#destroyMethod()起作用,destroyMethod方法被調(diào)用了");
          ????}

          ????/**
          ?????*?實現(xiàn)?DisposableBean?注解
          ?????*
          ?????*?@throws?Exception
          ?????*/

          ????@Override
          ????public?void?destroy()?throws?Exception?{
          ????????System.out.println("DisposableBean接口起作用,destroy方法被調(diào)用了");
          ????}

          }
          聲明LifeCycle

          通過@Bean聲明了LifeCycle,并且initMethod和destroyMethod屬性分別指定到了LifeCycle類的initMethod方法和destroyMethod方法

                
                @Bean(initMethod?=?"initMethod",?destroyMethod?=?"destroyMethod")
          public?LifeCycle?lifeCycle()?{
          ????return?new?LifeCycle();
          }
          測試
                
                public?class?Application?{

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

          ????????//?關(guān)閉上下文,觸發(fā)銷毀操作
          ????????applicationContext.close();
          ????}

          ????@Bean(initMethod?=?"initMethod",?destroyMethod?=?"destroyMethod")
          ????public?LifeCycle?lifeCycle()?{
          ????????return?new?LifeCycle();
          ????}

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

          }

          執(zhí)行結(jié)果:

                
                LifeCycle對象被創(chuàng)建了
          Aware接口起作用,setApplicationContext被調(diào)用了,此時user=com.sanyou.spring.extension.User@57d5872c
          @PostConstruct注解起作用,postConstruct方法被調(diào)用了
          InitializingBean接口起作用,afterPropertiesSet方法被調(diào)用了
          @Bean#initMethod()起作用,initMethod方法被調(diào)用了
          @PreDestroy注解起作用,preDestroy方法被調(diào)用了
          DisposableBean接口起作用,destroy方法被調(diào)用了
          @Bean#destroyMethod()起作用,destroyMethod方法被調(diào)用了

          分析結(jié)果

          通過測試的結(jié)果可以看出,Bean在創(chuàng)建和銷毀的過程當我們實現(xiàn)了某些接口或者加了某些注解,Spring就會回調(diào)我們實現(xiàn)的接口或者執(zhí)行的方法。

          同時,在執(zhí)行setApplicationContext的時候,能打印出User對象,說明User已經(jīng)被注入了,說明注入發(fā)生在setApplicationContext之前。

          這里畫張圖總結(jié)一下Bean創(chuàng)建和銷毀過程中調(diào)用的順序。

          9cb6cf61c360249c349939efe9b96ea7.webp回調(diào)順序

          紅色部分發(fā)生在Bean的創(chuàng)建過程,灰色部分發(fā)生在Bean銷毀的過程中,在容器關(guān)閉的時候,就會銷毀Bean。

          這里說一下圖中的Aware接口指的是什么。其余的其實沒什么好說的,就是按照這種方式配置,Spring會調(diào)用對應的方法而已。

          Aware接口是指以Aware結(jié)尾的一些Spring提供的接口,當你的Bean實現(xiàn)了這些接口的話,在創(chuàng)建過程中會回調(diào)對應的set方法,并傳入響應的對象。

          這里列舉幾個Aware接口以及它們的作用

          接口 作用
          ApplicationContextAware 注入ApplicationContext
          ApplicationEventPublisherAware 注入ApplicationEventPublisher事件發(fā)布器
          BeanFactoryAware 注入BeanFactory
          BeanNameAware 注入Bean的名稱

          有了這些回調(diào),比如說我的Bean想拿到ApplicationContext,不僅可以通過@Autowired注入,還可以通過實現(xiàn)ApplicationContextAware接口拿到。

          通過上面的例子我們知道了比如說@PostConstruct注解、@Autowired注解、@PreDestroy注解的作用,但是它們是如何在不同的階段實現(xiàn)的呢?接著往下看。

          BeanPostProcessor

          BeanPostProcessor,中文名 Bean的后置處理器,在Bean創(chuàng)建的過程中起作用。

          BeanPostProcessor是Bean在創(chuàng)建過程中一個非常重要的擴展點,因為每個Bean在創(chuàng)建的各個階段,都會回調(diào)BeanPostProcessor及其子接口的方法,傳入正在創(chuàng)建的Bean對象,這樣如果想對Bean創(chuàng)建過程中某個階段進行自定義擴展,那么就可以自定義BeanPostProcessor來完成。

          說得簡單點,BeanPostProcessor就是在Bean創(chuàng)建過程中留的口子,通過這個口子可以對正在創(chuàng)建的Bean進行擴展。只不過Bean創(chuàng)建的階段比較多,然后BeanPostProcessor接口以及他的子接口InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多方法,可以使得在不同的階段都可以拿到正在創(chuàng)建的Bean進行擴展。

          來個Demo

          現(xiàn)在需要實現(xiàn)一個這樣的需求,如果Bean的類型是User,那么就設置這個對象的username屬性為 ”三友的java日記“。

          那么就可以這么寫:

                
                public?class?UserBeanPostProcessor?implements?BeanPostProcessor?{

          ????@Override
          ????public?Object?postProcessAfterInitialization(Object?bean,?String?beanName)?throws?BeansException?{
          ????????if?(bean?instanceof?User)?{
          ????????????//如果當前的Bean的類型是?User?,就把這個對象?username?的屬性賦值為?三友的java日記
          ????????????((User)?bean).setUsername("三友的java日記");
          ????????}

          ????????return?bean;
          ????}

          }

          測試:

                
                public?class?Application?{

          ????public?static?void?main(String[]?args)?{
          ????????AnnotationConfigApplicationContext?applicationContext?=?new?AnnotationConfigApplicationContext();
          ????????//將?UserBeanPostProcessor?和??User?注冊到容器中
          ????????applicationContext.register(UserBeanPostProcessor.class);
          ????????applicationContext.register(User.class);
          ????????applicationContext.refresh();

          ????????User?user?=?applicationContext.getBean(User.class);
          ????????System.out.println("獲取到的Bean為"?+?user?+?",屬性username值為:"?+?user.getUsername());
          ????}

          }

          測試結(jié)果:

                
                獲取到的Bean為com.sanyou.spring.extension.User@21a947fe,屬性username值為:三友的java日記

          從結(jié)果可以看出,每個生成的Bean在執(zhí)行到某個階段的時候,都會回調(diào)UserBeanPostProcessor,然后UserBeanPostProcessor就會判斷當前創(chuàng)建的Bean的類型,如果是User類型,那么就會將username的屬性設置為 ”三友的java日記“。

          Spring內(nèi)置的BeanPostProcessor

          這里我列舉了常見的一些BeanPostProcessor的實現(xiàn)以及它們的作用

          BeanPostProcessor 作用
          AutowiredAnnotationBeanPostProcessor 處理@Autowired、@Value注解
          CommonAnnotationBeanPostProcessor 處理@Resource、@PostConstruct、@PreDestroy注解
          AnnotationAwareAspectJAutoProxyCreator 處理一些注解或者是AOP切面的動態(tài)代理
          ApplicationContextAwareProcessor 處理Aware接口注入的
          AsyncAnnotationBeanPostProcessor 處理@Async注解
          ScheduledAnnotationBeanPostProcessor 處理@Scheduled注解

          通過列舉的這些BeanPostProcessor的實現(xiàn)可以看出,Spring Bean的很多注解的處理都是依靠BeanPostProcessor及其子類的實現(xiàn)來完成的,這也回答了上一小節(jié)的疑問,處理@Autowired、@PostConstruct、@PreDestroy注解是如何起作用的,其實就是通過BeanPostProcessor,在Bean的不同階段來調(diào)用對應的方法起作用的。

          BeanPostProcessor在Dubbo中的使用

          在Dubbo中可以通過@DubboReference(@Reference)來引用生產(chǎn)者提供的接口,這個注解的處理也是依靠ReferenceAnnotationBeanPostProcessor,也就是 BeanPostProcessor 的擴展來實現(xiàn)的。

                
                public?class?ReferenceAnnotationBeanPostProcessor?
          ???????extends?AbstractAnnotationBeanPostProcessor?
          ???????implements?ApplicationContextAware,?BeanFactoryPostProcessor?
          {
          ????????//?忽略
          }

          當Bean在創(chuàng)建的某一階段,走到了ReferenceAnnotationBeanPostProcessor這個類,就會根據(jù)反射找出這個類有沒有@DubboReference(@Reference)注解,有的話就構(gòu)建一個動態(tài)搭理注入就可以了。

          BeanPostProcessor在Spring Bean的擴展中扮演著重要的角色,是Spring Bean生命周期中很重要的一部分。正是因為有了BeanPostProcessor,你就可以在Bean創(chuàng)建過程中的任意一個階段擴展自己想要的東西。

          BeanFactoryPostProcessor

          通過上面一節(jié)我們知道 BeanPostProcessor 是對Bean的處理,那么BeanFactoryPostProcessor很容易就猜到是對BeanFactory,也就是Spring容器的處理。

          舉個例子,如果我們想禁止循環(huán)依賴,那么就可以這么寫。

                
                public?class?MyBeanFactoryPostProcessor?implements?BeanFactoryPostProcessor?{

          ????@Override
          ????public?void?postProcessBeanFactory(ConfigurableListableBeanFactory?beanFactory)?throws?BeansException?{
          ????????//?禁止循環(huán)依賴
          ????????((DefaultListableBeanFactory)?beanFactory).setAllowCircularReferences(false);
          ????}

          }

          后面只需要將注入到Spring容器中就會生效。

          BeanFactoryPostProcessor是可以對Spring容器做處理的,方法的入?yún)⒕褪荢pring的容器,通過這個接口,就對容器進行為所欲為的操作。

          Spring SPI機制

          SPI全稱為 (Service Provider Interface),是一種動態(tài)替換發(fā)現(xiàn)的機制,一種解耦非常優(yōu)秀的思想,SPI可以很靈活的讓接口和實現(xiàn)分離, 讓api提供者只提供接口,第三方來實現(xiàn),然后可以使用配置文件的方式來實現(xiàn)替換或者擴展,在框架中比較常見,提高框架的可擴展性。

          JDK有內(nèi)置的SPI機制的實現(xiàn)ServiceLoader,Dubbo也有自己的SPI機制的實現(xiàn)ExtensionLoader,但是這里我們都不講。。

          e29d751d488312a7e52d798be927510e.webp

          但是,我之前寫過相關(guān)的文章,面試常問的dubbo的spi機制到底是什么?(上),文章的前半部分有對比三者的區(qū)別。

          這里我們著重講一下Spring的SPI機制的實現(xiàn)SpringFactoriesLoader。

          SpringFactoriesLoader

          Spring的SPI機制規(guī)定,配置文件必須在classpath路徑下的META-INF文件夾內(nèi),文件名必須為spring.factories,文件內(nèi)容為鍵值對,一個鍵可以有多個值,只需要用逗號分割就行,同時鍵值都需要是類的全限定名。但是鍵和值可以沒有任何關(guān)系,當然想有也可以有。

          show me the code

          這里我自定義一個類,MyEnableAutoConfiguration作為鍵,值就是User

                
                public?class?MyEnableAutoConfiguration?{
          }

          spring.factories文件

                
                com.sanyou.spring.extension.spi.MyEnableAutoConfiguration=com.sanyou.spring.extension.User

          然后放在META-INF底下

          fccad875c58580afa07de3811ff8102a.webp

          測試:

                
                public?class?Application?{

          ????public?static?void?main(String[]?args)?{
          ????????List<String>?classNames?=?SpringFactoriesLoader.loadFactoryNames(MyEnableAutoConfiguration.class,?MyEnableAutoConfiguration.class.getClassLoader());
          ????????classNames.forEach(System.out::println);
          ????}

          }

          結(jié)果:

                
                com.sanyou.spring.extension.User

          可以看出,通過SpringFactoriesLoader的確可以從spring.factories文件中拿到MyEnableAutoConfiguration鍵對應的值。

          到這你可能說會,這SPI機制也沒啥用啊。的確,我這個例子比較簡單,拿到就是遍歷,但是在Spring中,如果Spring在加載類的話使用SPI機制,那我們就可以擴展,接著往下看。

          SpringBoot啟動擴展點

          SpringBoot項目在啟動的過程中有很多擴展點,這里就來盤點一下幾個常見的擴展點。

          1、自動裝配

          說到SpringBoot的擴展點,第一時間肯定想到的就是自動裝配機制,面試賊喜歡問,但是其實就是一個很簡單的東西。當項目啟動的時候,會去從所有的spring.factories文件中讀取@EnableAutoConfiguration鍵對應的值,拿到配置類,然后根據(jù)一些條件判斷,決定哪些配置可以使用,哪些不能使用。

          spring.factories文件?鍵值?不錯,自動裝配說白了就是SPI機制的一種運用場景。

          @EnableAutoConfiguration注解:

                
                @Import(AutoConfigurationImportSelector.class)
          public?@interface?EnableAutoConfiguration?
          {
          ????//忽略
          }

          我擦,這個注解也是使用@Import注解,而且配置類還實現(xiàn)了ImportSelector接口,跟前面也都對上了。在SpringBoot中,@EnableAutoConfiguration是通過@SpringBootApplication來使用的。

          在AutoConfigurationImportSelector中還有這樣一段代碼5f5bd424de3c09f8c000aca0684a2e90.webp

          所以,這段代碼也明顯地可以看出,自動裝配也是基于SPI機制實現(xiàn)的。

          那么我想實現(xiàn)自動裝配怎么辦呢?很簡單,只需兩步。

          第一步,寫個配置類:

                
                @Configuration
          public?class?UserAutoConfiguration?{

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

          }

          這里我為了跟前面的知識有關(guān)聯(lián),配置了一個UserFactoryBean。

          第二步,往spring.factories文件配置一下

                
                org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
          com.sanyou.spring.extension.springbootextension.UserAutoConfiguration

          到這就已經(jīng)實現(xià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);
          ????}

          }

          運行結(jié)果:

                
                調(diào)用?UserFactoryBean?的?getObject?方法生成?Bean:com.sanyou.spring.extension.User@3406472c
          獲取到的Bean為com.sanyou.spring.extension.User@3406472c

          從運行結(jié)果可以看出,自動裝配起了作用,并且雖然往容器中注入的Bean的class類型為UserFactoryBean,但是最終會調(diào)用UserFactoryBean的getObject的實現(xiàn)獲取到User對象。

          自動裝配機制是SpringBoot的一個很重要的擴展點,很多框架在整合SpringBoot的時候,也都通過自動裝配來的,實現(xiàn)項目啟動,框架就自動啟動的,這里我舉個Mybatis整合SpringBoot。

          6fdeb840b3c8cccb9c665365f23cebfa.webpMybatis整合SpringBoot的spring.factories文件

          2、PropertySourceLoader

          PropertySourceLoader,這是干啥的呢?

          我們都知道,在SpringBoot環(huán)境下,外部化的配置文件支持properties和yaml兩種格式。但是,現(xiàn)在不想使用properties和yaml格式的文件,想使用json格式的配置文件,怎么辦?

          當然是基于該小節(jié)講的PropertySourceLoader來實現(xiàn)的。

                
                public?interface?PropertySourceLoader?{

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

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

          }

          對于PropertySourceLoader的實現(xiàn),SpringBoot兩個實現(xiàn)

          PropertiesPropertySourceLoader:可以解析properties或者xml結(jié)尾的配置文件364853065b1b9838b454f1bd08cf0a6a.webp

          YamlPropertySourceLoader:解析以yml或者yaml結(jié)尾的配置文件a83a956b59d908c3bd242cc3149b8635.webp

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

          動手來一個。

          實現(xiàn)可以讀取json格式的配置文件

          實現(xiàn)這個功能,只需要兩步就可以了。

          第一步:自定義一個PropertySourceLoader

          JsonPropertySourceLoader,實現(xiàn)PropertySourceLoader接口

                
                public?class?JsonPropertySourceLoader?implements?PropertySourceLoader?{

          ????@Override
          ????public?String[]?getFileExtensions()?{
          ????????//這個方法表明這個類支持解析以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

          JsonPropertySourceLoader 已經(jīng)有了,那么怎么用呢?當然是SPI機制了,SpringBoot對于配置文件的處理,就是依靠SPI機制,這也是能擴展的重要原因。

          1f1cff2ae8af411866a1afb3c3058b37.webpSPI機制加載PropertySourceLoader實現(xiàn)9d74ba4de18f15ac3a24ff1214ff3c88.webpspring.factories文件配置PropertySourceLoader

          SpringBoot會先通過SPI機制加載所有PropertySourceLoader,然后遍歷每個PropertySourceLoader,判斷當前遍歷的PropertySourceLoader,通過getFileExtensions獲取到當前PropertySourceLoader能夠支持哪些配置文件格式的解析,讓后跟當前需要解析的文件格式進行匹配,如果能匹配上,那么就會使用當前遍歷的PropertySourceLoader來解析配置文件。

          PropertySourceLoader其實就屬于策略接口,配置文件的解析就是策略模式的運用。

          所以,只需要按照這種格式,在spring.factories文件中配置一下就行了。

                
                org.springframework.boot.env.PropertySourceLoader=\
          com.sanyou.spring.extension.springbootextension.propertysourceloader.JsonPropertySourceLoader

          到此,其實就擴展完了,接下來就來測試一下。

          測試

          先創(chuàng)建一個application.json的配置文件

          cad7cd87980a39ace9aa913064d1c477.webpapplication.json配置文件

          改造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();
          ????}

          }

          運行結(jié)果:

                
                獲取到的Bean為com.sanyou.spring.extension.User@481ba2cf,屬性username值為:三友的java日記

          成功將json配置文件的屬性注入到User對象中。

          至此,SpringBoot就支持了以json為結(jié)尾的配置文件格式。

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

          如果你的項目正在用Nacos作為配置中心,那么剛剛好,Nacos已經(jīng)實現(xiàn)json配置文件格式的解析。

          9629f3339496cee515edfe27556fe7cb.webpNacos對于PropertySourceLoader的實現(xiàn)

          Nacos不僅實現(xiàn)了json格式的解析,也實現(xiàn)了關(guān)于xml格式的配置文件的解析,并且優(yōu)先級會比SpringBoot默認的xml格式文件解析的優(yōu)先級高。至于Nacos為啥需要實現(xiàn)PropertySourceLoader?其實很簡單,因為Nacos作為配置中心,不僅支持properties和yaml格式的文件,還支持json格式的配置文件,那么客戶端拿到這些配置就需要解析,SpringBoot已經(jīng)支持了properties和yaml格式的文件的解析,那么Nacos只需要實現(xiàn)SpringBoot不支持的就可以了。

          3、ApplicationContextInitializer

          ApplicationContextInitializer也是SpringBoot啟動過程的一個擴展點。

          f8794c68d232b215a7e23590a7283bf2.webpApplicationContextInitializer

          在SpringBoot啟動過程,會回調(diào)這個類的實現(xiàn)initialize方法,傳入ConfigurableApplicationContext。

          那怎么用呢?

          依然是SPI。

          c6b6a1f21f0ad46550bf243fa8b41df8.webpSPI加載ApplicationContextInitializer

          然后遍歷所有的實現(xiàn),依次調(diào)用

          8f7d2c5c17571752140232f0a74d135b.webp調(diào)用initialize

          這里就不演示了,實現(xiàn)接口,按照如下這種配置就行了

          5bb78cabfc94dc64a092da668a230d0a.webp

          但是這里需要注意的是,此時傳入的ConfigurableApplicationContext并沒有調(diào)用過refresh方法,也就是里面是沒有Bean對象的,一般這個接口是用來配置ConfigurableApplicationContext,而不是用來獲取Bean的。

          4、EnvironmentPostProcessor

          EnvironmentPostProcessor在SpringBoot啟動過程中,也會調(diào)用,也是通過SPI機制來加載擴展的。

          b5ef47776c79b2fc58ffb80e2eb8e57f.webpEnvironmentPostProcessor

          EnvironmentPostProcessor是用來處理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在這個對象的。

          說這個類的主要原因,主要不是說擴展,而是他的一個實現(xiàn)類很關(guān)鍵。

          ff32e8fe67a6297e80073f2f63e7ed76.webpConfigFileApplicationListener

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

          5、ApplicationRunner和CommandLineRunner

          ApplicationRunner和CommandLineRunner都是在SpringBoot成功啟動之后會調(diào)用,可以拿到啟動時的參數(shù)。

          那怎么擴展呢?

          當然又是SPI了。

          d7494f90d67747df4f2599a7c1f8ddb8.webp

          這兩個其實不是通過SPI機制來擴展,而是直接從容器中獲取的,這又是為啥呢?

          因為調(diào)用ApplicationRunner和CommandLineRunner時,SpringBoot已經(jīng)啟動成功了,Spring容器都準備好了,需要什么Bean直接從容器中查找多方便。

          而前面說的幾個需要SPI機制的擴展點,是因為在SpringBoot啟動的時候,Spring容器還沒有啟動好,也就是無法從Spring容器獲取到這些擴展的對象,為了兼顧擴展性,所以就通過SPI機制來實現(xiàn)獲取到實現(xiàn)類。

          055ce5de63dc844902b459a76279b864.webp刷新上下文和調(diào)用Runnerba9ceb6910e38ce94dd5b0a916add699.webp加載和調(diào)用Runner

          所以要想擴展這個點,只需要實現(xiàn)接口,添加到Spring容器就可以了。

          Spring Event 事件

          Event 事件可以說是一種觀察者模式的實現(xiàn),主要是用來解耦合的。當發(fā)生了某件事,只要發(fā)布一個事件,對這個事件的監(jiān)聽者(觀察者)就可以對事件進行響應或者處理。

          舉個例子來說,假設發(fā)生了火災,可能需要打119、救人,那么就可以基于事件的模型來實現(xiàn),只需要打119、救人監(jiān)聽火災的發(fā)生就行了,當發(fā)生了火災,通知這些打119、救人去觸發(fā)相應的邏輯操作。

          ca149c05da01a18b67cc43250668a9d9.webp

          什么是Spring Event 事件

          那么是什么是Spring Event 事件,就是Spring實現(xiàn)了這種事件模型,你只需要基于Spring提供的API進行擴展,就可以完成事件的發(fā)布訂閱

          Spring提供的事件api:

          ApplicationEvent

          572361b3e697d66eb84d486ab3f7122e.webpApplicationEvent

          事件的父類,所有具體的事件都得繼承這個類,構(gòu)造方法的參數(shù)是這個事件攜帶的參數(shù),監(jiān)聽器就可以通過這個參數(shù)來進行一些業(yè)務操作。

          ApplicationListener

          32e9d9ddc4b7fd9a0e5e1224c983e766.webpApplicationListener

          事件監(jiān)聽的接口,泛型是子類需要監(jiān)聽的事件類型,子類需要實現(xiàn)onApplicationEvent,參數(shù)就是事件類型,onApplicationEvent方法的實現(xiàn)就代表了對事件的處理,當事件發(fā)生時,Spring會回調(diào)onApplicationEvent方法的實現(xiàn),傳入發(fā)布的事件。

          ApplicationEventPublisher

          bfe5051eee5348b1a44eb2be5c2642d1.webpApplicationEventPublisher

          事件發(fā)布器,通過publishEvent方法就可以發(fā)布一個事件,然后就可以觸發(fā)監(jiān)聽這個事件的監(jiān)聽器的回調(diào)。

          ApplicationContext實現(xiàn)了ApplicationEventPublisher接口,所以通過ApplicationContext就可以發(fā)布事件。

          a8fda5645baead12bb0a58f188cca48a.webp

          那怎么才能拿到ApplicationContext呢?

          前面Bean生命周期那節(jié)說過,可以通過ApplicationContextAware接口拿到,甚至你可以通過實現(xiàn)ApplicationEventPublisherAware直接獲取到ApplicationEventPublisher,其實獲取到的ApplicationEventPublisher也就是ApplicationContext,因為是ApplicationContext實現(xiàn)了ApplicationEventPublisher。

          話不多說,上代碼

          就以上面的火災為例

          第一步:創(chuàng)建一個火災事件類

          火災事件類繼承ApplicationEvent

                
                //?火災事件
          public?class?FireEvent?extends?ApplicationEvent?{

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

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

          打119的火災事件的監(jiān)聽器:

                
                public?class?Call119FireEventListener?implements?ApplicationListener<FireEvent>?{

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

          }

          救人的火災事件的監(jiān)聽器:

                
                public?class?SavePersonFireEventListener?implements?ApplicationListener<FireEvent>?{

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

          }

          事件和對應的監(jiā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("著火了"));
          ????}

          }

          將兩個事件注冊到Spring容器中,然后發(fā)布FireEvent事件

          運行結(jié)果:

                
                119
          救人

          控制臺打印出了結(jié)果,觸發(fā)了監(jiān)聽。

          如果現(xiàn)在需要對火災進行救火,那么只需要去監(jiān)聽FireEvent,實現(xiàn)救火的邏輯,注入到Spring容器中,就可以了,其余的代碼根本不用動。

          Spring內(nèi)置的事件

          Spring內(nèi)置的事件很多,這里我羅列幾個

          事件類型 觸發(fā)時機
          ContextRefreshedEvent 在調(diào)用ConfigurableApplicationContext 接口中的refresh()方法時觸發(fā)
          ContextStartedEvent 在調(diào)用ConfigurableApplicationContext的start()方法時觸發(fā)
          ContextStoppedEvent 在調(diào)用ConfigurableApplicationContext的stop()方法時觸發(fā)
          ContextClosedEvent 當ApplicationContext被關(guān)閉時觸發(fā)該事件,也就是調(diào)用close()方法觸發(fā)

          在Spring容器啟動的過程中,Spring會發(fā)布這些事件,如果你需要這Spring容器啟動的某個時刻進行什么操作,只需要監(jiān)聽對應的事件即可。

          Spring事件的傳播

          Spring事件的傳播是什么意思呢?

          我們都知道,在Spring中有子父容器的概念,而Spring事件的傳播就是指當通過子容器發(fā)布一個事件之后,不僅可以觸發(fā)在這個子容器的事件監(jiān)聽器,還可以觸發(fā)在父容器的這個事件的監(jiān)聽器。

          上代碼

                
                public?class?EventPropagateApplication?{

          ????public?static?void?main(String[]?args)?{

          ????????//?創(chuàng)建一個父容器
          ????????AnnotationConfigApplicationContext?parentApplicationContext?=?new?AnnotationConfigApplicationContext();
          ????????//將?打119監(jiān)聽器?注冊到父容器中
          ????????parentApplicationContext.register(Call119FireEventListener.class);
          ????????parentApplicationContext.refresh();

          ????????//?創(chuàng)建一個子容器
          ????????AnnotationConfigApplicationContext?childApplicationContext?=?new?AnnotationConfigApplicationContext();
          ????????//將?救人監(jiān)聽器?注冊到子容器中
          ????????childApplicationContext.register(SavePersonFireEventListener.class);
          ????????childApplicationContext.refresh();

          ????????//?設置一下父容器
          ????????childApplicationContext.setParent(parentApplicationContext);

          ????????//?通過子容器發(fā)布著火的事件,觸發(fā)監(jiān)聽
          ????????childApplicationContext.publishEvent(new?FireEvent("著火了"));

          ????}

          }

          創(chuàng)建了兩個容器,父容器注冊了打119的監(jiān)聽器,子容器注冊了救人的監(jiān)聽器,然后將子父容器通過setParent關(guān)聯(lián)起來,最后通過子容器,發(fā)布了著火的事件。

          運行結(jié)果:

                
                救人
          119

          從打印的日志,的確可以看出,雖然是子容器發(fā)布了著火的事件,但是父容器的監(jiān)聽器也成功監(jiān)聽了著火事件。

          源碼驗證

          b743d3b2d00444ca7292e2a952ba3afe.webp事件傳播源碼

          從這段源碼可以看出,如果父容器不為空,就會通過父容器再發(fā)布一次事件。

          傳播特性的一個坑

          前面說過,在Spring容器啟動的過程,會發(fā)布很多事件,如果你需要有相應的擴展,可以監(jiān)聽這些事件。但是,在SpringCloud環(huán)境下,你的這些Spring發(fā)布的事件的監(jiān)聽器可能會執(zhí)行很多次。為什么會執(zhí)行很多次呢?其實就是跟傳播特性有關(guān)。

          在SpringCloud的環(huán)境下,為了使像FeignClient和RibbonClient這些不同的服務的配置相互隔離,會創(chuàng)建很多的子容器,而這些子容器都有一個公共的父容器,那就是SpringBoot項目啟動時創(chuàng)建的容器,事件的監(jiān)聽器都在這個容器中。而這些為了配置隔離創(chuàng)建的子容器,在容器啟動的過程中,也會發(fā)布諸如ContextRefreshedEvent等這樣的事件,如果你監(jiān)聽了這些事件,那么由于傳播特性的關(guān)系,你的這個事件的監(jiān)聽器就會觸發(fā)多次。

          如何解決這個坑呢?

          你可以進行判斷這些監(jiān)聽器有沒有執(zhí)行過,比如加一個判斷的標志;或者是監(jiān)聽類似的事件,比如ApplicationStartedEvent事件,這種事件是在SpringBoot啟動中發(fā)布的事件,而子容器不是SpringBoot,所以不會多次發(fā)這種事件,也就會只執(zhí)行一次。

          Spring事件的運用舉例

          1、在Mybatis中的使用

          又來以Mybatis舉例了。。Mybatis的SqlSessionFactoryBean監(jiān)聽了ApplicationEvent,然后判斷如果是ContextRefreshedEvent就進行相應的處理,這個類還實現(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();
          ????????}
          ????}
          ????
          }

          說實話,這監(jiān)聽代碼寫的不太好,監(jiān)聽了ApplicationEvent,那么所有的事件都會回調(diào)這個類的onApplicationEvent方法,但是onApplicationEvent方法實現(xiàn)又是當ApplicationEvent是ContextRefreshedEvent類型才會往下走,那為什么不直接監(jiān)聽ContextRefreshedEvent呢?

          可以給個差評。af1c4c1822b5af3cc9383435ad1794ca.webp

          膨脹了膨脹了。。

          2、在SpringCloud的運用

          在SpringCloud的中,當項目啟動的時候,會自動往注冊中心進行注冊,那么是如何實現(xiàn)的呢?當然也是基于事件來的。當web服務器啟動完成之后,就發(fā)布ServletWebServerInitializedEvent事件。

          06116b5f673e7979e3ee12ba8e8041a8.webp

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

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

          04b82fcf4a95dfd1dfd4bb61e35b16cc.webpAbstractAutoServiceRegistration

          一般不同的注冊中心都會去繼承這個類,監(jiān)聽項目啟動,實現(xiàn)往注冊中心服務端進行注冊。

          399fc6f69eba7c3b6520df59e5aa7935.webpNacos對于AbstractAutoServiceRegistration的繼承
          Spring Event事件在Spring內(nèi)部中運用很多,是解耦合的利器。在實際項目中,你既可以監(jiān)聽Spring/Boot內(nèi)置的一些事件,進行相應的擴展,也可以基于這套模型在業(yè)務中自定義事件和相應的監(jiān)聽器,減少業(yè)務代碼的耦合。

          命名空間

          最后來講一個可能沒有留意,但是很神奇的擴展點--命名空間。起初我知道這個擴展點的時候,我都驚呆了,這玩意也能擴展?真的不得不佩服Spring設計的可擴展性。

          e31f5e053b783a242391170d8868cb66.webp

          回憶一下啥是命名空間?

          先看一段配置

                
                <?xml?version="1.0"?encoding="UTF-8"?>
          <beans?xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          ???????xmlns="http://www.springframework.org/schema/beans"
          ???????xmlns:context="http://www.springframework.org/schema/context"
          ???????xsi:schemaLocation="
          ???????http://www.springframework.org/schema/beans
          ???????http://www.springframework.org/schema/beans/spring-beans.xsd
          ???????http://www.springframework.org/schema/context?
          ???????http://www.springframework.org/schema/beans/spring-context.xsd
          ???????"
          >


          ????<context:component-scan?base-package="com.sanyou.spring.extension"/>

          </beans>

          這一段xml配置想必都很熟悉,其中, context 標簽就代表了一個命名空間。

          也就說,這個標簽是可以擴展的。

          話不多說,來個擴展

          接下來自定義命名空間 sanyou,總共分為3步。

          第一步:定義一個xsd文件

          如下:

                
                <?xml?version="1.0"?encoding="UTF-8"?standalone="no"?>
          <!--?xmlns?和?targetNamespace?需要定義,結(jié)尾為sanyou,前面都一樣的-->
          <xsd:schema?xmlns="http://sanyou.com/schema/sanyou"
          ????????????xmlns:xsd="http://www.w3.org/2001/XMLSchema"
          ????????????targetNamespace="http://sanyou.com/schema/sanyou">


          ????<xsd:import?namespace="http://www.w3.org/XML/1998/namespace"/>

          ????<xsd:complexType?name="Bean">
          ????????<xsd:attribute?name="class"?type="xsd:string"?use="required"/>
          ????</xsd:complexType>

          ????<!--??sanyou?便簽的子標簽,類型是Bean?,就會找到上面的complexType=Bean類型,然后處理屬性??-->
          ????<xsd:element?name="mybean"?type="Bean"/>
          </xsd:schema>

          這個xsd文件來指明sanyou這個命名空間下有哪些標簽和屬性。這里我只指定了一個標簽 mybean,mybean標簽里面有個class的屬性,然后這個標簽的目的就是將class屬性指定的Bean的類型,注入到Spring容器中,作用跟spring的標簽的作用是一樣的。

          xsd文件沒有需要放的固定的位置,這里我放到 META-INF 目錄下

          第二步:解析這個命名空間

          解析命名空間很簡單,Spring都有配套的東西--NamespaceHandler接口,只要實現(xiàn)這個接口就行了。但一般我們不直接實現(xiàn) NamespaceHandler 接口,我們可以繼承 NamespaceHandlerSupport 類,這個類實現(xiàn)了 NamespaceHandler 接口。

                
                public?class?SanYouNameSpaceHandler?extends?NamespaceHandlerSupport?{

          ????@Override
          ????public?void?init()?{
          ????????//注冊解析?mybean?標簽的解析器
          ????????registerBeanDefinitionParser("mybean",?new?SanYouBeanDefinitionParser());
          ????}

          ????private?static?class?SanYouBeanDefinitionParser?extends?AbstractSingleBeanDefinitionParser?{
          ????????@Override
          ????????protected?boolean?shouldGenerateId()?{
          ????????????return?true;
          ????????}

          ????????@Override
          ????????protected?String?getBeanClassName(Element?element)?{
          ????????????return?element.getAttribute("class");
          ????????}

          ????}
          }

          SanYouNameSpaceHandler的作用就是將sanyou命名空間中的mybean這個標簽讀出來,拿到class的屬性,然后將這個class屬性指定的class類型注入到Spring容器中,至于注冊這個環(huán)節(jié)的代碼,都交給了SanYouBeanDefinitionParser的父類來做了。

          第三步:創(chuàng)建并配置spring.handlers和spring.schemas文件

          先創(chuàng)建spring.handlers和spring.schemas文件

          spring.handlers文件內(nèi)容

                
                http\://sanyou.com/schema/sanyou=com.sanyou.spring.extension.namespace.SanYouNameSpaceHandler
              

          通過spring.handlers配置文件,就知道sanyou命名空間應該找SanYouNameSpaceHandler進行解析

          spring.schemas文內(nèi)容

                
                http\://sanyou.com/schema/sanyou.xsd=META-INF/sanyou.xsd
              

          spring.schemas配置xsd文件的路徑

          文件都有了,只需要放到classpath下的META-INF文件夾就行了。

          469c7075249f91a088268e33e628a487.webpxsd、spring.handlers、spring.schema文件

          到這里,就完成了擴展,接下來進行測試

          測試

          先構(gòu)建一個applicationContext.xml文件,放到resources目錄下

                
                <?xml?version="1.0"?encoding="UTF-8"?>
          <beans?xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          ???????xmlns="http://www.springframework.org/schema/beans"
          ???????xmlns:sanyou="http://sanyou.com/schema/sanyou"
          ???????xsi:schemaLocation="
          ???????http://www.springframework.org/schema/beans
          ???????http://www.springframework.org/schema/beans/spring-beans.xsd
          ???????http://sanyou.com/schema/sanyou
          ???????http://sanyou.com/schema/sanyou.xsd
          ???????"
          >


          ????<!--使用?sanyou?標簽,配置一個?User?Bean-->
          ????<sanyou:mybean?class="com.sanyou.spring.extension.User"/>

          </beans>

          再寫個測試類

                
                public?class?Application?{

          ????public?static?void?main(String[]?args)?{

          ????????ClassPathXmlApplicationContext?applicationContext?=?new?ClassPathXmlApplicationContext("applicationContext.xml");
          ????????applicationContext.refresh();

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

          ????????System.out.println(user);
          ????}

          }

          運行結(jié)果:

                
                com.sanyou.spring.extension.User@27fe3806

          成功獲取到User這個對象,說明自定義標簽生效了。

          Spring內(nèi)置命名空間的擴展

          18776c207524970383ef0daacbf481d5.webpNameSpaceHandler的spring實現(xiàn)

          通過NameSpaceHandler接口的這些實現(xiàn)類的命名就可以看出來有哪些擴展和這些擴展的作用,比如有處理aop的,有處理mvc的等等之類的。

          開源框架對命名空間的擴展

          1、Mybatis的擴展

          e5452a682f461da8c12463985773b7c2.webpMybatis的NameSpaceHandler實現(xiàn)

          這個就是來掃描指定路徑的mapper接口的,處理 scan 標簽,跟@MapperScan注解的作用是一樣的。

          2、dubbo的擴展

          使用dubbo可能寫過如下的配置

                
                
                  <dubbo:registry?address="zookeeper://192.168.10.119:2181"?/>
                  

          這個dubbo命名空間肯定就是擴展的Spring的,也有對應的dubbo實現(xiàn)的NameSpaceHandler。

          400639b3857e4a6c45b40717ee34b355.webpDubboNamespaceHandler

          不得不說,dubbo解析的標簽可真的多啊,不過功能也是真的多。

          總結(jié)

          到這,本文就接近尾聲了,這里畫兩張圖來總結(jié)一下本文講了Spring的哪些擴展點。

          c7040cebc53643d29ce79a9e01381aad.webp

          整體0934bef2a884fb8884c8a8d6baa0d78c.webpSpringBoot啟動擴展點

          通過學習Spring的這些擴展點,既可以幫助我們應對日常的開發(fā),還可以幫助我們更好地看懂Spring的源碼。

          最后,本文前前后后花了一周多的時間完成,如果對你有點幫助,還請幫忙點贊、在看、轉(zhuǎn)發(fā)、非常感謝。

          哦,差點忘了,本文所有demo代碼都在這了

          https://github.com/sanyou3/spring-extension.git

          ?? 專屬專欄/一對一提問/簡歷修改/學習打卡/讀書活動,歡迎加入? JavaGuide 知識星球 (文末可領(lǐng)取優(yōu)惠券)。

          推薦閱讀?:

          ?? 如果本文對你有幫助的話,歡迎?點贊&在看&分享?,這對我繼續(xù)分享&創(chuàng)作優(yōu)質(zhì)文章非常重要。非常感謝!

          瀏覽 75
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  A V免费视频 | 九草福利视频 | 操屄激情| 精品国产一| 男女拍拍拍拍免费视频 |