基於spring實現事件驅動

稀飯下雪發表於2019-04-07

乾貨點

通過閱讀該篇部落格,你可以瞭解瞭解java的反射機制、可以瞭解如何基於spring生命週期使用自定義註解解決日常研發問題。具體原始碼可以點選連結

問題描述

在日常研發中,經常會遇見業務A的某個action被觸發後,同時觸發業務B的action的行為,這種單對單的形式可以直接在業務A的action執行結束後直接呼叫業務B的action,那麼如果是單對多的情況呢?

方案解決

這裡提供一種在日常研發中經常使用到的機制,基於spring實現的事件驅動,即在業務A的action執行完,丟擲一個事件,而業務B、C、D等監聽到該事件後處理相應的業務。

場景範例

這裡提供一個場景範例,該範例基於springboot空殼專案實現,具體可以檢視原始碼,此處只梳理關鍵步驟。

步驟一:

定義一個註解,標誌接收事件的註解,即所有使用了該註解的函式都會在對應事件被丟擲的時候被呼叫,該註解實現比較簡單,程式碼如下

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc 接收事件的註解
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ReceiveAnno {

  // 監聽的事件
  Class clz();
}
複製程式碼

如果想了解註解多個引數的意義是什麼的可以點選連結檢視博主之前寫過文章。

定義事件介面

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc
 */
public interface IEvent {
}

複製程式碼

所有事件都需要實現該介面,主要是為了後面泛型和型別識別。

定義MethodInfo

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc
 */
public class MethodInfo {

  public Object obj;
  public Method method;

  public static MethodInfo valueOf(Method method, Object obj) {

    MethodInfo info = new MethodInfo();
    info.method = method;
    info.obj = obj;
    return info;
  }

  public Object getObj() {
    return obj;
  }

  public Method getMethod() {
    return method;
  }
}

複製程式碼

該類只是做了Object和Method的封裝,沒有其他作用。

步驟二:

實現一個事件容器,該容器的作用是存放各個事件以及需要觸發的各個業務的method的對應關係。

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc 事件容器
 */
public class EventContainer {

  private static Map<Class<IEvent>, List<MethodInfo>> eventListMap = new HashMap<>();

  public static void addEventToMap(Class clz, Method method, Object obj) {

    List<MethodInfo> methodInfos = eventListMap.get(clz);
    if (methodInfos == null) {
      methodInfos = new ArrayList<>();
      eventListMap.put(clz, methodInfos);
    }

    methodInfos.add(MethodInfo.valueOf(method, obj));
  }

  public static void submit(Class clz) {

    List<MethodInfo> methodInfos = eventListMap.get(clz);
    if (methodInfos == null) {
      return;
    }

    for (MethodInfo methodInfo : methodInfos) {
      Method method = methodInfo.getMethod();
      try {
        method.setAccessible(true);
        method.invoke(methodInfo.getObj());
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    }
  }
}

複製程式碼

其中的addEventToMap函式的作用是將對應的事件、事件觸發後需要觸發的對應業務內的Method存放在eventListMap內;而submit函式會在其他業務類內丟擲事件的時候被呼叫,而作用是從eventListMap中取出對應的Method,並通過反射觸發。

步驟三:

實現事件處理器,該事件處理器的作用是在bean被spring容器例項化後去判斷對應的bean是否有相應函式加了@ReceiveAnno註解,如果有則從中取出對應的Event並放入EventContainer中。

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc 事件處理器
 */
@Component
public class EventProcessor extends InstantiationAwareBeanPostProcessorAdapter {

  @Override
  public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {

    ReflectionUtils.doWithLocalMethods(bean.getClass(), new ReflectionUtils.MethodCallback() {
      @Override
      public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {

        ReceiveAnno anno = method.getAnnotation(ReceiveAnno.class);
        if (anno == null) {
          return;
        }

        Class clz = anno.clz();
        try {
          if (!IEvent.class.isInstance(clz.newInstance())) {
            FormattingTuple message = MessageFormatter.format("{}沒有實現IEvent介面", clz);
            throw new RuntimeException(message.getMessage());
          }
        } catch (InstantiationException e) {
          e.printStackTrace();
        }

        EventContainer.addEventToMap(clz, method, bean);
      }
    });

    return super.postProcessAfterInstantiation(bean, beanName);
  }

}

複製程式碼

關於InstantiationAwareBeanPostProcessorAdapter的描述,有需要的可以檢視我之前的文章,其中比較詳細描述到Spring中的InstantiationAwareBeanPostProcessor類的作用。

步驟四:

對應的業務類的實現如下:

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc
 */
@Slf4j
@Service
public class AFuncService implements IAFuncService {

  @Override
  public void login() {
    log.info("[{}]丟擲登入事件 ... ", this.getClass());
    EventContainer.submit(LoginEvent.class);
  }
}

複製程式碼

A業務類,login會在被呼叫的生活丟擲LoginEvent事件。

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc
 */
@Service
@Slf4j
public class BFuncService implements IBFuncService {

  @ReceiveAnno(clz = LoginEvent.class)
  private void doAfterLogin() {
    log.info("[{}]監聽到登入事件 ... ", this.getClass());
  }
}

複製程式碼
/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc
 */
@Service
@Slf4j
public class CFuncService implements ICFuncService {

  @ReceiveAnno(clz = LoginEvent.class)
  private void doAfterLogin() {
    log.info("[{}]監聽到登入事件 ... ", this.getClass());
  }
}

複製程式碼

B和C業務類的doAfterLogin都分別加了註解 @ReceiveAnno(clz = LoginEvent.class) ,在監聽到事件LoginEvent後被觸發。

為了觸發方便,我在spring提供的測試類內加了實現,程式碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EventMechanismApplicationTests {

  @Autowired
  private AFuncService aFuncService;

  @Test
  public void contextLoads() {
    aFuncService.login();
  }

}

複製程式碼

可以從中看出啟動該測試類後,會呼叫業務A的login函式,而我們要的效果是B業務類和C業務類的doAfterLogin函式會被自動觸發,那麼結果如何呢?

結果列印

結果輸出.png

我們可以從結果列印中看到,在業務類A的login函式觸發後,業務類B和業務類C都監聽到了監聽到登入事件,證明該機制正常解決了單對多的行為觸發問題。


系列部落格可以關注公眾號:

公眾號.jpg

個人網站:myblog.lixifan.cn/

相關文章