Spring event framework extension - custom policy listening

Posted by Traveller29 on Sat, 19 Feb 2022 09:09:09 +0100

Recently, when using Spring event framework to write business development, due to the functional limitations of Spring event framework, it can not well meet our development needs. Therefore, after reading the Spring event source code, the Spring event framework has been expanded. The process will be described in detail below.

Problem Description:

   System a needs to monitor many events, such as A event, B event, C Event and perform similar business processing on it.
Copy code

General solution 1:

Write many listeners in system A to listen to and process events A, B and C respectively! For example:

public class SystemListener {
    @EventListener
    public void listenAEvent(AEvent event) {
        //Handling A events
    }
    @EventListener
    public void listenBEvent(BEvent event) {
        //Handling B events
    }
    @EventListener
    public void listenCEvent(CEvent event) {
        //Handling C events
    }
}

Copy code

This method seems simple and clear, but it actually has a fatal defect. When there are many related events (more than 300), it needs to write many similar monitoring methods, and the readability and maintainability of the code will be particularly poor.

General solution 2:

Provide an event base class in system A. multiple events a, B and C must be based on this event base class. In this way, one monitoring method can monitor multiple events of a, B and C at the same time. For example:

    @EventListener
    public void listenEvent(BaseEvent event) {
        //Handling events
        System.out.println("handle event: " + event.getClass());
    }

Copy code

After testing, this method can monitor multiple events at the same time. However, the actual landing problem is very big, because it is not feasible for system a to force the events thrown by other modules to inherit the base class events provided by itself. It not only causes strong coupling, but also makes these events unable to inherit other base classes, which is unrealistic. Someone asked, why can't system a directly monitor ApplicationEvent??? It's true, but it's obviously unreasonable for system a to monitor many events that it doesn't want to pay attention to.

General solution 3:

@The EventListener annotation identifies multiple events for event listening, for example:

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

Copy code

After testing, this method is feasible, but it has the following two fatal defects:

● the method cannot take parameters, which is not feasible when event data is required

● there are too many events, and it's too terrible to lift it up

General solution 4:

We can also use the condition field of @ EventListener to specify our listening condition, and the value of condition is expressed in SPEL.

@EventListener(condition = "")
public void listenEvent() {
    System.out.println("handle event: " );
}
Copy code

After testing, this method is feasible, but the defect is that SPEL is not flexible enough and cannot be expressed in SPEL in the face of complex business logic judgment.

Framework expansion ideas:

Design objectives:

● user defined logic can be inserted into the monitoring conditions, which can be judged flexibly according to the business content

● the expansion plug-in can be opened in configuration mode

Effect preview:

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

As shown above, we only need to add a @ ListenerStrategy to the monitoring method and our customized policy implementation to flexibly inject our own monitoring logic@ Listener strategy is defined as follows:

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

Copy code

The policy interface is defined as follows:

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

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

Copy code

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

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

Copy code

effect:

The unit test fully met our expectations.

Realization idea:

Replace the default applicationeventmulticast instance in the container

Through the Spring source code, we find that event publishing is mainly carried out by the implementation class of applicationeventmulticast.

Look carefully. The implementation of Spring is to use the components in the container if they are called applicationeventmulticast. Otherwise, a simpleapplicationeventmulticast will be instantiated by default. The specific codes are as follows:

Therefore, we can customize the implementation of an applicationeventmulticast, rewrite the listening and matching method, and inject it into the container to open our extended component function. The code implementation is as follows:

/**
 * Custom event distributor for 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;
        }
        //Judge whether policy listening is enabled. If it is enabled, it needs to be processed
        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 monitoring, it doesn't 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;
            //If it does not comply with the monitoring policy, it will not be monitored
            if (!listenerStrategy.match(applicationEvent)) {
                return false;
            }
        }
        return true;
    }
}

Copy code

Rewrite the supportsEvent method above to change the event matching logic we want. It takes effect by injecting it into the Spring container. Of course, the above implementation has some defects. This method will be called repeatedly when detecting events, and the performance of reflection is low. In this case, we can do a preprocessing cache. The code implementation is as follows:

/**
 * Custom event distributor for 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;
        }
        //Judge whether policy listening is enabled. If it is enabled, it needs to be processed
        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;
            //If it does not comply with the monitoring policy, it will not be monitored
            if (!listenerStrategy.match(applicationEvent)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void afterSingletonsInstantiated() {
        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 monitoring, it doesn't matter
                if (annotation == null) {
                    continue;
                }
                strategyMap.putIfAbsent(listener, annotation);
            }
        }
    }
}


Copy code

Please indicate the source of the above content! Students are welcome to criticize and correct the inaccuracies!


Author: Xiao Chao 6379
Link: https://juejin.cn/post/6955783021224001567
Source: Nuggets
 

Topics: Java Spring Spring Boot Design Pattern AOP