SpringBoot2实践系列(四十九):SpringAOP实现统一记录请求和响应到日志、解密并修改请求入参值

star2017 1年前 ⋅ 787 阅读

在实际开发中,可能需要打印方法的入参和返回的数据以帮助出现问题时可快递定位.

常规的做法在方法中的业务处理之前使用 Logger 打印方法入参,在业务处理之后打印结果数据,这样就会在很多方法中存在重复代码。

像打印日志这类跨多个业务和模块的需求,可以通过 Spring AOP 来统一实现,完全省略了方法中手动添加 Logger 的操作。

统一打印日志

统一打印日志可以集成 ELK,归集这些日志数据,可以做一些统计分析工作,用来对功能和性能进行优化调整。

下面示例,使用前置和后置通知实现将 Controller 方法的入参和响应记录到日志,以方便快速定位问题。

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Aspect
@Component
public class ReqRespLogAop {

    private static final Logger logger = LogManager.getLogger(ReqRespLogAop.class);

    private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal("StopWatch");

    @Pointcut("execution(* com.xxxxxx.controller.*.*(..))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void logRequestParams(JoinPoint joinPoint) {
        // 开始时间
        threadLocal.set(System.currentTimeMillis());

        Object[] args = joinPoint.getArgs();
        //过滤序列化异常
        Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
        List<Object> logArgs = stream.filter(arg -> (!(arg instanceof HttpServletRequest) &&
                !(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile))).collect(Collectors.toList());

        String fullClassName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String remoteHost = request.getRemoteHost();
        logger.info("Request Client Host:{}", remoteHost);
        logger.info("Request URI:{}, API:{}, Request Body:{}", request.getRequestURI(), fullClassName + "." + methodName, JSON.toJSONString(logArgs));
    }

    @AfterReturning(returning = "response", pointcut = "pointcut()")
    public void logResponseBody(Object response) {
        if (response != null) {
            //响应信息
            logger.info("Response Body:{}", JSON.toJSONString(response));
        }
        //计算请求处理耗时,单位:秒
        Long timeConsume = (System.currentTimeMillis() - threadLocal.get());
        if (timeConsume > (2 * 1000)) {
            logger.warn("请求处理耗时:{}", timeConsume + " ms");
        } else {
            logger.info("请求处理耗时:{}", timeConsume + " ms");
        }
        threadLocal.remove();
    }
}

如果还需要记录其它方法的入参和返回数据,例如记录业务层的方法处理

在切面类里可以定义多个切点,使用切入点表达式组合的方式来实现,参考 Spring Boot 2实践系列(四十八):Spring AOP详解与应用:切入点表达式组合

解密及修改入参值

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.clearofchina.core.exception.BusinessException;
import com.clearofchina.prediagnose.annotate.UuidValid;
import com.clearofchina.prediagnose.constants.SysConstants;
import com.clearofchina.prediagnose.utils.CryptoUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Aspect
@Component
public class RequestResponseLogAop {

    private static final Logger logger = LogManager.getLogger(RequestResponseLogAop.class);

    private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal("StopWatch");

    @Pointcut("execution(* com.*.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * @desc: 记录请求到日志
     * @param: [joinPoint]
     */
    @Before("pointcut()")
    public void logRequestInfo(JoinPoint joinPoint) {
        // 开始时间
        threadLocal.set(System.currentTimeMillis());
        Object[] args = joinPoint.getArgs();

        //过滤序列化异常
        Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
        List<Object> logArgs = stream.filter(arg -> (!(arg instanceof HttpServletRequest) &&
                !(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile))).collect(Collectors.toList());

        String fullClassName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String remoteHost = request.getRemoteHost();
        logger.info("Request Client Host:{}", remoteHost);
        logger.info("Request URI:{}, API:{}", request.getRequestURI(), fullClassName + "." + methodName);
        logger.info("Request Body:{}", JSON.toJSONString(logArgs));
        // uuid参数做了对称加密,对其进行解密校验
        this.validUuid(joinPoint, args);
    }

    /**
     * @desc: uuid解密校验
     * @param: [joinPoint, args]
     */
    private void validUuid(JoinPoint joinPoint, Object[] args) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        // UuidValid 是一个自定义注解,作用在 Controller 层的方法上
        UuidValid annotation = targetMethod.getAnnotation(UuidValid.class);
        if (ObjectUtils.isNotEmpty(annotation)) {
            Object obj = args[0];
            JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(obj));
            String uuid = (String) jsonObject.get("uuid");
            String registerNo = null;
            try {
                // 入参 uuid 解密出来为 registerNo
                registerNo = CryptoUtil.decryptDES(SysConstants.UUID_SECRET, uuid);
            } catch (Exception e) {
                e.printStackTrace();
                throw new BusinessException("UUID错误");
            }

            try {
                // 反射拿到对象属性
                Field field = obj.getClass().getDeclaredField("registerNo");
                if (ObjectUtils.isNotEmpty(field)) {
                    field.setAccessible(true);
                    // 设置值
                    field.set(obj, registerNo);
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @desc: 记录响应到日志
     * @param: [response]
     */
    @AfterReturning(returning = "response", pointcut = "pointcut()")
    public void logResponseInfo(Object response) {

        if (response instanceof ResponseEntity) {
            logger.info("Response Body:{}", "输出文件");
        } else {
            //响应信息
            logger.info("Response Body:{}", JSON.toJSONString(response));
        }

        //计算请求处理耗时,单位:秒
        Long timeConsume = (System.currentTimeMillis() - threadLocal.get());
        if (timeConsume > (2 * 1000)) {
            logger.warn("请求处理耗时:{}", timeConsume + " ms");
        } else {
            logger.info("请求处理耗时:{}", timeConsume + " ms");
        }
        threadLocal.remove();
    }
}
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: