SpringBoot+Redis publish-subscribe mode encapsulates custom annotations for logs between microservices

Scene:
In projects, system operation records are usually saved as logs in order to record user operation behaviors and analyze system problems when system failures occur. In order to maintain the low intrusion of code and the standardization of log data between microservices, You can use custom log annotations to achieve the above purpose, and cooperate with Redis’s publish and subscribe model to achieve decoupling between applications (message middleware MQ, Spring Event, Spring asynchronous processing and other mechanisms can also be used).

  1. Log custom annotations
    Can be defined such as title, moduleName (module name), operationType (operation type), etc.
@Target({<!-- -->ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {<!-- -->

    String title() default "";

    ModuleEnum moduleName() default ModuleEnum.OTHER;

    OperationTypeEnum operationType() default OperationTypeEnum.OTHER;

    boolean isSaveRequestData() default true;

    boolean isSaveResponseData() default true;
}
  1. module name enum
public enum ModuleEnum {<!-- -->

    RBAC("RBAC", "RBAC basic service"),
    OTHER("OTHER","Other");

    private final String type;
    private final String desc;

    ModuleEnum(String type, String desc) {<!-- -->
        this.type = type;
        this.desc = desc;
    }

    public String getType() {<!-- -->
        return type;
    }

    public String getDesc() {<!-- -->
        return desc;
    }
}
  1. Business operation type enumeration
public enum OperationTypeEnum {<!-- -->

    OTHER("OTHER","Other operations"),


    INSERT("INSERT","New"),


    UPDATE("UPDATE","Update"),


    DELETE("DELETE","Delete"),


    GRANT("GRANT","Authorization"),


    EXPORT("EXPORT","Export"),


    IMPORT("IMPORT","import"),

    LOGIN("LOGIN","Login"),

    LOGOUT("LOGOUT","Exit");

    private final String type;
    private final String desc;

    OperationTypeEnum(String type, String desc) {<!-- -->
        this.type = type;
        this.desc = desc;
    }

    public String getType() {<!-- -->
        return type;
    }

    public String getDesc() {<!-- -->
        return desc;
    }
}
  1. Log annotation aspect
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {<!-- -->

    private final MessagePublisher messagePublisher;

    /** Exclude sensitive attribute fields */
    public static final String[] EXCLUDE_PROPERTIES = {<!-- --> "password", "oldPassword", "newPassword", "confirmPassword" };

    /**
     * Executed after processing the request
     *
     * @param joinPoint cut point
     */
    @AfterReturning(pointcut = "@annotation(log)", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Log log, Object result) {<!-- -->
        handleLog(joinPoint, log, null, result);
    }

    /**
     * Intercept abnormal operations
     *
     * @param joinPoint cut point
     * @param e exception
     */
    @AfterThrowing(value = "@annotation(log)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log log, Exception e) {<!-- -->
        handleLog(joinPoint, log, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {<!-- -->
        try {<!-- -->
            // Get the current user
            IUser loginUser = UserThreadLocal.get();
            SysLogBo sysLogBo = new SysLogBo();
            sysLogBo.setStatus(StatusEnum.SUCCESS.getStatus());
            //requested address
            String ip = NetWorkUtil.getIpAddr(HttpServletUtil.getRequest());
            sysLogBo.setIp(ip);
            sysLogBo.setUrl(StringUtils.substring(HttpServletUtil.getRequest().getRequestURI(), 0, 255));
            if (loginUser != null) {<!-- -->
                sysLogBo.setOperatorName(loginUser.getUsername());
            }

            if (e != null) {<!-- -->
                sysLogBo.setStatus(StatusEnum.FAIL.getStatus());
                sysLogBo.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            //Set method name
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            sysLogBo.setMethod(className + "." + methodName + "()");
            //Set request method
            sysLogBo.setRequestMethod(HttpServletUtil.getRequest().getMethod());
            // Process the parameters on the setting annotation
            getControllerMethodDescription(joinPoint, controllerLog, sysLogBo, jsonResult);
            //Message occurs
            messagePublisher.publishMessage("yyou-log",GsonUtil.bean2json(sysLogBo));
        } catch (Exception exp) {<!-- -->
            //Record local exception log
            log.error("Exception message:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * Get the description information of the method in the annotation for Controller layer annotation
     *
     * @param log log
     * @param sysLogBo operation log
     * @throwsException
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLogBo sysLogBo, Object jsonResult) throws Exception {<!-- -->
        //Set action action
        sysLogBo.setOperationType(log.operationType().name());
        //Set the operator category
        sysLogBo.setModuleName(log.moduleName().getDesc());
        // Do you need to save the request, parameters and values?
        if (log.isSaveRequestData()) {<!-- -->
            // Get parameter information and pass it into the database.
            setRequestValue(joinPoint, sysLogBo);
        }
        // Do you need to save the response, parameters and values?
        if (log.isSaveResponseData() & amp; & amp; Objects.nonNull(jsonResult)) {<!-- -->
            sysLogBo.setResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * Get the requested parameters and put them in the log
     *
     * @param sysLogBo operation log
     * @throws Exception exception
     */
    private void setRequestValue(JoinPoint joinPoint, SysLogBo sysLogBo) throws Exception {<!-- -->
        String requestMethod = sysLogBo.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {<!-- -->
            String params = argsArrayToString(joinPoint.getArgs());
            sysLogBo.setParam(StringUtils.substring(params, 0, 2000));
        } else {<!-- -->
            Map<?, ?> paramsMap = HttpServletUtil.getParamMap(HttpServletUtil.getRequest());
            sysLogBo.setParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000));
        }
    }

    /**
     * Parameter assembly
     */
    private String argsArrayToString(Object[] paramsArray) {<!-- -->
        String params = "";
        if (paramsArray != null & amp; & amp; paramsArray.length > 0) {<!-- -->
            for (Object o : paramsArray) {<!-- -->
                if (Objects.nonNull(o) & amp; & amp; !isFilterObject(o)) {<!-- -->
                    try {<!-- -->
                        String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter());
                        params + = jsonObj.toString() + " ";
                    } catch (Exception e) {<!-- -->
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * Ignore sensitive attributes
     */
    public PropertyPreExcludeFilter excludePropertyPreFilter() {<!-- -->
        return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES);
    }

    /**
     * Determine whether filtering is required.
     *
     * @param o Object information.
     * @return If it is an object that needs to be filtered, return true; otherwise, return false.
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {<!-- -->
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {<!-- -->
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {<!-- -->
            Collection collection = (Collection) o;
            for (Object value : collection) {<!-- -->
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {<!-- -->
            Map map = (Map) o;
            for (Object value : map.entrySet()) {<!-- -->
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}
  1. Redis message producer service
@Component
public class MessagePublisher {<!-- -->

    private final RedisTemplate<String, String> redisTemplate;

    public MessagePublisher(RedisTemplate<String, String> redisTemplate) {<!-- -->
        this.redisTemplate = redisTemplate;
    }

    public void publishMessage(String topic,String message) {<!-- -->
        redisTemplate.convertAndSend(topic, message);
    }
}
  1. Redis message configuration
@Configuration
public class RedisMessageConfig {<!-- -->

    @Bean
    public ChannelTopic topic() {<!-- -->
        return new ChannelTopic("yyou-log"); // Define the channel name of the message queue
    }

    @Bean
    public MessageListenerAdapter messageListenerAdapter(MessageConsumer messageConsumer) {<!-- -->
        return new MessageListenerAdapter(messageConsumer);
    }

    @Bean
    public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
                                                        MessageListenerAdapter messageListenerAdapter,
                                                        ChannelTopic topic) {<!-- -->
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        container.addMessageListener(messageListenerAdapter, topic);
        return container;
    }
}
  1. Redis message consumer service
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class MessageConsumer implements MessageListener {<!-- -->

    private final SysLogService sysLogService;

    @Override
    public void onMessage(Message message, byte[] pattern) {<!-- -->
        String data = message.toString();
        log.info("Received message: " + data);
        sysLogService.add(GsonUtil.json2bean(data, SysLogBo.class));
    }
}