三萬字盤點Spring/Boot的那些常用擴展點
大家好,我是三友。
Spring對於每個Java後端程式設計師來說肯定不陌生,日常開發和面試必備的。本文就來盤點Spring/SpringBoot常見的擴展點,同時也來看看常見的開源框架是如何基於這些擴展點跟Spring/SpringBoot整合的。
話不多說,直接進入正題。
FactoryBean
提起FactoryBean,就有一道「著名」的面試題「說一說FactoryBean和BeanFactory的區別」。其實這兩者除了名字有點像,沒有半毛錢關係。。
BeanFactory是Bean的工廠,可以幫我們生成想要的Bean,而FactoryBean就是一種Bean的類型。當往容器中注入class類型為FactoryBean的類型的時候,最終生成的Bean是用過FactoryBean的getObject獲取的。
來個FactoryBean的Demo
定義一個UserFactoryBean,實現FactoryBean介面,getObject方法返回一個User對象
public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { User user = new User(); System.out.println("調用 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)); } }
結果:
調用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@396e2f39 獲取到的Bean為com.sanyou.spring.extension.User@396e2f39
從結果可以看出,明明註冊到Spring容器的是UserFactoryBean,但是卻能從容器中獲取到User類型的Bean,User這個Bean就是通過UserFactoryBean的getObject方法返回的。
FactoryBean在開源框架中的使用
1、 在Mybatis中的使用
Mybatis在整合Spring的時候,就是通過FactoryBean來實現的,這也就是為什麼在Spring的Bean中可以注入Mybatis的Mapper介面的動態代理對象的原因。
程式碼如下,省略了不重要的程式碼。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { // mapper的介面類型 private Class<T> mapperInterface; @Override public T getObject() throws Exception { // 通過SqlSession獲取介面的動態搭理對象 return getSqlSession().getMapper(this.mapperInterface); } @Override public Class<T> getObjectType() { return this.mapperInterface; } }
getObject方法的實現就是返回通過SqlSession獲取到的Mapper介面的動態代理對象。
而@MapperScan註解的作用就是將每個介面對應的MapperFactoryBean註冊到Spring容器的。
2、在OpenFeign中的使用
FeignClient介面的動態代理也是通過FactoryBean注入到Spring中的。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { // FeignClient介面類型 private Class<?> type; @Override public Object getObject() throws Exception { return getTarget(); } @Override public Class<?> getObjectType() { return type; } }
getObject方法是調用getTarget方法來返回的動態代理。
@EnableFeignClients註解的作用就是將每個介面對應的FeignClientFactoryBean注入到Spring容器的。
一般來說,FactoryBean 比較適合那種複雜Bean的構建,在其他框架整合Spring的時候用的比較多。
@Import註解
@Import註解在項目中可能不常見,但是下面這兩個註解肯定常見。
@Import({SchedulingConfiguration.class}) public @interface EnableScheduling { } @Import({AsyncConfigurationSelector.class}) public @interface EnableAsync { //忽略 }
@EnableScheduling和@EnableAsync兩個註解,一個是開啟定時任務,一個是開啟非同步執行。通過這兩個註解可以看出,他們都使用了@Import註解,所以真正起作用的是@Import註解。並且在很多情況下,@EnbaleXXX這種格式的註解,都是通過@Import註解起作用的,代表開啟了某個功能。
@Import註解導入的配置類的分類
@Import註解導入的配置類可以分為三種情況:
第一種:配置類實現了 ImportSelector 介面
public interface ImportSelector { String[] selectImports(AnnotationMetadata importingClassMetadata); @Nullable default Predicate<String> getExclusionFilter() { return null; } }
當配置類實現了 ImportSelector 介面的時候,就會調用 selectImports 方法的實現,獲取一批類的全限定名,最終這些類就會被註冊到Spring容器中。
UserImportSelector實現了ImportSelector,selectImports方法返回User的全限定名,代表吧User這個類註冊容器中
public class UserImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { System.out.println("調用 UserImportSelector 的 selectImports 方法獲取一批類限定名"); return new String[]{"com.sanyou.spring.extension.User"}; } }
測試:
// @Import 註解導入 UserImportSelector @Import(UserImportSelector.class) public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); //將 Application 註冊到容器中 applicationContext.register(Application.class); applicationContext.refresh(); System.out.println("獲取到的Bean為" + applicationContext.getBean(User.class)); } }
結果:
調用 UserImportSelector 的 selectImports 方法獲取一批類限定名 獲取到的Bean為com.sanyou.spring.extension.User@282003e1
所以可以看出,的確成功往容器中注入了User這個Bean
第二種:配置類實現了 ImportBeanDefinitionRegistrar 介面
public interface ImportBeanDefinitionRegistrar { default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) { registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
當配置類實現了 ImportBeanDefinitionRegistrar 介面,你就可以自定義往容器中註冊想注入的Bean。這個介面相比與 ImportSelector 介面的主要區別就是,ImportSelector介面是返回一個類,你不能對這個類進行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加屬性之類的。
來個demo:
實現ImportBeanDefinitionRegistrar介面
public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { //構建一個 BeanDefinition , Bean的類型為 User AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class) // 設置 User 這個Bean的屬性username的值為三友的java日記 .addPropertyValue("username", "三友的java日記") .getBeanDefinition(); System.out.println("往Spring容器中注入User"); //把 User 這個Bean的定義註冊到容器中 registry.registerBeanDefinition("user", beanDefinition); } }
測試:
// 導入 UserImportBeanDefinitionRegistrar @Import(UserImportBeanDefinitionRegistrar.class) public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); //將 Application 註冊到容器中 applicationContext.register(Application.class); applicationContext.refresh(); User user = applicationContext.getBean(User.class); System.out.println("獲取到的Bean為" + user + ",屬性username值為:" + user.getUsername()); } }
結果:
往Spring容器中注入User 獲取到的Bean為com.sanyou.spring.extension.User@6385cb26,屬性username值為:三友的java日記
第三種:配置類什麼介面都沒實現
這種就不演示了,就是一個普普通通的類。
總結
@Import註解作用示意圖
其實不論是什麼樣的配置類,主要的作用就是往Spring容器中註冊Bean,只不過注入的方式不同罷了。
這種方式有什麼好處呢?
ImportSelector和ImportBeanDefinitionRegistrar的方法是有入參的,也就是註解的一些屬性的封裝,所以就可以根據註解的屬性的配置,來決定應該返回樣的配置類或者是應該往容器中注入什麼樣的類型的Bean,可以看一下 @EnableAsync 的實現,看看是如何根據@EnableAsync註解的屬性來決定往容器中注入什麼樣的Bean。
@Import的核心作用就是導入配置類,並且還可以根據配合(比如@EnableXXX)使用的註解的屬性來決定應該往Spring中注入什麼樣的Bean。
Bean的生命周期
第一節講的FactoryBean是一種特殊的Bean的類型,@Import註解是往Spring容器中註冊Bean。其實不論是@Import註解,還是@Component、@Bean等註解,又或是xml配置,甚至是demo中的register方法,其實主要都是做了一件事,那就是往Spring容器去註冊Bean。
Bean註冊示意圖
為什麼需要去註冊Bean?
當然是為了讓Spring知道要為我們生成Bean,並且需要按照我的要求來生成Bean,比如說,我要@Autowired一個對象,那麼你在創建Bean的過程中,就得給我@Autowired一個對象,這就是一個IOC的過程。所以這就涉及了Bean的創建,銷毀的過程,也就是面試常問的Bean的生命周期。我之前寫過兩篇文章,來剖析Bean的生命周期的源碼,有需要的小夥伴可以看一下關注微信公眾號 三友的java日記 回復 Bean 即可獲取。
本節來著重看一下,一個Bean在創建的過程中,有哪些常見的操作Spring在Bean的創建過程中給我們完成,並且操作的順序是什麼樣的。
話不多說,直接測試,基於結果來分析。
Bean生命周期的回調
先來測試
創建LifeCycle類
創建了一個LifeCycle,實現了 InitializingBean、ApplicationContextAware、DisposableBean介面,加了@PostConstruct、@PreDestroy註解,注入了一個User對象。
public class LifeCycle implements InitializingBean, ApplicationContextAware, DisposableBean { @Autowired private User user; public LifeCycle() { System.out.println("LifeCycle對象被創建了"); } /** * 實現的 Aware 回調介面 * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("Aware介面起作用,setApplicationContext被調用了,此時user=" + user); } @PostConstruct public void postConstruct() { System.out.println("@PostConstruct註解起作用,postConstruct方法被調用了"); } /** * 實現 InitializingBean 介面 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean介面起作用,afterPropertiesSet方法被調用了"); } /** * 通過 {@link Bean#initMethod()}來指定 * * @throws Exception */ public void initMethod() throws Exception { System.out.println("@Bean#initMethod()起作用,initMethod方法被調用了"); } @PreDestroy public void preDestroy() throws Exception { System.out.println("@PreDestroy註解起作用,preDestroy方法被調用了"); } /** * 通過 {@link Bean#destroyMethod()}來指定 * * @throws Exception */ public void destroyMethod() throws Exception { System.out.println("@Bean#destroyMethod()起作用,destroyMethod方法被調用了"); } /** * 實現 DisposableBean 註解 * * @throws Exception */ @Override public void destroy() throws Exception { System.out.println("DisposableBean介面起作用,destroy方法被調用了"); } }
聲明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(); // 關閉上下文,觸發銷毀操作 applicationContext.close(); } @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod") public LifeCycle lifeCycle() { return new LifeCycle(); } @Bean public User user() { return new User(); } }
執行結果:
LifeCycle對象被創建了 Aware介面起作用,setApplicationContext被調用了,此時user=com.sanyou.spring.extension.User@57d5872c @PostConstruct註解起作用,postConstruct方法被調用了 InitializingBean介面起作用,afterPropertiesSet方法被調用了 @Bean#initMethod()起作用,initMethod方法被調用了 @PreDestroy註解起作用,preDestroy方法被調用了 DisposableBean介面起作用,destroy方法被調用了 @Bean#destroyMethod()起作用,destroyMethod方法被調用了
分析結果
通過測試的結果可以看出,Bean在創建和銷毀的過程當我們實現了某些介面或者加了某些註解,Spring就會回調我們實現的介面或者執行的方法。
同時,在執行setApplicationContext的時候,能列印出User對象,說明User已經被注入了,說明注入發生在setApplicationContext之前。
這裡畫張圖總結一下Bean創建和銷毀過程中調用的順序。
回調順序
紅色部分發生在Bean的創建過程,灰色部分發生在Bean銷毀的過程中,在容器關閉的時候,就會銷毀Bean。
這裡說一下圖中的Aware介面指的是什麼。其餘的其實沒什麼好說的,就是按照這種方式配置,Spring會調用對應的方法而已。
Aware介面是指以Aware結尾的一些Spring提供的介面,當你的Bean實現了這些介面的話,在創建過程中會回調對應的set方法,並傳入響應的對象。
這裡列舉幾個Aware介面以及它們的作用
介面 | 作用 |
---|---|
ApplicationContextAware | 注入ApplicationContext |
ApplicationEventPublisherAware | 注入ApplicationEventPublisher事件發布器 |
BeanFactoryAware | 注入BeanFactory |
BeanNameAware | 注入Bean的名稱 |
有了這些回調,比如說我的Bean想拿到ApplicationContext,不僅可以通過@Autowired注入,還可以通過實現ApplicationContextAware介面拿到。
通過上面的例子我們知道了比如說@PostConstruct註解、@Autowired註解、@PreDestroy註解的作用,但是它們是如何在不同的階段實現的呢?接著往下看。
BeanPostProcessor
BeanPostProcessor,中文名 Bean的後置處理器,在Bean創建的過程中起作用。
BeanPostProcessor是Bean在創建過程中一個非常重要的擴展點,因為每個Bean在創建的各個階段,都會回調BeanPostProcessor及其子介面的方法,傳入正在創建的Bean對象,這樣如果想對Bean創建過程中某個階段進行自定義擴展,那麼就可以自定義BeanPostProcessor來完成。
說得簡單點,BeanPostProcessor就是在Bean創建過程中留的口子,通過這個口子可以對正在創建的Bean進行擴展。只不過Bean創建的階段比較多,然後BeanPostProcessor介面以及他的子介面InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多方法,可以使得在不同的階段都可以拿到正在創建的Bean進行擴展。
來個Demo
現在需要實現一個這樣的需求,如果Bean的類型是User,那麼就設置這個對象的username屬性為 」三友的java日記「。
那麼就可以這麼寫:
public class UserBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof User) { //如果當前的Bean的類型是 User ,就把這個對象 username 的屬性賦值為 三友的java日記 ((User) bean).setUsername("三友的java日記"); } return bean; } }
測試:
public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); //將 UserBeanPostProcessor 和 User 註冊到容器中 applicationContext.register(UserBeanPostProcessor.class); applicationContext.register(User.class); applicationContext.refresh(); User user = applicationContext.getBean(User.class); System.out.println("獲取到的Bean為" + user + ",屬性username值為:" + user.getUsername()); } }
測試結果:
獲取到的Bean為com.sanyou.spring.extension.User@21a947fe,屬性username值為:三友的java日記
從結果可以看出,每個生成的Bean在執行到某個階段的時候,都會回調UserBeanPostProcessor,然後UserBeanPostProcessor就會判斷當前創建的Bean的類型,如果是User類型,那麼就會將username的屬性設置為 」三友的java日記「。
Spring內置的BeanPostProcessor
這裡我列舉了常見的一些BeanPostProcessor的實現以及它們的作用
BeanPostProcessor | 作用 |
---|---|
AutowiredAnnotationBeanPostProcessor | 處理@Autowired、@Value註解 |
CommonAnnotationBeanPostProcessor | 處理@Resource、@PostConstruct、@PreDestroy註解 |
AnnotationAwareAspectJAutoProxyCreator | 處理一些註解或者是AOP切面的動態代理 |
ApplicationContextAwareProcessor | 處理Aware介面注入的 |
AsyncAnnotationBeanPostProcessor | 處理@Async註解 |
ScheduledAnnotationBeanPostProcessor | 處理@Scheduled註解 |
通過列舉的這些BeanPostProcessor的實現可以看出,Spring Bean的很多註解的處理都是依靠BeanPostProcessor及其子類的實現來完成的,這也回答了上一小節的疑問,處理@Autowired、@PostConstruct、@PreDestroy註解是如何起作用的,其實就是通過BeanPostProcessor,在Bean的不同階段來調用對應的方法起作用的。
BeanPostProcessor在Dubbo中的使用
在Dubbo中可以通過@DubboReference(@Reference)來引用生產者提供的介面,這個註解的處理也是依靠ReferenceAnnotationBeanPostProcessor,也就是 BeanPostProcessor 的擴展來實現的。
public class ReferenceAnnotationBeanPostProcessor extends AbstractAnnotationBeanPostProcessor implements ApplicationContextAware, BeanFactoryPostProcessor { // 忽略 }
當Bean在創建的某一階段,走到了ReferenceAnnotationBeanPostProcessor這個類,就會根據反射找出這個類有沒有@DubboReference(@Reference)註解,有的話就構建一個動態搭理注入就可以了。
BeanPostProcessor在Spring Bean的擴展中扮演著重要的角色,是Spring Bean生命周期中很重要的一部分。正是因為有了BeanPostProcessor,你就可以在Bean創建過程中的任意一個階段擴展自己想要的東西。
BeanFactoryPostProcessor
通過上面一節我們知道 BeanPostProcessor 是對Bean的處理,那麼BeanFactoryPostProcessor很容易就猜到是對BeanFactory,也就是Spring容器的處理。
舉個例子,如果我們想禁止循環依賴,那麼就可以這麼寫。
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 禁止循環依賴 ((DefaultListableBeanFactory) beanFactory).setAllowCircularReferences(false); } }
後面只需要將注入到Spring容器中就會生效。
BeanFactoryPostProcessor是可以對Spring容器做處理的,方法的入參就是Spring的容器,通過這個介面,就對容器進行為所欲為的操作。
Spring SPI機制
SPI全稱為 (Service Provider Interface),是一種動態替換髮現的機制,一種解耦非常優秀的思想,SPI可以很靈活的讓介面和實現分離, 讓api提供者只提供介面,第三方來實現,然後可以使用配置文件的方式來實現替換或者擴展,在框架中比較常見,提高框架的可擴展性。
JDK有內置的SPI機制的實現ServiceLoader,Dubbo也有自己的SPI機制的實現ExtensionLoader,但是這裡我們都不講。。
但是,我之前寫過相關的文章,文章的前半部分有對比三者的區別,有需要的小夥伴可以關注微信公眾號 三友的java日記 回復 dubbo spi 即可獲得。
這裡我們著重講一下Spring的SPI機制的實現SpringFactoriesLoader。
SpringFactoriesLoader
Spring的SPI機制規定,配置文件必須在classpath路徑下的META-INF文件夾內,文件名必須為spring.factories,文件內容為鍵值對,一個鍵可以有多個值,只需要用逗號分割就行,同時鍵值都需要是類的全限定名。但是鍵和值可以沒有任何關係,當然想有也可以有。
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.class, MyEnableAutoConfiguration.class.getClassLoader()); classNames.forEach(System.out::println); } }
結果:
com.sanyou.spring.extension.User
可以看出,通過SpringFactoriesLoader的確可以從spring.factories文件中拿到MyEnableAutoConfiguration鍵對應的值。
到這你可能說會,這SPI機制也沒啥用啊。的確,我這個例子比較簡單,拿到就是遍歷,但是在Spring中,如果Spring在載入類的話使用SPI機制,那我們就可以擴展,接著往下看。
SpringBoot啟動擴展點
SpringBoot項目在啟動的過程中有很多擴展點,這裡就來盤點一下幾個常見的擴展點。
1、自動裝配
說到SpringBoot的擴展點,第一時間肯定想到的就是自動裝配機制,面試賊喜歡問,但是其實就是一個很簡單的東西。當項目啟動的時候,會去從所有的spring.factories文件中讀取@EnableAutoConfiguration鍵對應的值,拿到配置類,然後根據一些條件判斷,決定哪些配置可以使用,哪些不能使用。
spring.factories文件?鍵值?不錯,自動裝配說白了就是SPI機制的一種運用場景。
@EnableAutoConfiguration註解:
@Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { //忽略 }
我擦,這個註解也是使用@Import註解,而且配置類還實現了ImportSelector介面,跟前面也都對上了。在SpringBoot中,@EnableAutoConfiguration是通過@SpringBootApplication來使用的。
在AutoConfigurationImportSelector中還有這樣一段程式碼
所以,這段程式碼也明顯地可以看出,自動裝配也是基於SPI機制實現的。
那麼我想實現自動裝配怎麼辦呢?很簡單,只需兩步。
第一步,寫個配置類:
@Configuration public class UserAutoConfiguration { @Bean public UserFactoryBean userFactoryBean() { return new UserFactoryBean(); } }
這裡我為了跟前面的知識有關聯,配置了一個UserFactoryBean。
第二步,往spring.factories文件配置一下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.sanyou.spring.extension.springbootextension.UserAutoConfiguration
到這就已經實現了自動裝配的擴展。
接下來進行測試:
@SpringBootApplication public class Application { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class); User user = applicationContext.getBean(User.class); System.out.println("獲取到的Bean為" + user); } }
運行結果:
調用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@3406472c 獲取到的Bean為com.sanyou.spring.extension.User@3406472c
從運行結果可以看出,自動裝配起了作用,並且雖然往容器中注入的Bean的class類型為UserFactoryBean,但是最終會調用UserFactoryBean的getObject的實現獲取到User對象。
自動裝配機制是SpringBoot的一個很重要的擴展點,很多框架在整合SpringBoot的時候,也都通過自動裝配來的,實現項目啟動,框架就自動啟動的,這裡我舉個Mybatis整合SpringBoot。
Mybatis整合SpringBoot的spring.factories文件
2、PropertySourceLoader
PropertySourceLoader,這是幹啥的呢?
我們都知道,在SpringBoot環境下,外部化的配置文件支援properties和yaml兩種格式。但是,現在不想使用properties和yaml格式的文件,想使用json格式的配置文件,怎麼辦?
當然是基於該小節講的PropertySourceLoader來實現的。
public interface PropertySourceLoader { //可以支援哪種文件格式的解析 String[] getFileExtensions(); // 解析配置文件,讀出內容,封裝成一個PropertySource<?>結合返回回去 List<PropertySource<?>> load(String name, Resource resource) throws IOException; }
對於PropertySourceLoader的實現,SpringBoot兩個實現
PropertiesPropertySourceLoader:可以解析properties或者xml結尾的配置文件
YamlPropertySourceLoader:解析以yml或者yaml結尾的配置文件
所以可以看出,要想實現json格式的支援,只需要自己實現可以用來解析json格式的配置文件的PropertySourceLoader就可以了。
動手來一個。
實現可以讀取json格式的配置文件
實現這個功能,只需要兩步就可以了。
第一步:自定義一個PropertySourceLoader
JsonPropertySourceLoader,實現PropertySourceLoader介面
public class JsonPropertySourceLoader implements PropertySourceLoader { @Override public String[] getFileExtensions() { //這個方法表明這個類支援解析以json結尾的配置文件 return new String[]{"json"}; } @Override public List<PropertySource<?>> load(String name, Resource resource) throws IOException { ReadableByteChannel readableByteChannel = resource.readableChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate((int) resource.contentLength()); //將文件內容讀到 ByteBuffer 中 readableByteChannel.read(byteBuffer); //將讀出來的位元組轉換成字元串 String content = new String(byteBuffer.array()); // 將字元串轉換成 JSONObject JSONObject jsonObject = JSON.parseObject(content); Map<String, Object> map = new HashMap<>(jsonObject.size()); //將 json 的鍵值對讀出來,放入到 map 中 for (String key : jsonObject.keySet()) { map.put(key, jsonObject.getString(key)); } return Collections.singletonList(new MapPropertySource("jsonPropertySource", map)); } }
第二步:配置PropertySourceLoader
JsonPropertySourceLoader 已經有了,那麼怎麼用呢?當然是SPI機制了,SpringBoot對於配置文件的處理,就是依靠SPI機制,這也是能擴展的重要原因。
SPI機制載入PropertySourceLoader實現spring.factories文件配置PropertySourceLoader
SpringBoot會先通過SPI機制載入所有PropertySourceLoader,然後遍歷每個PropertySourceLoader,判斷當前遍歷的PropertySourceLoader,通過getFileExtensions獲取到當前PropertySourceLoader能夠支援哪些配置文件格式的解析,讓後跟當前需要解析的文件格式進行匹配,如果能匹配上,那麼就會使用當前遍歷的PropertySourceLoader來解析配置文件。
PropertySourceLoader其實就屬於策略介面,配置文件的解析就是策略模式的運用。
所以,只需要按照這種格式,在spring.factories文件中配置一下就行了。
org.springframework.boot.env.PropertySourceLoader=\
com.sanyou.spring.extension.springbootextension.propertysourceloader.JsonPropertySourceLoader
到此,其實就擴展完了,接下來就來測試一下。
測試
先創建一個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(); } }
運行結果:
獲取到的Bean為com.sanyou.spring.extension.User@481ba2cf,屬性username值為:三友的java日記
成功將json配置文件的屬性注入到User對象中。
至此,SpringBoot就支援了以json為結尾的配置文件格式。
Nacos對於PropertySourceLoader的實現
如果你的項目正在用Nacos作為配置中心,那麼剛剛好,Nacos已經實現json配置文件格式的解析。
Nacos對於PropertySourceLoader的實現
Nacos不僅實現了json格式的解析,也實現了關於xml格式的配置文件的解析,並且優先順序會比SpringBoot默認的xml格式文件解析的優先順序高。至於Nacos為啥需要實現PropertySourceLoader?其實很簡單,因為Nacos作為配置中心,不僅支援properties和yaml格式的文件,還支援json格式的配置文件,那麼客戶端拿到這些配置就需要解析,SpringBoot已經支援了properties和yaml格式的文件的解析,那麼Nacos只需要實現SpringBoot不支援的就可以了。
3、ApplicationContextInitializer
ApplicationContextInitializer也是SpringBoot啟動過程的一個擴展點。
ApplicationContextInitializer
在SpringBoot啟動過程,會回調這個類的實現initialize方法,傳入ConfigurableApplicationContext。
那怎麼用呢?
依然是SPI。
SPI載入ApplicationContextInitializer
然後遍歷所有的實現,依次調用
調用initialize
這裡就不演示了,實現介面,按照如下這種配置就行了
但是這裡需要注意的是,此時傳入的ConfigurableApplicationContext並沒有調用過refresh方法,也就是裡面是沒有Bean對象的,一般這個介面是用來配置ConfigurableApplicationContext,而不是用來獲取Bean的。
4、EnvironmentPostProcessor
EnvironmentPostProcessor在SpringBoot啟動過程中,也會調用,也是通過SPI機制來載入擴展的。
EnvironmentPostProcessor
EnvironmentPostProcessor是用來處理ConfigurableEnvironment的,也就是一些配置資訊,SpringBoot所有的配置都是存在這個對象的。
說這個類的主要原因,主要不是說擴展,而是他的一個實現類很關鍵。
ConfigFileApplicationListener
這個類的作用就是用來處理外部化配置文件的,也就是這個類是用來處理配置文件的,通過前面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment裡面。
5、ApplicationRunner和CommandLineRunner
ApplicationRunner和CommandLineRunner都是在SpringBoot成功啟動之後會調用,可以拿到啟動時的參數。
那怎麼擴展呢?
當然又是SPI了。
這兩個其實不是通過SPI機制來擴展,而是直接從容器中獲取的,這又是為啥呢?
因為調用ApplicationRunner和CommandLineRunner時,SpringBoot已經啟動成功了,Spring容器都準備好了,需要什麼Bean直接從容器中查找多方便。
而前面說的幾個需要SPI機制的擴展點,是因為在SpringBoot啟動的時候,Spring容器還沒有啟動好,也就是無法從Spring容器獲取到這些擴展的對象,為了兼顧擴展性,所以就通過SPI機制來實現獲取到實現類。
刷新上下文和調用Runner載入和調用Runner
所以要想擴展這個點,只需要實現介面,添加到Spring容器就可以了。
Spring Event 事件
Event 事件可以說是一種觀察者模式的實現,主要是用來解耦合的。當發生了某件事,只要發布一個事件,對這個事件的監聽者(觀察者)就可以對事件進行響應或者處理。
舉個例子來說,假設發生了火災,可能需要打119、救人,那麼就可以基於事件的模型來實現,只需要打119、救人監聽火災的發生就行了,當發生了火災,通知這些打119、救人去觸發相應的邏輯操作。
什麼是Spring Event 事件
那麼是什麼是Spring Event 事件,就是Spring實現了這種事件模型,你只需要基於Spring提供的API進行擴展,就可以完成事件的發布訂閱
Spring提供的事件api:
ApplicationEvent
ApplicationEvent
事件的父類,所有具體的事件都得繼承這個類,構造方法的參數是這個事件攜帶的參數,監聽器就可以通過這個參數來進行一些業務操作。
ApplicationListener
ApplicationListener
事件監聽的介面,泛型是子類需要監聽的事件類型,子類需要實現onApplicationEvent,參數就是事件類型,onApplicationEvent方法的實現就代表了對事件的處理,當事件發生時,Spring會回調onApplicationEvent方法的實現,傳入發布的事件。
ApplicationEventPublisher
ApplicationEventPublisher
事件發布器,通過publishEvent方法就可以發布一個事件,然後就可以觸發監聽這個事件的監聽器的回調。
ApplicationContext實現了ApplicationEventPublisher介面,所以通過ApplicationContext就可以發布事件。
那怎麼才能拿到ApplicationContext呢?
前面Bean生命周期那節說過,可以通過ApplicationContextAware介面拿到,甚至你可以通過實現ApplicationEventPublisherAware直接獲取到ApplicationEventPublisher,其實獲取到的ApplicationEventPublisher也就是ApplicationContext,因為是ApplicationContext實現了ApplicationEventPublisher。
話不多說,上程式碼
就以上面的火災為例
第一步:創建一個火災事件類
火災事件類繼承ApplicationEvent
// 火災事件 public class FireEvent extends ApplicationEvent { public FireEvent(String source) { super(source); } }
第二步:創建火災事件的監聽器
打119的火災事件的監聽器:
public class Call119FireEventListener implements ApplicationListener<FireEvent> { @Override public void onApplicationEvent(FireEvent event) { System.out.println("打119"); } }
救人的火災事件的監聽器:
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> { @Override public void onApplicationEvent(FireEvent event) { System.out.println("救人"); } }
事件和對應的監聽都有了,接下來進行測試:
public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); //將 事件監聽器 註冊到容器中 applicationContext.register(Call119FireEventListener.class); applicationContext.register(SavePersonFireEventListener.class); applicationContext.refresh(); // 發布著火的事件,觸發監聽 applicationContext.publishEvent(new FireEvent("著火了")); } }
將兩個事件註冊到Spring容器中,然後發布FireEvent事件
運行結果:
打119 救人
控制台列印出了結果,觸發了監聽。
如果現在需要對火災進行救火,那麼只需要去監聽FireEvent,實現救火的邏輯,注入到Spring容器中,就可以了,其餘的程式碼根本不用動。
Spring內置的事件
Spring內置的事件很多,這裡我羅列幾個
事件類型 | 觸發時機 |
---|---|
ContextRefreshedEvent | 在調用ConfigurableApplicationContext 介面中的refresh()方法時觸發 |
ContextStartedEvent | 在調用ConfigurableApplicationContext的start()方法時觸發 |
ContextStoppedEvent | 在調用ConfigurableApplicationContext的stop()方法時觸發 |
ContextClosedEvent | 當ApplicationContext被關閉時觸發該事件,也就是調用close()方法觸發 |
在Spring容器啟動的過程中,Spring會發布這些事件,如果你需要這Spring容器啟動的某個時刻進行什麼操作,只需要監聽對應的事件即可。
Spring事件的傳播
Spring事件的傳播是什麼意思呢?
我們都知道,在Spring中有子父容器的概念,而Spring事件的傳播就是指當通過子容器發布一個事件之後,不僅可以觸發在這個子容器的事件監聽器,還可以觸發在父容器的這個事件的監聽器。
上程式碼
public class EventPropagateApplication { public static void main(String[] args) { // 創建一個父容器 AnnotationConfigApplicationContext parentApplicationContext = new AnnotationConfigApplicationContext(); //將 打119監聽器 註冊到父容器中 parentApplicationContext.register(Call119FireEventListener.class); parentApplicationContext.refresh(); // 創建一個子容器 AnnotationConfigApplicationContext childApplicationContext = new AnnotationConfigApplicationContext(); //將 救人監聽器 註冊到子容器中 childApplicationContext.register(SavePersonFireEventListener.class); childApplicationContext.refresh(); // 設置一下父容器 childApplicationContext.setParent(parentApplicationContext); // 通過子容器發布著火的事件,觸發監聽 childApplicationContext.publishEvent(new FireEvent("著火了")); } }
創建了兩個容器,父容器註冊了打119的監聽器,子容器註冊了救人的監聽器,然後將子父容器通過setParent關聯起來,最後通過子容器,發布了著火的事件。
運行結果:
救人 打119
從列印的日誌,的確可以看出,雖然是子容器發布了著火的事件,但是父容器的監聽器也成功監聽了著火事件。
源碼驗證
事件傳播源碼
從這段源碼可以看出,如果父容器不為空,就會通過父容器再發布一次事件。
傳播特性的一個坑
前面說過,在Spring容器啟動的過程,會發布很多事件,如果你需要有相應的擴展,可以監聽這些事件。但是,在SpringCloud環境下,你的這些Spring發布的事件的監聽器可能會執行很多次。為什麼會執行很多次呢?其實就是跟傳播特性有關。
在SpringCloud的環境下,為了使像FeignClient和RibbonClient這些不同的服務的配置相互隔離,會創建很多的子容器,而這些子容器都有一個公共的父容器,那就是SpringBoot項目啟動時創建的容器,事件的監聽器都在這個容器中。而這些為了配置隔離創建的子容器,在容器啟動的過程中,也會發布諸如ContextRefreshedEvent等這樣的事件,如果你監聽了這些事件,那麼由於傳播特性的關係,你的這個事件的監聽器就會觸發多次。
如何解決這個坑呢?
你可以進行判斷這些監聽器有沒有執行過,比如加一個判斷的標誌;或者是監聽類似的事件,比如ApplicationStartedEvent事件,這種事件是在SpringBoot啟動中發布的事件,而子容器不是SpringBoot,所以不會多次發這種事件,也就會只執行一次。
Spring事件的運用舉例
1、在Mybatis中的使用
又來以Mybatis舉例了。。Mybatis的SqlSessionFactoryBean監聽了ApplicationEvent,然後判斷如果是ContextRefreshedEvent就進行相應的處理,這個類還實現了FactoryBean介面。。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { @Override public void onApplicationEvent(ApplicationEvent event) { if (failFast && event instanceof ContextRefreshedEvent) { // fail-fast -> check all statements are completed this.sqlSessionFactory.getConfiguration().getMappedStatementNames(); } } }
說實話,這監聽程式碼寫的不太好,監聽了ApplicationEvent,那麼所有的事件都會回調這個類的onApplicationEvent方法,但是onApplicationEvent方法實現又是當ApplicationEvent是ContextRefreshedEvent類型才會往下走,那為什麼不直接監聽ContextRefreshedEvent呢?
可以給個差評。
膨脹了膨脹了。。
2、在SpringCloud的運用
在SpringCloud的中,當項目啟動的時候,會自動往註冊中心進行註冊,那麼是如何實現的呢?當然也是基於事件來的。當web伺服器啟動完成之後,就發布ServletWebServerInitializedEvent事件。
然後不同的註冊中心的實現都只需要監聽這個事件,就知道web伺服器已經創建好了,那麼就可以往註冊中心註冊服務實例了。如果你的服務沒往註冊中心,看看是不是web環境,因為只有web環境才會發這個事件。
SpringCloud提供了一個抽象類 AbstractAutoServiceRegistration,實現了對WebServerInitializedEvent(ServletWebServerInitializedEvent的父類)事件的監聽
AbstractAutoServiceRegistration
一般不同的註冊中心都會去繼承這個類,監聽項目啟動,實現往註冊中心服務端進行註冊。
Nacos對於AbstractAutoServiceRegistration的繼承
Spring Event事件在Spring內部中運用很多,是解耦合的利器。在實際項目中,你既可以監聽Spring/Boot內置的一些事件,進行相應的擴展,也可以基於這套模型在業務中自定義事件和相應的監聽器,減少業務程式碼的耦合。
命名空間
最後來講一個可能沒有留意,但是很神奇的擴展點–命名空間。起初我知道這個擴展點的時候,我都驚呆了,這玩意也能擴展?真的不得不佩服Spring設計的可擴展性。
回憶一下啥是命名空間?
先看一段配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns="//www.springframework.org/schema/beans" xmlns:context="//www.springframework.org/schema/context" xsi:schemaLocation=" //www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/beans/spring-context.xsd "> <context:component-scan base-package="com.sanyou.spring.extension"/> </beans>
這一段xml配置想必都很熟悉,其中, context 標籤就代表了一個命名空間。
也就說,這個標籤是可以擴展的。
話不多說,來個擴展
接下來自定義命名空間 sanyou,總共分為3步。
第一步:定義一個xsd文件
如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- xmlns 和 targetNamespace 需要定義,結尾為sanyou,前面都一樣的--> <xsd:schema xmlns="//sanyou.com/schema/sanyou" xmlns:xsd="//www.w3.org/2001/XMLSchema" targetNamespace="//sanyou.com/schema/sanyou"> <xsd:import namespace="//www.w3.org/XML/1998/namespace"/> <xsd:complexType name="Bean"> <xsd:attribute name="class" type="xsd:string" use="required"/> </xsd:complexType> <!-- sanyou 便簽的子標籤,類型是Bean ,就會找到上面的complexType=Bean類型,然後處理屬性 --> <xsd:element name="mybean" type="Bean"/> </xsd:schema>
這個xsd文件來指明sanyou這個命名空間下有哪些標籤和屬性。這裡我只指定了一個標籤 mybean,mybean標籤裡面有個class的屬性,然後這個標籤的目的就是將class屬性指定的Bean的類型,注入到Spring容器中,作用跟spring的 標籤的作用是一樣的。
xsd文件沒有需要放的固定的位置,這裡我放到 META-INF 目錄下
第二步:解析這個命名空間
解析命名空間很簡單,Spring都有配套的東西–NamespaceHandler介面,只要實現這個介面就行了。但一般我們不直接實現 NamespaceHandler 介面,我們可以繼承 NamespaceHandlerSupport 類,這個類實現了 NamespaceHandler 介面。
public class SanYouNameSpaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
//註冊解析 mybean 標籤的解析器
registerBeanDefinitionParser("mybean", new SanYouBeanDefinitionParser());
}
private static class SanYouBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected boolean shouldGenerateId() {
return true;
}
@Override
protected String getBeanClassName(Element element) {
return element.getAttribute("class");
}
}
}
SanYouNameSpaceHandler的作用就是將sanyou命名空間中的mybean這個標籤讀出來,拿到class的屬性,然後將這個class屬性指定的class類型注入到Spring容器中,至於註冊這個環節的程式碼,都交給了SanYouBeanDefinitionParser的父類來做了。
第三步:創建並配置spring.handlers和spring.schemas文件
先創建spring.handlers和spring.schemas文件
spring.handlers文件內容
http\://sanyou.com/schema/sanyou=com.sanyou.spring.extension.namespace.SanYouNameSpaceHandler
通過spring.handlers配置文件,就知道sanyou命名空間應該找SanYouNameSpaceHandler進行解析
spring.schemas文內容
http\://sanyou.com/schema/sanyou.xsd=META-INF/sanyou.xsd
spring.schemas配置xsd文件的路徑
文件都有了,只需要放到classpath下的META-INF文件夾就行了。
xsd、spring.handlers、spring.schema文件
到這裡,就完成了擴展,接下來進行測試
測試
先構建一個applicationContext.xml文件,放到resources目錄下
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns="//www.springframework.org/schema/beans" xmlns:sanyou="//sanyou.com/schema/sanyou" xsi:schemaLocation=" //www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd //sanyou.com/schema/sanyou //sanyou.com/schema/sanyou.xsd "> <!--使用 sanyou 標籤,配置一個 User Bean--> <sanyou:mybean class="com.sanyou.spring.extension.User"/> </beans>
再寫個測試類
public class Application { public static void main(String[] args) { ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); applicationContext.refresh(); User user = applicationContext.getBean(User.class); System.out.println(user); } }
運行結果:
com.sanyou.spring.extension.User@27fe3806
成功獲取到User這個對象,說明自定義標籤生效了。
Spring內置命名空間的擴展
NameSpaceHandler的spring實現
通過NameSpaceHandler介面的這些實現類的命名就可以看出來有哪些擴展和這些擴展的作用,比如有處理aop的,有處理mvc的等等之類的。
開源框架對命名空間的擴展
1、Mybatis的擴展
Mybatis的NameSpaceHandler實現
這個就是來掃描指定路徑的mapper介面的,處理 scan 標籤,跟@MapperScan註解的作用是一樣的。
2、dubbo的擴展
使用dubbo可能寫過如下的配置
<dubbo:registry address="zookeeper://192.168.10.119:2181" />
這個dubbo命名空間肯定就是擴展的Spring的,也有對應的dubbo實現的NameSpaceHandler。
DubboNamespaceHandler
不得不說,dubbo解析的標籤可真的多啊,不過功能也是真的多。
總結
到這,本文就接近尾聲了,這裡畫兩張圖來總結一下本文講了Spring的哪些擴展點。
整體SpringBoot啟動擴展點
通過學習Spring的這些擴展點,既可以幫助我們應對日常的開發,還可以幫助我們更好地看懂Spring的源碼。
最後,本文前前後後花了一周多的時間完成,如果對你有點幫助,還請幫忙點贊、在看、轉發、非常感謝。
掃碼或者搜索關注公眾號三友的java日記,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回復 面試 即可獲得一套面試題。
哦,差點忘了,本文所有demo程式碼,可以掃碼或者搜索關注微信公眾號 三友的java日記 ,回復 spring擴展點 即可獲取。
往期熱門文章推薦
掃碼或者搜索關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回復 面試 即可獲得一套面試真題。