干货点

通过阅读该篇博客,你可以了解了解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 事件处理器 */@Componentpublic 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);  }}
步骤四:

对应的业务类的实现如下:

/** * @author xifanxiaxue * @date 3/31/19 * @desc */@Slf4j@Servicepublic 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@Slf4jpublic class BFuncService implements IBFuncService {  @ReceiveAnno(clz = LoginEvent.class)  private void doAfterLogin() {    log.info("[{}]监听到登录事件 ... ", this.getClass());  }}
/** * @author xifanxiaxue * @date 3/31/19 * @desc */@Service@Slf4jpublic 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)@SpringBootTestpublic class EventMechanismApplicationTests {  @Autowired  private AFuncService aFuncService;  @Test  public void contextLoads() {    aFuncService.login();  }}

可以从中看出启动该测试类后,会调用业务A的login函数,而我们要的效果是B业务类和C业务类的doAfterLogin函数会被自动触发,那么结果如何呢?

结果打印

我们可以从结果打印中看到,在业务类A的login函数触发后,业务类B和业务类C都监听到了监听到登录事件,证明该机制正常解决了单对多的行为触发问题。