之前 Spring 原始碼系列文章中大多是底層原始碼的分析,通過原始碼可以讓我們能夠清晰的瞭解 Spring 到底是什麼,而不是停留於表面的認知。比如當我們要使用 @Autowired 註解時,可以拿到我們想要的 bean ,但是為什麼可以是值得思考的。– 關於閱讀原始碼
Spring原始碼的閱讀結合日常的使用,可以幫助我們更好的掌握這個龐大的技術體系,實際的開發工作中有很多地方可以借鑑它的一些思想來幫助我們更好的實現自己的業務邏輯。本篇將以擴充套件點為切入點,來了解下在Spring生命週期中擴充套件Spring中的Bean功能。
ApplicationListener 擴充套件
ApplicationListener
其實是 spring
事件通知機制中核心概念;在java的事件機制中,一般會有三個概念:
- event object : 事件物件
- event source :事件源,產生事件的地方
- event listener :監聽事件並處理
ApplicationListener
繼承自 java.util.EventListener
,提供了對於Spring
中事件機制的擴充套件。
ApplicationListener
在實際的業務場景中使用的非常多,比如我一般喜歡在容器初始化完成之後來做一些資源載入或者一些元件的初始化。這裡的容器指的就是Ioc
容器,對應的事件是ContextRefreshedEvent
。
@Componentpublic class StartApplicationListener implementsApplicationListener<
ContextRefreshedEvent>
{
@Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//初始化資原始檔 //初始化元件 如:cache
}
}複製程式碼
上面這段程式碼會在容器重新整理完成之後來做一些事情。下面通過自定義事件來看看怎麼使用,在看具體的demo
之前,先來了解下一些關注點。
日常工作了,如果要使用 Spring
事件傳播機制,我們需要關注的點有以下幾點:
- 事件類,這個用來描述事件本身一些屬性,一般繼承
ApplicationEvent
- 監聽類,用來監聽具體的事件並作出響應。需要實現
ApplicationListener
介面 - 事件釋出類,需要通過這個類將時間釋出出去,這樣才能被監聽者監聽到,需要實現
ApplicationContextAware
介面。 - 將事件類和監聽類交給
Spring
容器。
那麼下面就按照這個思路來看下demo
的具體實現。
事件類:UserRegisterEvent
UserRegisterEvent
,使用者註冊事件;這裡作為事件物件,繼承自 ApplicationEvent
。
/** * @description: 使用者註冊事件 * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/25 */public class UserRegisterEvent extends ApplicationEvent {
public String name;
public UserRegisterEvent(Object o) {
super(o);
} public UserRegisterEvent(Object o, String name) {
super(o);
this.name=name;
}
}複製程式碼
事件釋出類:UserService
使用者註冊服務,這裡需要在使用者註冊時將註冊事件釋出出去,所以通過實現ApplicationEventPublisherAware
介面,使UserService
具有事件釋出能力。
ApplicationEventPublisherAware:釋出事件,也就是把某個事件告訴的所有與這個事件相關的監聽器。
/** * @description: 使用者註冊服務,實現ApplicationEventPublisherAware介面 ,表明本身具有事件釋出能力 * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/25 */public class UserService implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
} public void register(String name) {
System.out.println("使用者:" + name + " 已註冊!");
applicationEventPublisher.publishEvent(new UserRegisterEvent(name));
}
}複製程式碼
這裡的UserService
實際上是作為事件源存在的,通過register
將使用者註冊事件傳播出去。那麼下面就是需要定義如何來監聽這個事件,並且將事件進行消費處理掉,這裡就是通過ApplicationListener
來完成。
監聽類:BonusServerListener
當使用者觸發註冊操作時,向積分服務傳送訊息,為使用者初始化積分。
/** * @description: BonusServerListener 積分處理,當使用者註冊時,給當前使用者增加初始化積分 * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/25 */public class BonusServerListener implementsApplicationListener<
UserRegisterEvent>
{
public void onApplicationEvent(UserRegisterEvent event) {
System.out.println("積分服務接到通知,給 " + event.getSource() + " 增加積分...");
}
}複製程式碼
註冊到容器中
<
?xml version="1.0" encoding="UTF-8"?>
<
beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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/context/spring-context.xsd">
<
bean id="userService" class="com.glmapper.extention.UserService"/>
<
bean id="bonusServerListener" class="com.glmapper.extention.BonusServerListener"/>
<
/beans>
複製程式碼
客戶端類
/** * @description: 客戶端類 * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/25 */public class MainTest {
public static void main(String[] args) {
ApplicationContext context =new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
//註冊事件觸發 userService.register("glmapper");
}
}複製程式碼
客戶端類中,註冊一個name
為glmapper
的使用者,執行結果:
使用者:glmapper 已註冊!積分服務接到通知,給 glmapper 增加積分...複製程式碼
現在來考慮另外一個問題,增加一個功能,使用者註冊之後給使用者發一個郵件。這個其實就是增加一個監聽類就可以,前提是這個監聽者是監聽當前事件的。
/** * @description: 郵件服務監聽器,當監聽到使用者的註冊行為時, 給使用者傳送郵件通知 * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/25 */public class EmailServerListener implementsApplicationListener<
UserRegisterEvent>
{
public void onApplicationEvent(UserRegisterEvent event) {
System.out.println("郵件服務接到通知,給 " + event.getSource() + " 傳送郵件...");
複製程式碼
這裡如果將UserRegisterEvent
換成UserLoginEvent
,那麼郵件服務將不會有任何行為。
增加傳送郵件監聽類之後的執行結果:
使用者:glmapper 已註冊!郵件服務接到通知,給 glmapper 傳送郵件...積分服務接到通知,給 glmapper 增加積分...複製程式碼
Spring
的事件傳播機制是基於觀察者模式(Observer
)實現的,它可以將 Spring Bean
的改變定義為事件 ApplicationEvent
,通過 ApplicationListener
監聽 ApplicationEvent
事件,一旦Spring Bean
使用 ApplicationContext.publishEvent( ApplicationEvent event )
釋出事件後,Spring
容器會通知註冊在 容器中所有 ApplicationListener
介面的實現類,最後 ApplicationListener
介面實現類判斷是否處理剛釋出出來的 ApplicationEvent
事件。
ApplicationContextAware 擴充套件
ApplicationContextAware
中只有一個setApplicationContext
方法。實現了ApplicationContextAware
介面的類,可以在該Bean
被載入的過程中獲取Spring
的應用上下文ApplicationContext
,通過ApplicationContext
可以獲取Spring
容器內的很多資訊。
這種一般在需要手動獲取Bean
的注入例項物件時會使用到。下面通過一個簡單的demo
來了解下。
GlmapperApplicationContext
持有ApplicationContext
物件,通過實現 ApplicationContextAware
介面來給ApplicationContext
做賦值。
/** * @description: GlmapperApplicationContext * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/29 */public class GlmapperApplicationContext implementsApplicationContextAware {
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
} public ApplicationContext getApplicationContext(){
return applicationContext;
}
}複製程式碼
需要手動獲取的bean
:
/** * @description: HelloService * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/29 */public class HelloService {
public void sayHello(){
System.out.println("Hello Glmapper");
}
}複製程式碼
在配置檔案中進行配置:
<
bean id="helloService"class="com.glmapper.extention.applicationcontextaware.HelloService"/>
<
bean id="glmapperApplicationContext"class="com.glmapper.extention.applicationcontextaware.GlmapperApplicationContext"/>
複製程式碼
客戶端類呼叫:
public class MainTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloService helloService = (HelloService) context.getBean("helloService");
helloService.sayHello();
//這裡通過實現ApplicationContextAware介面的類來完成bean的獲取 GlmapperApplicationContext glmapperApplicationContext = (GlmapperApplicationContext) context.getBean("glmapperApplicationContext");
ApplicationContext applicationContext = glmapperApplicationContext.getApplicationContext();
HelloService glmapperHelloService = (HelloService) applicationContext.getBean("helloService");
glmapperHelloService.sayHello();
}
}複製程式碼
BeanFactoryAware 擴充套件
我們知道BeanFactory
是整個Ioc
容器最頂層的介面,它規定了容器的基本行為。實現BeanFactoryAware
介面就表明當前類具體BeanFactory
的能力。
BeanFactoryAware
介面中只有一個setBeanFactory
方法。實現了BeanFactoryAware
介面的類,可以在該Bean
被載入的過程中獲取載入該Bean
的BeanFactory
,同時也可以獲取這個BeanFactory
中載入的其它Bean
。
來想一個問題,我們為什麼需要通過BeanFactory
的getBean
來獲取Bean
呢?Spring已經提供了很多便捷的注入方式,那麼通過BeanFactory
的getBean
來獲取Bean
有什麼好處呢?來看一個場景。
現在有一個HelloService
,這個HelloService
就是打招呼,我們需要通過不同的語言來實現打招呼,比如用中文,用英文。一般的做法是:
public interface HelloService {
void sayHello();
}//英文打招呼實現public class GlmapperHelloServiceImpl implements HelloService {
public void sayHello() {
System.out.println("Hello Glmapper");
}
}//中文打招呼實現public class LeishuHelloServiceImpl implements HelloService {
public void sayHello() {
System.out.println("你好,磊叔");
}
}複製程式碼
客戶端類來呼叫務必會出現下面的方式:
if (condition=="英文"){
glmapperHelloService.sayHello();
}if (condition=="中文"){
leishuHelloService.sayHello();
}複製程式碼
如果有一天,老闆說我們要做國際化,要實現全球所有的語言來問候。你是說好的,還是控制不住要動手呢?
那麼有沒有什麼方式可以動態的去決定我的客戶端類到底去呼叫哪一種語言實現,而不是用過if-else方式來羅列呢?是的,對於這些需要動態的去獲取物件的場景,BeanFactoryAware
就可以很好的搞定。OK,來看程式碼改造:
引入BeanFactoryAware
:
/** * @description: 實現BeanFactoryAware ,讓當前bean本身具有 BeanFactory 的能力 * * 實現 BeanFactoηAware 介面的 bean 可以直接訪問 Spring 容器,被容器建立以後, * 它會擁有一個指向 Spring 容器的引用,可以利用該bean根據傳入引數動態獲取被spring工廠載入的bean * * @email: <
a href="glmapper_2018@163.com">
<
/a>
* @author: guolei.sgl * @date: 18/7/29 */public class GlmapperBeanFactory implements BeanFactoryAware {
private BeanFactory beanFactory;
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory=beanFactory;
} /** * 提供一個execute 方法來實現不同業務實現類的排程器方案。 * @param beanName */ public void execute(String beanName){
HelloService helloService=(HelloService) beanFactory.getBean(beanName);
helloService.sayHello();
}
}複製程式碼
這裡為了邏輯方便理解,再加入一個HelloFacade
類,這個類的作用就是持有一個BeanFactoryAware
的例項物件,然後通過HelloFacade
例項物件的方法來遮蔽底層BeanFactoryAware
例項的實現細節。
public class HelloFacade {
private GlmapperBeanFactory glmapperBeanFactory;
//呼叫glmapperBeanFactory的execute方法 public void sayHello(String beanName){
glmapperBeanFactory.execute(beanName);
} public void setGlmapperBeanFactory(GlmapperBeanFactory beanFactory){
this.glmapperBeanFactory = beanFactory;
}
}複製程式碼
客戶端類
public class MainTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloFacade helloFacade = (HelloFacade) context.getBean("helloFacade");
GlmapperBeanFactory glmapperBeanFactory = (GlmapperBeanFactory) context.getBean("glmapperBeanFactory");
//這裡其實可以不通過set方法注入到helloFacade中, //可以在helloFacade中通過autowired //注入;這裡在使用main方法來執行驗證,所以就手動set進入了 helloFacade.setGlmapperBeanFactory(glmapperBeanFactory);
//這個只需要傳入不同HelloService的實現類的beanName, //就可以執行不同的業務邏輯 helloFacade.sayHello("glmapperHelloService");
helloFacade.sayHello("leishuHelloService");
}
}複製程式碼
可以看到在呼叫者(客戶端)類中,只需要通過一個beanName
就可以實現不同實現類的切換,而不是通過一堆if-else來判斷。另外有的小夥伴可能會說,程式怎麼知道用哪個beanName
呢?其實這個也很簡單,這個引數我們可以通過一些途徑來拼接得到,比如使用一個prefix
用來指定語言,prefix
+HelloService
就可以確定唯一的beanName
。
小結
本來想著在一篇文章裡面把擴充套件點都寫一下的,但是實在太長了。後面差不多還有兩篇。本系列中所有的demo
可以在github
獲取,也歡迎小夥伴把能夠想到的擴充套件點pr過來。