Late night overtime boring, imitating an EventBus based on reflection

Posted by rross46 on Mon, 29 Jun 2020 18:57:16 +0200

Dry point

By reading this blog, you can learn about java's reflection mechanism and how to use custom annotations to solve everyday R&D problems based on the spring lifecycle.

Problem Description

In daily R&D, it is common to encounter an action triggered by Business A and at the same time trigger the action of Business B. This form of one-to-one can directly invoke the action of Business B after the action of Business A is executed, so what if it is one-to-many?

Solution Solution

This provides a mechanism that is often used in daily R&D, event-driven based on spring implementation, i.e. when an action of Business A is executed, an event is thrown, and Business B, C, D, etc. listen for the event and post-process the corresponding business.

Scene examples

Here is an example of a scenario based on the springboot Shell project implementation. See the source code specifically Here, only the key steps are combed.

Step 1:

Define a comment that flags the event received, that is, all functions that use the comment will be called when the corresponding event is thrown. The comment implementation is simple and the code is as follows

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc Notes for Receiving Events
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ReceiveAnno {

  // Listened Events
  Class clz();
}

Define event interfaces

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

All events need to implement this interface, primarily for later generics and type recognition.

Define 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;
  }
}

This class is just an encapsulation of Object and Method and has no other effect.

Step 2:

Implement an event container that stores events and the corresponding relationships of method s for each business that needs to be triggered.

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc Event Container
 */
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();
      }
    }
  }
}

The addEventToMap function stores the Methods in the corresponding business that need to be triggered after the event is triggered in the eventListMap, while the submit function is called when an event is thrown in another business class, and the function is to take the corresponding Method from the eventListMap and trigger it through reflection.

Step 3:

Implements an event handler that determines if the corresponding bean has a function annotated with @ReceiveAnno after it has been instantiated by the spring container and, if so, removes the corresponding Event from it and places it in the EventContainer.

/**
 * @author xifanxiaxue
 * @date 3/31/19
 * @desc Event Processor
 */
@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("{}Not implemented IEvent Interface", clz);
            throw new RuntimeException(message.getMessage());
          }
        } catch (InstantiationException e) {
          e.printStackTrace();
        }

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

    return super.postProcessAfterInstantiation(bean, beanName);
  }

}
Step 4:

The corresponding business class is implemented as follows:

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

  @Override
  public void login() {
    log.info("[{}]Throw logon event ... ", this.getClass());
    EventContainer.submit(LoginEvent.class);
  }
}

A business class, login throws a LoginEvent event in the invoked life.

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

  @ReceiveAnno(clz = LoginEvent.class)
  private void doAfterLogin() {
    log.info("[{}]Listen for logon events ... ", 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("[{}]Listen for logon events ... ", this.getClass());
  }
}

doAfterLogin for business classes B and C is annotated with @ReceiveAnno (clz =LoginEvent.class), triggered after listening for the event LoginEvent.

For triggering convenience, I added an implementation to the test class provided by spring, with the following code:

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

  @Autowired
  private AFuncService aFuncService;

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

}

You can see from this that when the test class is started, the login function of Business A is called, and the effect we want is that the doAfterLogin functions of Business Class B and Business Class C are automatically triggered, so what is the result?

Result Printing

We can see from the result print that after triggering the login function of business class A, business class B and business class C both listen for login events, proving that this mechanism correctly solves the one-to-many behavior triggering problem.

Topics: Java Spring SpringBoot shell