共计 4017 个字符,预计需要花费 11 分钟才能阅读完成。
干货点
通过阅读该篇博客,你可以了解了解 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); | |
} | |
} |
步骤四:
对应的业务类的实现如下:
/** | |
* @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 函数会被自动触发,那么结果如何呢?
结果打印
我们可以从结果打印中看到,在业务类 A 的 login 函数触发后,业务类 B 和业务类 C 都监听到了监听到登录事件,证明该机制正常解决了单对多的行为触发问题。