Recently, when we used Spring event framework to write business development, due to the functional limitations of Spring event framework, we could not well meet the development requirements. After reading the Spring event source code, I specifically extended the Spring event framework, a process detailed below.

Question brief:

System A needs to monitor many events, such as A event, B event and C event, for similar business processing.Copy the code

General Solution 1:

Write many listeners in A system, respectively monitor A, B, C events, and processing! Such as:

public class SystemListener {
    @EventListener
    public void listenAEvent(AEvent event) {
        // Handle event A
    }
    @EventListener
    public void listenBEvent(BEvent event) {
        // Handle the B event
    }
    @EventListener
    public void listenCEvent(CEvent event) {
        // Handle the C event}}Copy the code

This approach may seem simple and clear, but it has a fatal flaw. When there are many related events (more than 300), many similar listening methods need to be written, which makes the code very readable and maintainable.

General Solution 2:

System A provides an event base class on which multiple events A, B, and C must base. This allows A listener method to listen for multiple events A, B, and C at the same time. Such as:

    @EventListener
    public void listenEvent(BaseEvent event) {
        // Handle events
        System.out.println("Handle the event," + event.getClass());
    }

Copy the code

It is possible to use a single listener to listen for multiple events at the same time. But the actual landing is a big problem, because it is not feasible for system A to force events thrown by other modules to inherit events of its own base class. Not only does this result in strong coupling, but it also makes it impossible for these events to inherit from other base classes, which is unrealistic. Why can’t the system just listen for ApplicationEvent? Yes, but this makes system A listen to a lot of events that it does not want to pay attention to, which consumes a lot of calculation and is obviously unreasonable.

General Solution 3:

The @EventListener annotation identifies multiple events to listen for, for example:

@EventListener({AEvent.class, BEvent.class, CEvent.class})
public void listenEvent(a) {
    System.out.println("Handle the event," + event.getClass());
}

Copy the code

It worked, but it had two fatal flaws:

● Method can not take parameters, need event data is not feasible

● There are too many events to enumerate

General Solution 4:

We can also specify our listening condition using the condition field of @EventListener, whose value is expressed as SPEL.

@EventListener(condition = "")
public void listenEvent(a) {
    System.out.println("Handle the event," );
}
Copy the code

After testing, this method is feasible, but the drawback is that SPEL is not flexible enough to express in SPEL mode when faced with complex business logic judgments.

Framework development ideas:

Design objectives:

● Listening conditions can be inserted custom logic, flexible judgment according to business content

● Extension plug-ins can be opened in configuration mode

Effect preview:

@EventListener
@ListenerStrategy(strategy = TestEventListenerStrategy.class)
public void listenEvent(BaseEvent baseEvent) {
    System.out.println("Handle the event," );
}
Copy the code

As shown above, we simply add a @ListenerStrategy to the listener method, along with our custom policy implementation, and have the flexibility to inject our own listening logic. @listenerStrategy is defined as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
@Documented
public @interface ListenerStrategy {
    Class<? extends EventListenerStrategy> strategy();
}

Copy the code

The policy interface is defined as follows:

/** * listen to the policy interface. Developers need to customize the policy. You can provide this interface implementation and inject it into Spring */
public interface EventListenerStrategy {

    /** * Determine whether this event complies with the listening policy *@param event
     * @return* /
    boolean match(Class<? extends ApplicationEvent> event);
}

Copy the code

We tried to customize the policy to listen only on AEvent and its subclasses:

@Component
public class TestEventListenerStrategy implements EventListenerStrategy {
    @Override
    public boolean match(Class<? extends ApplicationEvent> event){
        returnAEvent.class.isAssignableFrom(event); }}Copy the code

Effect:

The unit tests met our expectations.

Implementation idea:

➢ replace container ApplicationEventMulticaster instance by default

Through the Spring source, we found that the main by ApplicationEventMulticaster implementation class event publishing.

Look closely, the realization of the Spring is the famous if the container for applicationEventMulticaster component, use the components in the container, otherwise will instantiate a SimpleApplicationEventMulticaster used by default. The specific code is as follows:

Since then, we define a ApplicationEventMulticaster implementation, rewrite the listening in matching method, and into the container, it can open our expanding component functions. The code implementation is as follows:

/** * custom event dispatcher */ for implementing policy listening
public class StrategyApplicationEventMulticaster extends SimpleApplicationEventMulticaster implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    protected boolean supportsEvent(ApplicationListener
        listener, ResolvableType eventType, Class
        sourceType) {
        boolean superJudge = super.supportsEvent(listener, eventType, sourceType);
        if(! superJudge) {return false;
        }
        // Check whether policy listening is enabled. If yes, handle the fault
        if (listener instanceof ApplicationListenerMethodAdapter) {
            Field field = ReflectionUtils.findField(ApplicationListenerMethodAdapter.class, "method");
            if (field == null) {
                throw new IllegalStateException("handle event error, can not find invokeMethod of listener");
            }
            ReflectionUtils.makeAccessible(field);
            Method originMethod = (Method) ReflectionUtils.getField(field, listener);
            field.setAccessible(false);
            ListenerStrategy annotation = originMethod.getAnnotation(ListenerStrategy.class);
            // If there is no policy listening, no matter
            if (annotation == null) {
                return true; } Class<? extends EventListenerStrategy> strategyType = annotation.strategy(); EventListenerStrategy listenerStrategy = applicationContext.getBean(strategyType); Class<? > event = eventType.resolve(); Class<? extends ApplicationEvent> applicationEvent = (Class<? extends ApplicationEvent>) event;// Do not listen if the listening policy is not met
            if(! listenerStrategy.match(applicationEvent)) {return false; }}return true; }}Copy the code

Override the supportsEvent method above to change to the event matching logic we want. It takes effect by injecting it into the Spring container. Of course, the above implementation is flawed in that this method will be called repeatedly while detecting events and will have low performance with reflection. In this case, we can do a pre-processing cache. The code implementation is as follows:

/** * custom event dispatcher */ for implementing policy listening
public class StrategyApplicationEventMulticaster extends SimpleApplicationEventMulticaster implements ApplicationContextAware.SmartInitializingSingleton {

    private ApplicationContext applicationContext;

    private ConcurrentHashMap<ApplicationListener, ListenerStrategy> strategyMap= new ConcurrentHashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    protected boolean supportsEvent(ApplicationListener
        listener, ResolvableType eventType, Class
        sourceType) {
        boolean superJudge = super.supportsEvent(listener, eventType, sourceType);
        if(! superJudge) {return false;
        }
        // Check whether policy listening is enabled. If yes, handle the fault
        if (listener instanceof ApplicationListenerMethodAdapter) {
            ListenerStrategy annotation = strategyMap.get(listener);
            if (annotation == null) {
                return true;
            }
            Class<? extends EventListenerStrategy> strategyType = annotation.strategy();
            EventListenerStrategy listenerStrategy = applicationContext.getBean(strategyType);
            if (listenerStrategy == null) {
                throw new IllegalStateException("no such eventListenerStrategy instance in applicationContext container; "+ strategyType.getName()); } Class<? > event = eventType.resolve(); Class<? extends ApplicationEvent> applicationEvent = (Class<? extends ApplicationEvent>) event;// Do not listen if the listening policy is not met
            if(! listenerStrategy.match(applicationEvent)) {return false; }}return true;
    }

    @Override
    public void afterSingletonsInstantiated(a) {
        for(ApplicationListener<? > listener : getApplicationListeners()) {if (listener instanceof ApplicationListenerMethodAdapter) {
                Field field = ReflectionUtils.findField(ApplicationListenerMethodAdapter.class, "method");
                if (field == null) {
                    throw new IllegalStateException("handle event error, can not find invokeMethod of listener");
                }
                ReflectionUtils.makeAccessible(field);
                Method originMethod = (Method) ReflectionUtils.getField(field, listener);
                field.setAccessible(false);
                ListenerStrategy annotation = originMethod.getAnnotation(ListenerStrategy.class);
                // If there is no policy listening, no matter
                if (annotation == null) {
                    continue; } strategyMap.putIfAbsent(listener, annotation); }}}}Copy the code

The above content is original, quote please indicate the source! Welcome to criticize and correct any inaccuracies!