聊一聊 Spring 中的擴展機制(一)

以前 Spring 源碼系列文章中大可能是底層源碼的分析,經過源碼可讓咱們可以清晰的瞭解 Spring 究竟是什麼,而不是停留於表面的認知。好比當咱們要使用 @Autowired 註解時,能夠拿到咱們想要的 bean ,可是爲何能夠是值得思考的。-- 關於閱讀源碼php

Spring源碼的閱讀結合平常的使用,能夠幫助咱們更好的掌握這個龐大的技術體系,實際的開發工做中有不少地方能夠借鑑它的一些思想來幫助咱們更好的實現本身的業務邏輯。本篇將以擴展點爲切入點,來了解下在Spring生命週期中擴展Spring中的Bean功能。java

ApplicationListener 擴展

ApplicationListener 實際上是 spring 事件通知機制中核心概念;在java的事件機制中,通常會有三個概念:git

  • event object : 事件對象
  • event source :事件源,產生事件的地方
  • event listener :監聽事件並處理

ApplicationListener 繼承自 java.util.EventListener ,提供了對於Spring中事件機制的擴展。github

ApplicationListener 在實際的業務場景中使用的很是多,好比我通常喜歡在容器初始化完成以後來作一些資源載入或者一些組件的初始化。這裏的容器指的就是Ioc容器,對應的事件是ContextRefreshedEventspring

@Component
public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
       //初始化資源文件
       //初始化組件 如:cache
    }
}
複製代碼

上面這段代碼會在容器刷新完成以後來作一些事情。下面經過自定義事件來看看怎麼使用,在看具體的demo以前,先來了解下一些關注點。bash

平常工做了,若是要使用 Spring 事件傳播機制,咱們須要關注的點有如下幾點:app

  • 事件類,這個用來描述事件自己一些屬性,通常繼承ApplicationEvent
  • 監聽類,用來監聽具體的事件並做出響應。須要實現 ApplicationListener 接口
  • 事件發佈類,須要經過這個類將時間發佈出去,這樣才能被監聽者監聽到,須要實現ApplicationContextAware接口。
  • 將事件類和監聽類交給Spring容器。

那麼下面就按照這個思路來看下demo的具體實現。ide

事件類:UserRegisterEvent

UserRegisterEvent ,用戶註冊事件;這裏做爲事件對象,繼承自 ApplicationEventthis

/** * @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具備事件發佈能力。spa

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 implements ApplicationListener<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 implements ApplicationListener<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 implements ApplicationContextAware {

    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過來。

相關文章
相關標籤/搜索