單機環境下優雅地使用事件驅動進行程式碼解耦

JavaDoop發表於2019-02-14

雖然現在的各種應用都是叢集部署,單機部署的應用越來越少了,但是不可否認的是,市場上還是存在許多單機應用的。本文要介紹的是 Guava 中的 EventBus 的使用。

EventBus 處理的事情類似觀察者模式,基於事件驅動,觀察者們監聽自己感興趣的特定事件,進行相應的處理。

本文想要介紹的內容是,在 Spring 環境中優雅地使用 Guava 包中的 EventBus,對我們的程式碼進行一定程度的解耦。當然,本文不介紹 EventBus 的原理,我所說的優雅也只是我覺得優雅,也許讀者有更漂亮的程式碼,歡迎在評論區留言。

Step 0:新增 Guava 依賴

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>22.0</version>
</dependency>
複製程式碼

作為 java 程式設計師,如果你還沒有使用過 Google Guava,請從現在開始將它加到你的每一個專案中。

Step 1:定義一個註解用於標記 listener

/**
 * 用於標記 listener
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBusListener {
}
複製程式碼

Step 2:定義註冊中心

package com.javadoop.eventbus;

import java.util.List;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.hongjiev.javadoop.util.SpringContextUtils;

@Component
public class EventBusCenter {

    // 管理同步事件
    private EventBus syncEventBus = new EventBus();

    // 管理非同步事件
    private AsyncEventBus asyncEventBus = new AsyncEventBus(Executors.newCachedThreadPool());

    public void postSync(Object event) {
        syncEventBus.post(event);
    }

    public void postAsync(Object event) {
        asyncEventBus.post(event);
    }

    @PostConstruct
    public void init() {

        // 獲取所有帶有 @EventBusListener 的 bean,將他們註冊為監聽者
        List<Object> listeners = SpringContextUtils.getBeansWithAnnotation(EventBusListener.class);
        for (Object listener : listeners) {
            asyncEventBus.register(listener);
            syncEventBus.register(listener);
        }
    }
}
複製程式碼

Step 3:定義各種事件

舉個例子,我們定義一個訂單建立事件:

package com.javadoop.eventbus.event;

public class OrderCreatedEvent {
    private long orderId;
    private long userId;
    public OrderCreatedEvent(long orderId, long userId) {
        this.setOrderId(orderId);
        this.setUserId(userId);
    }
    // getter、setter
}
複製程式碼

Step 4:定義事件監聽器

首先,類上面需要加我們之前定義的註解:@EventBusListener,然後監聽方法需要加註解 @Subscribe,方法引數為具體事件。

package com.javadoop.eventbus.listener;

import org.springframework.stereotype.Component;
import com.google.common.eventbus.Subscribe;
import com.javadoop.eventbus.EventBusListener;
import com.javadoop.eventbus.event.OrderCreatedEvent;

@Component
@EventBusListener
public class OrderChangeListener {

    @Subscribe
    public void created(OrderCreatedEvent event) {
        long orderId = event.getOrderId();
        long userId = event.getUserId();
        // 訂單建立成功後的各種操作,如發簡訊、發郵件等等。
        // 注意,事件可以被訂閱多次,也就是說可以有很多方法監聽 OrderCreatedEvent 事件,
        // 所以沒必要在一個方法中處理髮簡訊、發郵件、更新庫存等
    }

    @Subscribe
    public void change(OrderChangeEvent event) {
        // 處理訂單變化後的修改
        // 如傳送提醒、更新物流等
    }
}
複製程式碼

Step 5:傳送事件

@Service
public class OrderService {

    @Autowired
    private EventBusCenter eventBusCenter;

    public void createOrder() {
        // 處理建立訂單
        // ...
        // 傳送非同步事件
        eventBusCenter.postAsync(new OrderCreatedEvent(1L, 1L));
    }
}
複製程式碼

總結

EventBus 的好處在於,它將發生事件的程式碼和事件處理的程式碼進行了解耦。

比如系統中很多地方都會修改訂單,使用者可以自己修改、客服也可以修改、甚至可能是團購沒成團系統進行的訂單修改,所有這些觸發訂單修改的地方都要發簡訊、發郵件,假設以後還要增加其他操作,那麼需要修改的地方就比較多。

而如果採用事件驅動的話,只要這些地方丟擲事件就可以了,後續的維護是比較簡單的。

而且,EventBus 支援同步事件和非同步事件,可以滿足我們不同場景下的需求。比如發簡訊,系統完全沒必要等在那邊,完全是可以非同步做的。

附錄:SpringContextUtils

上面的程式碼使用到了 SpringContextUtils,我想大部分的 Spring 應用都會寫這麼一個工具類來從 Spring 容器中獲取 Bean,用於一些不方便採用注入的地方。

@Component
public class SpringContextUtils implements BeanFactoryPostProcessor {

    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        SpringContextUtils.beanFactory = configurableListableBeanFactory;
    }

    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = beanFactory.getBean(clz);
        return result;
    }

    public static <T> List<T> getBeansOfType(Class<T> type) {
        return beanFactory.getBeansOfType(type).entrySet().stream().map(entry->entry.getValue()).collect(Collectors.toList());
    }

    // 上面的例子用到了這個
    public static List<Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType) {
        Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(annotationType);

        // java 8 的寫法,將 map 的 value 收集起來到一個 list 中
        return beansWithAnnotation.entrySet().stream().map(entry->entry.getValue()).collect(Collectors.toList());

        // java 7
        List<Object> result = new ArrayList<>();
        for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
            result.add(entry.getValue());
        }
        return result;
    }
}
複製程式碼

(全文完)

相關文章