聊一聊 Spring 中的擴充套件機制(一)

glmapper發表於2018-08-19

之前 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");

}
}複製程式碼

客戶端類中,註冊一個nameglmapper的使用者,執行結果:

使用者: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被載入的過程中獲取載入該BeanBeanFactory,同時也可以獲取這個BeanFactory中載入的其它Bean

來想一個問題,我們為什麼需要通過BeanFactorygetBean來獲取Bean呢?Spring已經提供了很多便捷的注入方式,那麼通過BeanFactorygetBean來獲取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過來。

來源:https://juejin.im/post/5b7964d6f265da43412866c7

相關文章