1. 好記性不如爛筆頭:Spring(春天)

        共 48900字,需瀏覽 98分鐘

         ·

        2024-07-25 16:28

        大家好,我是3y。

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

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

        FactoryBean

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

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

        來個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<Textends 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注解的作用就是將每個接口對應(yīng)的MapperFactoryBean注冊到Spring容器的。

        2、在OpenFeign中的使用

        FeignClient接口的動態(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方法來返回的動態(tài)代理。

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

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

        @Import注解

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

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

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

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

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

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

        public interface ImportSelector {

           String[] selectImports(AnnotationMetadata importingClassMetadata);

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

        }

        當(dāng)配置類實現(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 注解導(dǎo)入 UserImportSelector
        @Import(UserImportSelector.class)
        public class Application 
        {

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

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

        }

        結(jié)果:

        調(diào)用 UserImportSelector 的 selectImports 方法獲取一批類限定名
        獲取到的Bean為com.sanyou.spring.extension.User@282003e1

        所以可以看出,的確成功往容器中注入了User這個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) {
           }

        }

        當(dāng)配置類實現(xiàn)了 ImportBeanDefinitionRegistrar 接口,你就可以自定義往容器中注冊想注入的Bean。這個接口相比與 ImportSelector 接口的主要區(qū)別就是,ImportSelector接口是返回一個類,你不能對這個類進(jìn)行任何操作,但是 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)
                        // 設(shè)置 User 這個Bean的屬性username的值為三友的java日記
                        .addPropertyValue("username", "三友的java日記")
                        .getBeanDefinition()
        ;

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

        }

        測試:

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

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

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

        }

        結(jié)果:

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

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

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

        總結(jié)

        @Import注解作用示意圖

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

        這種方式有什么好處呢?

        ImportSelector和ImportBeanDefinitionRegistrar的方法是有入?yún)⒌?,也就是注解的一些屬性的封裝,所以就可以根據(jù)注解的屬性的配置,來決定應(yīng)該返回樣的配置類或者是應(yīng)該往容器中注入什么樣的類型的Bean,可以看一下 @EnableAsync 的實現(xiàn),看看是如何根據(jù)@EnableAsync注解的屬性來決定往容器中注入什么樣的Bean。

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

        Bean的生命周期

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

        Bean注冊示意圖

        為什么需要去注冊Bean?

        當(dāng)然是為了讓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 InitializingBeanApplicationContextAwareDisposableBean {

            @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)建和銷毀的過程當(dāng)我們實現(xiàn)了某些接口或者加了某些注解,Spring就會回調(diào)我們實現(xiàn)的接口或者執(zhí)行的方法。

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

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

        回調(diào)順序

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

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

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

        這里列舉幾個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)建過程中一個非常重要的擴(kuò)展點,因為每個Bean在創(chuàng)建的各個階段,都會回調(diào)BeanPostProcessor及其子接口的方法,傳入正在創(chuàng)建的Bean對象,這樣如果想對Bean創(chuàng)建過程中某個階段進(jìn)行自定義擴(kuò)展,那么就可以自定義BeanPostProcessor來完成。

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

        來個Demo

        現(xiàn)在需要實現(xiàn)一個這樣的需求,如果Bean的類型是User,那么就設(shè)置這個對象的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 ,就把這個對象 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就會判斷當(dāng)前創(chuàng)建的Bean的類型,如果是User類型,那么就會將username的屬性設(shè)置為 ”三友的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)用對應(yīng)的方法起作用的。

        BeanPostProcessor在Dubbo中的使用

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

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

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

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

        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的容器,通過這個接口,就對容器進(jìn)行為所欲為的操作。

        Spring SPI機(jī)制

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

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

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

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

        SpringFactoriesLoader

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

        show me the code

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

        public class MyEnableAutoConfiguration {
        }

        spring.factories文件

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

        然后放在META-INF底下

        測試:

        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鍵對應(yīng)的值。

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

        SpringBoot啟動擴(kuò)展點

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

        1、自動裝配

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

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

        @EnableAutoConfiguration注解:

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

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

        在AutoConfigurationImportSelector中還有這樣一段代碼

        所以,這段代碼也明顯地可以看出,自動裝配也是基于SPI機(jī)制實現(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)了自動裝配的擴(kuò)展。

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

        @SpringBootApplication
        public class Application {

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

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

                System.out.println("獲取到的Bean為" + user);
            }

        }

        運行結(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對象。

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

        Mybatis整合SpringBoot的spring.factories文件

        2、PropertySourceLoader

        PropertySourceLoader,這是干啥的呢?

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

        當(dāng)然是基于該小節(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é)尾的配置文件

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

        所以可以看出,要想實現(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)有了,那么怎么用呢?當(dāng)然是SPI機(jī)制了,SpringBoot對于配置文件的處理,就是依靠SPI機(jī)制,這也是能擴(kuò)展的重要原因。

        SPI機(jī)制加載PropertySourceLoader實現(xiàn)
        spring.factories文件配置PropertySourceLoader

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

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

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

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

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

        測試

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

        application.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配置文件格式的解析。

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

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

        3、ApplicationContextInitializer

        ApplicationContextInitializer也是SpringBoot啟動過程的一個擴(kuò)展點。

        ApplicationContextInitializer

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

        那怎么用呢?

        依然是SPI。

        SPI加載ApplicationContextInitializer

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

        調(diào)用initialize

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

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

        4、EnvironmentPostProcessor

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

        EnvironmentPostProcessor

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

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

        ConfigFileApplicationListener

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

        5、ApplicationRunner和CommandLineRunner

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

        那怎么擴(kuò)展呢?

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

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

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

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

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

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

        Spring Event 事件

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

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

        什么是Spring Event 事件

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

        Spring提供的事件api:

        ApplicationEvent

        ApplicationEvent

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

        ApplicationListener

        ApplicationListener

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

        ApplicationEventPublisher

        ApplicationEventPublisher

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

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

        那怎么才能拿到ApplicationContext呢?

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

        話不多說,上代碼

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

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

        火災(zāi)事件類繼承ApplicationEvent

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

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

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

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

        public class Call119FireEventListener implements ApplicationListener<FireEvent{

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

        }

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

        public class SavePersonFireEventListener implements ApplicationListener<FireEvent{

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

        }

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

        public class Application {

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

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

        }

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

        運行結(jié)果:

        119
        救人

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

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

        Spring內(nèi)置的事件

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

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

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

        Spring事件的傳播

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

        我們都知道,在Spring中有子父容器的概念,而Spring事件的傳播就是指當(dāng)通過子容器發(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();

                // 設(shè)置一下父容器
                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)聽了著火事件。

        源碼驗證

        事件傳播源碼

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

        傳播特性的一個坑

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

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

        如何解決這個坑呢?

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

        Spring事件的運用舉例

        1、在Mybatis中的使用

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

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

        可以給個差評。

        膨脹了膨脹了。。

        2、在SpringCloud的運用

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

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

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

        AbstractAutoServiceRegistration

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

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

        命名空間

        最后來講一個可能沒有留意,但是很神奇的擴(kuò)展點--命名空間。起初我知道這個擴(kuò)展點的時候,我都驚呆了,這玩意也能擴(kuò)展?真的不得不佩服Spring設(shè)計的可擴(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)簽就代表了一個命名空間。

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

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

        接下來自定義命名空間 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 便簽的子標(biāo)簽,類型是Bean ,就會找到上面的complexType=Bean類型,然后處理屬性  -->
            <xsd:element name="mybean" type="Bean"/>
        </xsd:schema>

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

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

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

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

        public class SanYouNameSpaceHandler extends NamespaceHandlerSupport {

            @Override
            public void init() {
                //注冊解析 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這個標(biāo)簽讀出來,拿到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命名空間應(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)行測試

        測試

        先構(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 標(biāo)簽,配置一個 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這個對象,說明自定義標(biāo)簽生效了。

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

        NameSpaceHandler的spring實現(xiàn)

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

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

        1、Mybatis的擴(kuò)展

        Mybatis的NameSpaceHandler實現(xiàn)

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

        2、dubbo的擴(kuò)展

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

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

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

        DubboNamespaceHandler

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

        總結(jié)

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

        整體
        SpringBoot啟動擴(kuò)展點

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

        END


        Java項目訓(xùn)練營

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

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

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

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

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

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

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

        瀏覽 314
        2點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 天天日天天日天天日 | 毛片超碰 | 久久伊人在线 | 天天综合网7799精品视频 | 黄色电影视 |