<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(春天)

          共 48900字,需瀏覽 98分鐘

           ·

          2024-07-25 16:28

          大家好,我是3y。

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

          話不多說,直接進(jìn)入正題。

          FactoryBean

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

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

          來個(gè)FactoryBean的Demo

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

          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() {
                  // 這個(gè) FactoryBean 返回的Bean的類型
                  return User.class;
              }

          }

          測(cè)試類:

          public class Application {

              public static void main(String[] args) {
                  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
                  //將 UserFactoryBean 注冊(cè)到容器中
                  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é)果可以看出,明明注冊(cè)到Spring容器的是UserFactoryBean,但是卻能從容器中獲取到User類型的Bean,User這個(gè)Bean就是通過UserFactoryBean的getObject方法返回的。

          FactoryBean在開源框架中的使用

          1、 在Mybatis中的使用

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

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

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

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

          而@MapperScan注解的作用就是將每個(gè)接口對(duì)應(yīng)的MapperFactoryBean注冊(cè)到Spring容器的。

          2、在OpenFeign中的使用

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

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

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

          @EnableFeignClients注解的作用就是將每個(gè)接口對(duì)應(yīng)的FeignClientFactoryBean注入到Spring容器的。

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

          @Import注解

          @Import注解在項(xiàng)目中可能不常見,但是下面這兩個(gè)注解肯定常見。

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

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

          @Import注解導(dǎo)入的配置類的分類

          @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),獲取一批類的全限定名,最終這些類就會(huì)被注冊(cè)到Spring容器中。

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

          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"};
              }

          }

          測(cè)試:

          // @Import 注解導(dǎo)入 UserImportSelector
          @Import(UserImportSelector.class)
          public class Application 
          {

              public static void main(String[] args) {
                  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
                  //將 Application 注冊(cè)到容器中
                  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 接口,你就可以自定義往容器中注冊(cè)想注入的Bean。這個(gè)接口相比與 ImportSelector 接口的主要區(qū)別就是,ImportSelector接口是返回一個(gè)類,你不能對(duì)這個(gè)類進(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的值為三友的java日記
                          .addPropertyValue("username", "三友的java日記")
                          .getBeanDefinition()
          ;

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

          }

          測(cè)試:

          // 導(dǎo)入 UserImportBeanDefinitionRegistrar
          @Import(UserImportBeanDefinitionRegistrar.class)
          public class Application 
          {

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

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

          }

          結(jié)果:

          往Spring容器中注入U(xiǎn)ser
          獲取到的Bean為com.sanyou.spring.extension.User@6385cb26,屬性u(píng)sername值為:三友的java日記

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

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

          總結(jié)

          @Import注解作用示意圖

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

          這種方式有什么好處呢?

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

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

          Bean的生命周期

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

          Bean注冊(cè)示意圖

          為什么需要去注冊(cè)Bean?

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

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

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

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

          先來測(cè)試

          創(chuàng)建LifeCycle類

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

          public class LifeCycle implements InitializingBeanApplicationContextAwareDisposableBean {

              @Autowired
              private User user;

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

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

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

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

              /**
               * 實(shí)現(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)用了");
              }

              /**
               * 實(shí)現(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();
          }
          測(cè)試
          public class Application {

              public static void main(String[] args) {
                  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
                  //將 LifeCycle 注冊(cè)到容器中
                  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對(duì)象被創(chuàng)建了
          Aware接口起作用,setApplicationContext被調(diào)用了,此時(shí)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é)果

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

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

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

          回調(diào)順序

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

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

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

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

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

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

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

          BeanPostProcessor

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

          BeanPostProcessor是Bean在創(chuàng)建過程中一個(gè)非常重要的擴(kuò)展點(diǎn),因?yàn)槊總€(gè)Bean在創(chuàng)建的各個(gè)階段,都會(huì)回調(diào)BeanPostProcessor及其子接口的方法,傳入正在創(chuàng)建的Bean對(duì)象,這樣如果想對(duì)Bean創(chuàng)建過程中某個(gè)階段進(jìn)行自定義擴(kuò)展,那么就可以自定義BeanPostProcessor來完成。

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

          來個(gè)Demo

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

          那么就可以這么寫:

          public class UserBeanPostProcessor implements BeanPostProcessor {

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

                  return bean;
              }

          }

          測(cè)試:

          public class Application {

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

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

          }

          測(cè)試結(jié)果:

          獲取到的Bean為com.sanyou.spring.extension.User@21a947fe,屬性u(píng)sername值為:三友的java日記

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

          Spring內(nèi)置的BeanPostProcessor

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

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

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

          BeanPostProcessor在Dubbo中的使用

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

          public class ReferenceAnnotationBeanPostProcessor 
                 extends AbstractAnnotationBeanPostProcessor 
                 implements ApplicationContextAwareBeanFactoryPostProcessor 
          {
                  // 忽略
          }

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

          BeanPostProcessor在Spring Bean的擴(kuò)展中扮演著重要的角色,是Spring Bean生命周期中很重要的一部分。正是因?yàn)橛辛薆eanPostProcessor,你就可以在Bean創(chuàng)建過程中的任意一個(gè)階段擴(kuò)展自己想要的東西。

          BeanFactoryPostProcessor

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

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

          public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

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

          }

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

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

          Spring SPI機(jī)制

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

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

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

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

          SpringFactoriesLoader

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

          show me the code

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

          public class MyEnableAutoConfiguration {
          }

          spring.factories文件

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

          然后放在META-INF底下

          測(cè)試:

          public class Application {

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

          }

          結(jié)果:

          com.sanyou.spring.extension.User

          可以看出,通過SpringFactoriesLoader的確可以從spring.factories文件中拿到MyEnableAutoConfiguration鍵對(duì)應(yīng)的值。

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

          SpringBoot啟動(dòng)擴(kuò)展點(diǎn)

          SpringBoot項(xiàng)目在啟動(dòng)的過程中有很多擴(kuò)展點(diǎn),這里就來盤點(diǎn)一下幾個(gè)常見的擴(kuò)展點(diǎn)。

          1、自動(dòng)裝配

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

          spring.factories文件?鍵值?不錯(cuò),自動(dòng)裝配說白了就是SPI機(jī)制的一種運(yùn)用場(chǎng)景。

          @EnableAutoConfiguration注解:

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

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

          在AutoConfigurationImportSelector中還有這樣一段代碼

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

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

          第一步,寫個(gè)配置類:

          @Configuration
          public class UserAutoConfiguration {

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

          }

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

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

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

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

          接下來進(jìn)行測(cè)試:

          @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對(duì)象。

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

          Mybatis整合SpringBoot的spring.factories文件

          2、PropertySourceLoader

          PropertySourceLoader,這是干啥的呢?

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

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

          public interface PropertySourceLoader {

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

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

          }

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

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

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

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

          動(dòng)手來一個(gè)。

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

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

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

          JsonPropertySourceLoader,實(shí)現(xiàn)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 的鍵值對(duì)讀出來,放入到 map 中
                  for (String key : jsonObject.keySet()) {
                      map.put(key, jsonObject.getString(key));
                  }

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

          }
          第二步:配置PropertySourceLoader

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

          SPI機(jī)制加載PropertySourceLoader實(shí)現(xiàn)
          spring.factories文件配置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)用。

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

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

          到此,其實(shí)就擴(kuò)展完了,接下來就來測(cè)試一下。

          測(cè)試

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

          application.json配置文件

          改造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 + ",屬性u(píng)sername值為:" + user.getUsername());
              }


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

          }

          運(yùn)行結(jié)果:

          獲取到的Bean為com.sanyou.spring.extension.User@481ba2cf,屬性u(píng)sername值為:三友的java日記

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

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

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

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

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

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

          3、ApplicationContextInitializer

          ApplicationContextInitializer也是SpringBoot啟動(dòng)過程的一個(gè)擴(kuò)展點(diǎn)。

          ApplicationContextInitializer

          在SpringBoot啟動(dòng)過程,會(huì)回調(diào)這個(gè)類的實(shí)現(xiàn)initialize方法,傳入ConfigurableApplicationContext。

          那怎么用呢?

          依然是SPI。

          SPI加載ApplicationContextInitializer

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

          調(diào)用initialize

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

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

          4、EnvironmentPostProcessor

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

          EnvironmentPostProcessor

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

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

          ConfigFileApplicationListener

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

          5、ApplicationRunner和CommandLineRunner

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

          那怎么擴(kuò)展呢?

          當(dāng)然又是SPI了。

          這兩個(gè)其實(shí)不是通過SPI機(jī)制來擴(kuò)展,而是直接從容器中獲取的,這又是為啥呢?

          因?yàn)檎{(diào)用ApplicationRunner和CommandLineRunner時(shí),SpringBoot已經(jīng)啟動(dòng)成功了,Spring容器都準(zhǔn)備好了,需要什么Bean直接從容器中查找多方便。

          而前面說的幾個(gè)需要SPI機(jī)制的擴(kuò)展點(diǎn),是因?yàn)樵赟pringBoot啟動(dòng)的時(shí)候,Spring容器還沒有啟動(dòng)好,也就是無法從Spring容器獲取到這些擴(kuò)展的對(duì)象,為了兼顧擴(kuò)展性,所以就通過SPI機(jī)制來實(shí)現(xiàn)獲取到實(shí)現(xiàn)類。

          刷新上下文和調(diào)用Runner
          加載和調(diào)用Runner

          所以要想擴(kuò)展這個(gè)點(diǎn),只需要實(shí)現(xiàn)接口,添加到Spring容器就可以了。

          Spring Event 事件

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

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

          什么是Spring Event 事件

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

          Spring提供的事件api:

          ApplicationEvent

          ApplicationEvent

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

          ApplicationListener

          ApplicationListener

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

          ApplicationEventPublisher

          ApplicationEventPublisher

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

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

          那怎么才能拿到ApplicationContext呢?

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

          話不多說,上代碼

          就以上面的火災(zāi)為例

          第一步:創(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("救人");
              }

          }

          事件和對(duì)應(yīng)的監(jiān)聽都有了,接下來進(jìn)行測(cè)試:

          public class Application {

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

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

          }

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

          運(yùn)行結(jié)果:

          119
          救人

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

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

          Spring內(nèi)置的事件

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

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

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

          Spring事件的傳播

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

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

          上代碼

          public class EventPropagateApplication {

              public static void main(String[] args) {

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

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

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

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

              }

          }

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

          運(yùn)行結(jié)果:

          救人
          119

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

          源碼驗(yàn)證

          事件傳播源碼

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

          傳播特性的一個(gè)坑

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

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

          如何解決這個(gè)坑呢?

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

          Spring事件的運(yùn)用舉例

          1、在Mybatis中的使用

          又來以Mybatis舉例了。。Mybatis的SqlSessionFactoryBean監(jiān)聽了ApplicationEvent,然后判斷如果是ContextRefreshedEvent就進(jìn)行相應(yīng)的處理,這個(gè)類還實(shí)現(xiàn)了FactoryBean接口。。

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

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

          可以給個(gè)差評(píng)。

          膨脹了膨脹了。。

          2、在SpringCloud的運(yùn)用

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

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

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

          AbstractAutoServiceRegistration

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

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

          命名空間

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

          回憶一下啥是命名空間?

          先看一段配置

          <?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 標(biāo)簽就代表了一個(gè)命名空間。

          也就說,這個(gè)標(biāo)簽是可以擴(kuò)展的。

          話不多說,來個(gè)擴(kuò)展

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

          第一步:定義一個(gè)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 便簽的子標(biāo)簽,類型是Bean ,就會(huì)找到上面的complexType=Bean類型,然后處理屬性  -->
              <xsd:element name="mybean" type="Bean"/>
          </xsd:schema>

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

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

          第二步:解析這個(gè)命名空間

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

          public class SanYouNameSpaceHandler extends NamespaceHandlerSupport {

              @Override
              public void init() {
                  //注冊(cè)解析 mybean 標(biāo)簽的解析器
                  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這個(gè)標(biāo)簽讀出來,拿到class的屬性,然后將這個(gè)class屬性指定的class類型注入到Spring容器中,至于注冊(cè)這個(gè)環(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命名空間應(yīng)該找SanYouNameSpaceHandler進(jìn)行解析

          spring.schemas文內(nèi)容

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

          spring.schemas配置xsd文件的路徑

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

          xsd、spring.handlers、spring.schema文件

          到這里,就完成了擴(kuò)展,接下來進(jìn)行測(cè)試

          測(cè)試

          先構(gòu)建一個(gè)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 標(biāo)簽,配置一個(gè) User Bean-->
              <sanyou:mybean class="com.sanyou.spring.extension.User"/>

          </beans>

          再寫個(gè)測(cè)試類

          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);
              }

          }

          運(yùn)行結(jié)果:

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

          成功獲取到User這個(gè)對(duì)象,說明自定義標(biāo)簽生效了。

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

          NameSpaceHandler的spring實(shí)現(xiàn)

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

          開源框架對(duì)命名空間的擴(kuò)展

          1、Mybatis的擴(kuò)展

          Mybatis的NameSpaceHandler實(shí)現(xiàn)

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

          2、dubbo的擴(kuò)展

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

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

          這個(gè)dubbo命名空間肯定就是擴(kuò)展的Spring的,也有對(duì)應(yīng)的dubbo實(shí)現(xiàn)的NameSpaceHandler。

          DubboNamespaceHandler

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

          總結(jié)

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

          整體
          SpringBoot啟動(dòng)擴(kuò)展點(diǎn)

          通過學(xué)習(xí)Spring的這些擴(kuò)展點(diǎn),既可以幫助我們應(yīng)對(duì)日常的開發(fā),還可以幫助我們更好地看懂Spring的源碼。

          END


          Java項(xiàng)目訓(xùn)練營(yíng)

          我開通了項(xiàng)目股東服務(wù),已經(jīng)有不少消息推送平臺(tái)項(xiàng)目股東拿了阿里/vivo等大廠offer了。我是沒找到網(wǎng)上有跟我提供相同的服務(wù),價(jià)格還比我低的

          ??一對(duì)一周到的服務(wù):有很多人的自學(xué)能力和基礎(chǔ)確實(shí)不太行,不知道怎么開始學(xué)習(xí),從哪開始看起,學(xué)習(xí)項(xiàng)目的過程中會(huì)走很多彎路,很容易就迷茫了。付費(fèi)最跟自學(xué)最主要的區(qū)別就是我的服務(wù)會(huì)更周到。我會(huì)告訴你怎么開始學(xué)這個(gè)開源項(xiàng)目,哪些是重點(diǎn)需要掌握的,如何利用最短的時(shí)間把握整個(gè)系統(tǒng)架構(gòu)和編碼的設(shè)計(jì),把時(shí)間節(jié)省下來去做其他事情。學(xué)習(xí)經(jīng)驗(yàn)/路線/簡(jiǎn)歷編寫/面試經(jīng)驗(yàn)知無不言

          ??本地直連遠(yuǎn)程服務(wù):生產(chǎn)環(huán)境的應(yīng)用系統(tǒng)肯定會(huì)依賴各種中間件,我專門買了兩臺(tái)服務(wù)器已經(jīng)搭建好必要的環(huán)境??,在本地就可以直接啟動(dòng)運(yùn)行體驗(yàn)和學(xué)習(xí),無須花額外的時(shí)間自行搭建調(diào)試。

          ??細(xì)致的文檔&視頻:巨細(xì)致的語雀文檔13W+ 字,共121個(gè)文檔,項(xiàng)目視頻還在持續(xù)制作更新中(24個(gè)),不怕你學(xué)不會(huì)。

          ??付費(fèi)社群優(yōu)質(zhì)的社群里需篩選過濾,學(xué)習(xí)氛圍是很重要的,多跟同輩或前輩聊聊,會(huì)少走很多彎路??

          ??股東專屬倉(cāng)庫(kù):有干練清爽的commit,一步一步從零復(fù)現(xiàn)austin,每個(gè)commit都跟隨著視頻。另外,有SpringCloud Alibaba版本的倉(cāng)庫(kù),開啟微服務(wù)的austin

          如果想獲取上面的權(quán)益,可以看看??Java項(xiàng)目訓(xùn)練營(yíng)

          瀏覽 314
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  麻豆AV无码精品一区二区色欲 | 99九九99九九九99九他書對 | 天堂av免费在线观看 | av片在线观看 | 懂色AV一区 |