Design and implementation of general interface open platform -- (13) message processor of message server

Posted by nemethpeter on Wed, 02 Feb 2022 13:53:19 +0100

Earlier, we introduced the configuration of each processor on the message processing chain of the server. This part is actually equivalent to the technical framework. Today, we will introduce the processor that processes specific business messages.

The processing framework consists of several main parts:
1. Message topic: we convert events into message topics according to business requirements. Message topics are equivalent to classifying different types of messages, which is the basic supporting part of our business processing.
2. Message processor factory: find the complete class name of the corresponding processor according to the message subject, and then create a specific message processor object through reflection technology.
3. Message processor: where specific business logic processing is really carried out, messages are divided into two types, and message processors also have two types. On the one hand, common parts can be extracted to form parent classes, on the other hand, they can be inherited by specific business logic processors.

Let's introduce them one by one.

Message subject

As mentioned above, the message subject is basic data. In fact, it needs to be defined and maintained separately in platform management. The main attributes are as follows:
Code: subject code, which is consistent with the business event code and can uniquely identify a type of message;
handler: processor, which stores the complete path (package name plus class name) of the specific logical processor corresponding to the message subject, and then the processor factory instantiates the processor through reflection according to this attribute;
responseTopicCode: response subject code. For request messages, the default response subject code needs to be configured to realize message response.

/**
 * Message subject
 * @author wqliu
 * @date 2021-08-21
 *
 */
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("ip_api_message_topic")
public class ApiMessageTopic extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * code
     */
    @TableField("code")
    private String code;

    /**
     * name
     */
    @TableField("name")
    private String name;

    /**
     * processor
     */
    @TableField("handler")
    private String handler;


    /**
     * Response subject code
     */
    @TableField("response_topic_code")
    private String responseTopicCode;

    /**
     * classification
     */
    @TableField("category")
    private String category;

    /**
     * state
     */
    @TableField("status")
    private String status;

    /**
     * remarks
     */
    @TableField("remark")
    private String remark;

    /**
     * Sort number
     */
    @TableField("order_no")
    private String orderNo;


}

Message processor factory

The simple factory pattern in the design pattern is used to get the complete path of the processor according to the message subject coding, and the instantiation of the processor is realized through reflection.

/**
 * Message processor factory
 * @author wqliu
 * @date 2021-10-13 9:07
 **/
public  class MessageHandlerFactory {
    private MessageHandlerFactory(){};

    public static MessageHandler createHandler(String topic){
        //Get classes using reflection techniques
        Class<MessageHandler> messageHandler=null;
        try {
            //Get the corresponding message processing class name according to the message subject
            ApiMessageTopicService service = SpringUtil.getBean(ApiMessageTopicService.class);
            String handlerName = service.getHandlerByCode(topic);
            messageHandler = (Class<MessageHandler>) Class.forName(handlerName);
            //Returns an instance of the message processing class
            return messageHandler.newInstance();
        }
        catch (CustomException e){
            throw new MessageException("S101",e.getMessage());
        }catch (Exception e){
            throw new MessageException("S102","Message processor does not exist");
        }
    }
}

In the technical framework of the message processing chain we designed, the request message processor and the response message processor are instantiated through the following code.

     //Transfer to specific message processor for processing
     ResponseMessageHandler handler = (ResponseMessageHandler)MessageHandlerFactory.createHandler(topic);
     handler.handleMessage(message,ctx.channel());

Message processor

Because there are two types of messages, and the processing logic for request messages and response messages is different. Accordingly, there are also two message processors: request message processor RequestMessageHandler and response message processor ResponseMessageHandler. The common operations of the two processors can be extracted to form an abstract parent class MessageHandler.

The specific business message processor will inherit RequestMessageHandler or ResponseMessageHandler and override some of its methods.

Let's start with a relatively simple response message processor.

Response message processor

Mainly do three things
1. Validation data
The verification here is different from the basic verification in the technical framework. It mainly verifies the following contents: message subject verification (whether it exists and is available), application verification (whether it exists and is available), permission verification and timeliness verification.

2. Update log
Find the message log according to the request message ID, and then fill in the response part. The design and implementation of the message log will be described in detail later.

3. Execute proprietary logic
A messageOperation method is reserved. When a specific message processor has additional personalized logic to process, it only needs to cover this method. This is actually the application of the template method in the design pattern

/**
 * Response message processor
 * @author wqliu
 * @date 2022-1-8 11:07
 **/
public class ResponseMessageHandler  extends MessageHandler{

    /**
     * Message processing
     *
     * @param message news
     * @param channel passageway
     */
    public void handleMessage(ResponseMessage responseMessage, Channel channel) {


        //Validation framework
        validateFramework(responseMessage);

        // Update message log
        apiMessageLogService.updateResponsePart(responseMessage);

        //Special treatment
        messageOperation(responseMessage, channel);
    }


    /**
     * Response message processing
     *
     * @param message
     * @param channel
     */
    protected void messageOperation(ResponseMessage message, Channel channel) {

    }


}

Request message processor

The request message processor is much more complex than the response processor. In addition to verifying messages, creating message logs and reserving business logic methods, it also needs to implement the following two important logic:

  1. Send a response message to the client sending the request, and update the response part of the corresponding log record
  2. Copy and forward the message, find all clients subscribing to the message topic, and push the message.

/**
 * Request message processor
 *
 * @author wqliu
 * @date 2022-1-8 10:42
 **/
@Slf4j
public class RequestMessageHandler extends MessageHandler {
    /**
     * Message processing
     *
     * @param message news
     * @param channel passageway
     */
    public void handleMessage(RequestMessage requestMessage, Channel channel) {

        // Log message requests
        apiMessageLogService.createRequestPart(requestMessage);

        //Validation framework
        validateFramework(requestMessage);

        //The request message status is set to no need to send by default
        apiMessageLogService.updateStatus(MessageStatusEnum.NOT_TO_REQUEST.name(),requestMessage.getId());


        //Special treatment
        messageOperation(requestMessage, channel);

        //Send response
        sendResponse(channel, MessageResponseResultEnum.SUCCESS.name(), "", requestMessage.getId(),
                requestMessage.getTopic());
        //Message processing (copy and forward)
        repostMessage(requestMessage);
    }




    /**
     * Message replication and forwarding
     *
     * @param requestMessage
     */
    protected void repostMessage(RequestMessage requestMessage) {
        //Find subscriptions
        String topic = requestMessage.getTopic();
        List<String> subscriberList = apiMessageSubscriptionService.getSubscriberList(topic);
        if (CollectionUtils.isEmpty(subscriberList)) {
            return;
        }
        //Create request message
        RequestMessage message = new RequestMessage();
        //TODO: for the convenience of testing, the same client is used as the producer and consumer of the message. When forwarding the message, the message center attaches the message subject temp suffix
        message.setTopic(topic+".temp");
        message.setContent(requestMessage.getContent());
        message.setPublishAppCode(appConfig.getApiPlatformMessage().getMessageServerAppCode());
        //Set the status to wait for sending
        message.setStatus(MessageStatusEnum.WAIT_REQUEST.name());
        //Traverse subscribers and send messages
        subscriberList.stream().forEach(appCode -> {
            //Copy the message through prototype mode and change the response application code
            RequestMessage newMessage = message.clone();
            newMessage.setResponseAppCode(appCode);
            //TODO: Data permission filtering
            boolean hasDataPermission = dataPermissionFilter(requestMessage, appCode);
            if (hasDataPermission) {
                //send message
                sendMessage(newMessage);
            }

        });

    }


    /**
     * send message
     *
     * @param message
     */
    protected void sendMessage(RequestMessage message) {
        String appCode = message.getResponseAppCode();

        //Get docking mode
        String integrationModel = apiAppService.getIntegrationModelByAppCode(appCode);
        if (integrationModel.equals(IntegrationModelEnum.CLIENT.name())) {
            //Client mode
            Channel channel = MessageServerHolder.appChannelMap.get(appCode);
            // log.info("the server sends a business message to the subscriber {}: {}", appCode, JSON.toJSONString(message));
            ChannelFuture channelFuture = channel.writeAndFlush(message);
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    //Set status
                    message.setStatus(MessageStatusEnum.REQUESTED.name());
                    message.setSendCount(message.getSendCount() + 1);

                    // Log message requests
                    apiMessageLogService.createRequestPart(message);
                }
            });
        } else if (integrationModel.equals(IntegrationModelEnum.INTERFACE.name())) {
            //The status is set to pending
            message.setStatus(MessageStatusEnum.WAIT_HANDLE.name());

            // Log message requests
            apiMessageLogService.createRequestPart(message);
        }

    }


    /**
     * Request message processing
     *
     * @param message
     * @param channel
     */
    protected void messageOperation(RequestMessage message, Channel channel) {

    }


}

Among them, the related logic of message replication and forwarding is relatively complex. In the next part, we will specifically talk about it.

Common processor

The parent class of request and response processor is mainly the reuse of common part, which mainly completes the work of data verification and sending response.

/**
 * Message processing parent class
 *
 * @author wqliu
 * @date 2021-10-13 9:07
 **/
@Slf4j
public  class MessageHandler {

    protected AppConfig appConfig = SpringUtil.getBean(AppConfig.class);

    protected ApiMessageLogService apiMessageLogService = SpringUtil.getBean(ApiMessageLogService.class);

    protected ApiMessageSubscriptionService apiMessageSubscriptionService= SpringUtil.getBean(ApiMessageSubscriptionService.class);


    protected ApiAppService apiAppService = SpringUtil.getBean(ApiAppService.class);

    protected ApiMessageTopicService apiMessageTopicService=SpringUtil.getBean(ApiMessageTopicService.class);


    protected ApiMessagePermissionService apiMessagePermissionService=
            SpringUtil.getBean(ApiMessagePermissionService.class);

    protected ApiDataPermissionService apiDataPermissionService=   SpringUtil.getBean(ApiDataPermissionService.class);


    /**
     * Data permission wildcard
     */
    public static final String DATA_PERMISSION_ALL = "*";

    /**
     * Validation framework
     */
    protected  void validateFramework(BaseMessage message){

        // Message subject verification (whether it exists and is available)
        validateTopic(message.getTopic());
        // Application verification (existence and availability)
        validateAppCode(message.getPublishAppCode());
        // Permission verification
        validatePermission(message.getPublishAppCode(),message.getTopic());
        // Timeliness verification
        validatePublishTimeValid(message.getPublishTime());
    }

    /**
     * Send response
     *
     * @param channel
     * @param result
     * @param errorCode
     * @param errorMessage
     * @param requestMessageId
     */
    public void sendResponse(Channel channel, String result, String errorMessage,
                             String requestMessageId, String topic) {


        // Organization response message
        ResponseMessage messageResponse = new ResponseMessage();
        //By default, the response subject code is obtained from the message subject entity class, which can be overridden by subclasses
        messageResponse.setTopic(this.getResponseTopicCode(topic));
        messageResponse.setContent(this.getResponseContent());

        messageResponse.setPublishAppCode(appConfig.getApiPlatformMessage().getMessageServerAppCode());
        messageResponse.setResult(result);
        messageResponse.setErrorMessage(errorMessage);
        messageResponse.setRequestMessageId(requestMessageId);


        // Send response to requestor
        channel.writeAndFlush(messageResponse);

        // Update message log
        apiMessageLogService.updateResponsePart(messageResponse);

    }




    /**
     * Verify permissions
     *
     * @param publishAppCode Application coding
     * @param topicCode          Subject code
     */
    protected void validatePermission(String publishAppCode, String topicCode) {

        boolean hasPermission = apiMessagePermissionService.checkPermission(publishAppCode, topicCode);
        if(hasPermission==false){
            throw new MessageException("301", "Application has no permission");
        }

    }

    /**
     * Verify timeliness
     *
     * @param publishTimeString Publishing time string
     */
    protected void validatePublishTimeValid(String publishTimeString) {


        // The data validation phase has verified that it can be converted. The conversion exception will not be processed here
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date publishTime = null;
        try {
            publishTime = dateFormat.parse(publishTimeString);
        } catch (ParseException e) {
            // The pre sequence phase has verified the date format. No exception will be thrown here. It is only for compilation
        }

        // Get the current time of the system
        Date currentTime = new Date();
        // Comparison time
        long diff = Math.abs(currentTime.getTime() - publishTime.getTime());
        //Maximum allowable time difference in milliseconds
        int maxTimeSpan = 10*60*1000;
        if (diff  > maxTimeSpan)
        {
            // Request time out of reasonable range (10 minutes)
            throw new MessageException("S401", "The release time is beyond the reasonable range");
        }
    }

    /**
     * Verification application
     *
     * @param publishAppCode Application coding
     */
    protected void validateAppCode(String publishAppCode) {
        try {
            ApiApp app =apiAppService.getByCode(publishAppCode);
            if(app.getStatus().equals(StatusEnum.DEAD.name())){
                throw new MessageException("S202", "App disabled");

            }
        }catch (Exception ex){
            throw new MessageException("S201", "Invalid app ID");
        }
    }

    /**
     * Verify subject code
     *
     * @param topicCode Subject code
     */
    protected void validateTopic(String topicCode) {
        try {
            ApiMessageTopic messageTopic = apiMessageTopicService.getByCode(topicCode);
            if(messageTopic.getStatus().equals(StatusEnum.DEAD.name())){
                throw new MessageException("S102", "Message subject not available");
            }
        }catch (Exception ex){
            throw new MessageException("S101", "Message subject does not exist");
        }

    }



    /**
     * Get response message subject
     *
     * @return
     */
    protected String getResponseTopicCode(String topic) {
        //By default, it is obtained from the message subject entity class
        ApiMessageTopicService service = SpringUtil.getBean(ApiMessageTopicService.class);
        String topicCode = service.getResponseTopicCodeByCode(topic);
        return topicCode;
    }

    /**
     * Get response message content
     *
     * @return
     */
    protected String getResponseContent() {
        return StringUtils.EMPTY;
    }


    /**
     * Data permission filtering
     * @param message news
     * @param appCode Responder application code
     * @return true,Yes, false, no permission
     */
    protected boolean dataPermissionFilter(RequestMessage message,String appCode){
        //By default, it returns true, without Data permission control, and can be overridden by subclasses that need data permission control
        return true;
    }

}

Topics: Netty