Decoupling tool between modules (classes): EventPublishSubscribeUtils

If you are familiar with C ා language, you will generally know the benefits of delegation and event. You only need to define the open delegation or event (special representation of delegation) variables in advance in one class, and then you can subscribe to the delegation or event at will in other classes. When the delegation or event is triggered for execution, all subscribers will be notified automatically for consumption processing. (the observer pattern is best implemented by delegation, and so is the basic idea of event driven by DDD). Of course, what I think here is that instead of defining delegates or events in each class, a unified mediator (EventPublishSubscribeUtils) provides event subscription and publishing operations, so there is no need to directly rely on each module , you only need to complete the publish notification and subscription callback through the intermediary. Why not?

Here I use the unique delegation type of C ා language to quickly implement a simple EventPublishSubscribeUtils. The code is as follows:

    /// <summary>
    ///Custom event publish / subscribe callback tool class (business decoupling, separation of concerns, avoiding interdependence) - Demo
    ///EventBus simplified, observer mode
    /// author:zuowenjun
    /// </summary>
    public static class EventPublishSubscribeUtils
    {
        private static ConcurrentDictionary<Type, EventHandler<object>> EventHandlers { get; } = new ConcurrentDictionary<Type, EventHandler<object>>();

        private static void removeRegisters(ref EventHandler<object> srcEvents, EventHandler<object> removeTargetEvents)
        {
            var evtTypes = removeTargetEvents.GetInvocationList().Select(d => d.GetType());
            var registeredEventHandlers = Delegate.Combine(srcEvents.GetInvocationList().Where(ei => evtTypes.Contains(ei.GetType())).ToArray());
            srcEvents -= (EventHandler<object>)registeredEventHandlers;
        }

        public static void Register<T>(EventHandler<object> eventHandlers)
        {
            EventHandlers.AddOrUpdate(typeof(T), eventHandlers,
                (t, e) =>
                {
                    //According to the matching of subscription delegate types, filter out the existing same subscription, and then re subscribe to prevent repeated subscriptions and multiple executions.
                    removeRegisters(ref e, eventHandlers);
                    e += eventHandlers;
                    return e;
                });
        }


        public static void UnRegister<T>(EventHandler<object> eventHandlers = null)
        {
            Type eventMsgType = typeof(T);
            if (eventHandlers == null)
            {
                EventHandlers.TryRemove(eventMsgType, out eventHandlers);
                return;
            }

            var e = EventHandlers[eventMsgType];
            removeRegisters(ref e, eventHandlers);
        }

        public static void PublishEvent<T>(T eventMsg, object sender)
        {
            Type eventMsgType = eventMsg.GetType();
            if (EventHandlers.ContainsKey(eventMsgType))
            {
                EventHandlers[eventMsgType].Invoke(sender, eventMsg);
            }
        }
    }

Then it's easier to use. We just need to register and subscribe to event messages through EventPublishSubscribeUtils.Register, and publish event notifications through EventPublishSubscribeUtils.PublishEvent, so that two or more unrelated modules (classes) can realize 1-to-many communication and collaborative processing through message types. The use example code is as follows:

    class EventMessage
    {
        public string Name { get; set; }

        public string Msg { get; set; }

        public DateTime CreatedDate { get; set; }
    }

    class DemoA
    {
        public DemoA()
        {
            EventHandler<object> eventHandlers = EventCallback1;
            eventHandlers += EventCallback2;

            EventPublishSubscribeUtils.Register<EventMessage>(eventHandlers);
        }

        private void EventCallback1(object sender, object e)
        {
            string json = JsonConvert.SerializeObject(e);
            System.Diagnostics.Debug.WriteLine($"EventCallback1=> sender:{sender},e:{json}");
        }

        private void EventCallback2(object sender, object e)
        {
            string json = JsonConvert.SerializeObject(e);
            System.Diagnostics.Debug.WriteLine($"EventCallback2=> sender:{sender},e:{json}");
        }

    }

    class DemoB
    {
        public void ShowMsg(string name, string msg)
        {
            System.Diagnostics.Debug.WriteLine($"ShowMsg=> name:{name},msg:{msg}");
            var eventMsg = new EventMessage
            {
                Name = name,
                Msg = msg,
                CreatedDate = DateTime.Now
            };
            EventPublishSubscribeUtils.PublishEvent(eventMsg, nameof(DemoB.ShowMsg));
        }
    }

//The main method uses:
var demoA = new DemoA();
var demoB = new DemoB();

demoB.ShowMsg("Dream on the journey", "i love csharp and java!");

From the above example code, we can see that DemoA and DemoB are independent and independent. They do not know the existence of each other. They only care about the processing of the business, and trigger the callback demoA.EventCallback1 by executing the demoB.ShowMsg method. Is the demoA.EventCallback2 method better than DemoB directly from DemoA?

c ා has delegate type (reference of method), so how to implement it in java?

In fact, in the same way, we can achieve the same effect as C ා with the help of anonymous internal class + anonymous implementation class (for example, functional interface), and can also achieve similar event publishing and subscription functions. The following is the code of EventPublishSubscribeUtils class implemented in java language:

Due to the needs of the project, I purposely implemented two modes, one supporting 1-to-many common mode, the other supporting 1-to-1 subscription callback mode, with return value.

/**
 * Custom event publish / subscribe callback tool class (business decoupling, separation of concerns, avoiding interdependence)
 * EventBus Simplified version, observer mode
 * <pre>
 * Two modes are supported
 * 1.No return value: subscription event consumption (register) + publishing event message (publishEvent/publishEventAsync)
 * 2.There are return values: listening callback + notifyCallback. Through notifyMessageType+MessageChannel, you can identify a unique group of notification callbacks and listening callback processing
 * <pre>
 * @author zuowenjun
 * @date 20200310
 */
public final class EventPublishSubscribeUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(EventPublishSubscribeUtils.class);

    private static final Map<Class<?>, LinkedList<Consumer<Object>>> eventConsumers = new ConcurrentHashMap<>();

    private static final Map<Class<?>, ConcurrentHashMap<MessageChannel, Function<Object, Object>>> callbackFuncs = new ConcurrentHashMap<>();

    private EventPublishSubscribeUtils() {
    }


    /**
     * Register event callback consumer
     * Usage: eventsubscribeconsumerutils.register (this:: XXXX method) or lambda expression
     * Note: if the callback method adds transaction annotation, the method of its proxy object should be assigned to complete the callback, such as:
     * EventSubscribeConsumeUtils.register((xxxService)SpringUtils.getBean(this.class)::xxxx Method)
     *
     * @param eventConsumer
     */
    public static void register(Class<?> eventMessageType, Consumer<Object> eventConsumer) {

        if (eventConsumer == null) {
            return;
        }

        LinkedList<Consumer<Object>> eventConsumerItems = null;
        if (!eventConsumers.containsKey(eventMessageType)) {
            eventConsumers.putIfAbsent(eventMessageType, new LinkedList<>());
        }
        eventConsumerItems = eventConsumers.get(eventMessageType);

        eventConsumerItems.add(eventConsumer);
    }

    /**
     * Unsubscribe callback
     *
     * @param eventMessageType
     * @param eventConsumer
     */
    public static void unRegister(Class<?> eventMessageType, Consumer<Object> eventConsumer) {
        if (!eventConsumers.containsKey(eventMessageType)) {
            return;
        }

        LinkedList<Consumer<Object>> eventConsumerItems = eventConsumers.get(eventMessageType);
        int eventConsumerIndex = eventConsumerItems.indexOf(eventConsumer);
        if (eventConsumerIndex == -1) {
            return;
        }
        eventConsumerItems.remove(eventConsumerIndex);
    }


    /**
     * Publish event, synchronously trigger and execute callback event consumer method (with blocking waiting), i.e. event message producer
     * Usage: called when the event message callback needs to be triggered, such as: publishEvent(eventMessage);
     *
     * @param eventMessage
     */
    public static <T> void publishEvent(T eventMessage) {
        Class<?> eventMessageType = eventMessage.getClass();

        if (!eventConsumers.containsKey(eventMessageType)) {
            return;
        }

        LOGGER.info("Event published, notification consumption in progress:{}", JSONObject.toJSONString(eventMessage));

        for (Consumer<Object> eventConsumer : eventConsumers.get(eventMessageType)) {
            try {
                eventConsumer.accept(eventMessage);
            } catch (Exception ex) {
                LOGGER.error("eventConsumer.accept error:{},eventMessageType:{},eventMessage:{}",
                        ex, eventMessageType, JSONObject.toJSONString(eventMessage));
            }
        }
    }


    /**
     * Publish event, asynchronously trigger and execute callback event consumer method (asynchronously non blocking), i.e. event message producer
     * Usage: called when the event message callback needs to be triggered, such as: publishEventAsync(eventMessage);
     *
     * @param eventMessage
     */
    public static <T> void publishEventAsync(final T eventMessage) {
        Executor asyncTaskExecutor = (Executor) SpringUtils.getBean("asyncTaskExecutor");
        asyncTaskExecutor.execute(() -> {
            publishEvent(eventMessage);
        });
    }


    /**
     * Listen for callback processing (return value is required), that is, callback consumers with return value
     *
     * @param notifyMessageType
     * @param messageChannel
     * @param callbackFunc
     */
    public static void listenCallback(Class<?> notifyMessageType, MessageChannel messageChannel, Function<Object, Object> callbackFunc) {
        if (!callbackFuncs.containsKey(notifyMessageType)) {
            callbackFuncs.putIfAbsent(notifyMessageType, new ConcurrentHashMap<>());
        }

        Map<MessageChannel, Function<Object, Object>> functionMap = callbackFuncs.get(notifyMessageType);
        if (!functionMap.containsKey(messageChannel)) {
            functionMap.putIfAbsent(messageChannel, callbackFunc);
        } else {
            LOGGER.error("The notification message type:{}+Message channel:{},Has been subscribed for listening, duplicate subscription listening is invalid!", notifyMessageType.getSimpleName(), messageChannel.getDescription());
        }

    }

    /**
     * Notification callback (synchronously waiting for the processing result of the listening callback), that is, the producer
     *
     * @param notifyMessage
     * @param messageChannel
     * @param <R>
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <R> R notifyCallback(Object notifyMessage, MessageChannel messageChannel) {
        Class<?> notifyMessageType = notifyMessage.getClass();

        Map<MessageChannel, Function<Object, Object>> functionMap = callbackFuncs.getOrDefault(notifyMessageType, null);
        if (functionMap != null) {
            Function<Object, Object> callbackFunction = functionMap.getOrDefault(messageChannel, null);
            if (callbackFunction != null) {
                LOGGER.info("Notification callback message published, callback processing in progress:{},messageChannel:[{}]", JSONObject.toJSONString(notifyMessage), messageChannel.getDescription());
                Object result = callbackFunction.apply(notifyMessage);
                try {
                    return (R) result;
                } catch (ClassCastException castEx) {
                    throw new ClassCastException(String.format("The actual type of the returned value after listening callback processing is inconsistent with the expected type of the value to be received by the publishing notification callback, resulting in type conversion failure:%s," +
                                    "Please make sure notifyCallback And listenCallback For notification message type:%s+Message channel:%s The return value type must be consistent.",
                            castEx.getMessage(), notifyMessageType.getSimpleName(), messageChannel.getDescription()));
                }

            }
        }
        return null;
    }


}

Of course, if you need to realize 1-to-1 communication, you need to specify the message communication channel (i.e. unique identification) in addition to the message type. The purpose is to realize the same message type and support different point-to-point processing.

/**
 * Custom message channel
 * Function: used to identify different listener callers under the same message type (notifyMessage+messageChannel can identify a unique set of notification callbacks [producers] and listener callbacks [consumers])
 * @author zuowenjun
 * @date 2020-03-31
 */
public enum MessageChannel {
    None("invalid"),
    MSG_A("Test message A"),
    ;

    private String description;

    MessageChannel(String description) {
        this.description=description;
    }

    public String getDescription() {
        return description;
    }
}

The example code of using method is as follows:

@Service
public class DemoAService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DemoAService.class);
    

    public void showMsg(String name, String msg) {
        System.out.printf("[%1$tF %1$tT.%1$tL]hello!%s,DemoAService showMsg:%s %n", new Date(), name, msg);

        EventMessage eventMessage = new EventMessage();
        eventMessage.setName("aaa");
        eventMessage.setMsg("test");
        eventMessage.setCreatedDate(new Date());
        EventPublishSubscribeUtils.publishEvent(eventMessage);

        String msgJsonStr = EventPublishSubscribeUtils.notifyCallback(eventMessage, MessageChannel.MSG_A);

        System.out.printf("[%1$tF %1$tT.%1$tL]DemoAService showMsg notifyCallback json result:%2$s %n", new Date(), msgJsonStr);
    }

}


@Service
public class DemoBService {

    @Autowired
    private DemoAService demoAService;

    @PostConstruct
    public void init(){

        //Subscription consumption, no return value, supports 1-to-many, that is, the same message type can be subscribed by multiple consumers at the same time
        EventPublishSubscribeUtils.register(EventMessage.class,this::showFinishedMsg);

        //Subscription listening callback, with return value, only 1-to-1
        EventPublishSubscribeUtils.listenCallback(EventMessage.class, MessageChannel.MSG_A,this::getMsgCallbak);
    }

    private void showFinishedMsg(Object eventMsg){
        EventMessage eventMessage=(EventMessage)eventMsg;
        System.out.printf("[%1$tF %1$tT.%1$tL]%s,receive msg:%s doing...%n",
                eventMessage.getCreatedDate(),eventMessage.getName(),eventMessage.getMsg());

        //Analog logic processing
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.printf("[%1$tF %1$tT.%1$tL]%s,do finished!!!%n",new Date(),eventMessage.getName());
    }

    private String getMsgCallbak(Object eventMsg){
        EventMessage eventMessage=(EventMessage)eventMsg;
        eventMessage.setMsg(eventMessage.getMsg()+"--callback added!");
        eventMessage.setCreatedDate(new Date());

        System.out.printf("[%1$tF %1$tT.%1$tL]%s,do msg callback!!!%n",new Date(),eventMessage.getName());

        return JSONObject.toJSONString(eventMessage);
    }

}


As shown in the above code, with the help of EventPublishSubscribeUtils, we decoupled the dependency between two service beans, avoided the problem of circular dependency, removed the way of using @ Lazy annotation to solve the circular dependency, and made it easier to expand and change. In fact, Spring uses a similar Event mechanism at the bottom layer, which shows that this method can be used properly.

Here I use a simple diagram to compare the difference before and after the event publishsubscribeutils is not referenced. You can feel which is more convenient:
Before:

After that:

Finally, with regard to business decoupling and business boundary identification, I personally think that MQ can be used for cross process communication, and Event driven ideas can be used for the same process across multiple modules (classes, or across multiple business boundaries). What do you think? If there is a better plan, please comment and exchange, thank you.

Tags: Java JSON Lambda Spring

Posted on Thu, 07 May 2020 10:24:07 -0400 by edwardtilbury