好記性不如爛筆頭: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<T> extends 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>, InitializingBean, ApplicationContextAware {
// 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é)
其實(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。
為什么需要去注冊(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 InitializingBean, ApplicationContextAware, DisposableBean {
@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)用的順序。
紅色部分發(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 ApplicationContextAware, BeanFactoryPostProcessor {
// 忽略
}
當(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.class, MyEnableAutoConfiguration.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。
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ò)展的重要原因。
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的配置文件
改造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不僅實(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)。
在SpringBoot啟動(dòng)過程,會(huì)回調(diào)這個(gè)類的實(shí)現(xiàn)initialize方法,傳入ConfigurableApplicationContext。
那怎么用呢?
依然是SPI。
然后遍歷所有的實(shí)現(xiàn),依次調(diào)用
這里就不演示了,實(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是用來處理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在這個(gè)對(duì)象的。
說這個(gè)類的主要原因,主要不是說擴(kuò)展,而是他的一個(gè)實(shí)現(xiàn)類很關(guān)鍵。
這個(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)類。
所以要想擴(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
事件的父類,所有具體的事件都得繼承這個(gè)類,構(gòu)造方法的參數(shù)是這個(gè)事件攜帶的參數(shù),監(jiān)聽器就可以通過這個(gè)參數(shù)來進(jìn)行一些業(yè)務(wù)操作。
ApplicationListener
事件監(jiān)聽的接口,泛型是子類需要監(jiān)聽的事件類型,子類需要實(shí)現(xiàn)onApplicationEvent,參數(shù)就是事件類型,onApplicationEvent方法的實(shí)現(xiàn)就代表了對(duì)事件的處理,當(dāng)事件發(fā)生時(shí),Spring會(huì)回調(diào)onApplicationEvent方法的實(shí)現(xiàn),傳入發(fā)布的事件。
ApplicationEventPublisher
事件發(fā)布器,通過publishEvent方法就可以發(fā)布一個(gè)事件,然后就可以觸發(fā)監(jiān)聽這個(gè)事件的監(jiān)聽器的回調(diào)。
ApplicationContext實(shí)現(xiàn)了ApplicationEventPublisher接口,所以通過ApplicationContext就可以發(fā)布事件。
那怎么才能拿到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>, 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();
}
}
}
說實(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)聽
一般不同的注冊(cè)中心都會(huì)去繼承這個(gè)類,監(jiān)聽項(xiàng)目啟動(dòng),實(shí)現(xiàn)往注冊(cè)中心服務(wù)端進(jìn)行注冊(cè)。
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的
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文件夾就行了。
到這里,就完成了擴(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接口的這些實(shí)現(xiàn)類的命名就可以看出來有哪些擴(kuò)展和這些擴(kuò)展的作用,比如有處理aop的,有處理mvc的等等之類的。
開源框架對(duì)命名空間的擴(kuò)展
1、Mybatis的擴(kuò)展
這個(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。
不得不說,dubbo解析的標(biāo)簽可真的多啊,不過功能也是真的多。
總結(jié)
到這,本文就接近尾聲了,這里畫兩張圖來總結(jié)一下本文講了Spring的哪些擴(kuò)展點(diǎn)。
通過學(xué)習(xí)Spring的這些擴(kuò)展點(diǎn),既可以幫助我們應(yīng)對(duì)日常的開發(fā),還可以幫助我們更好地看懂Spring的源碼。
我開通了項(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)
