日常甩锅系列之Aop日志

233 阅读3分钟

日常甩锅系列之Aop日志

项目开发到联调阶段的时候,会经常出现和前端进行扯皮的操作,你说你有理,他说他有理,一般的操作就是:

前端:我肯定传递了,你看下我的参数

后台:我写的接口不可能出问题,100%可靠,你在请求一下,我看下服务器日志,刚才日志过多,你在请求一次

陷入了无线循环,但是今天我们有了日志切面就可以规避这些问题了,直接就能定位基础的请求入参及响应内容,方便后台排查问题及及时帅锅,开玩笑的,下面进入正题。

简单了解一下AOP

本片文章只会大致说明一下aop的基本概念及使用,详细内容请参考官方文档或网上各路大神的文章。

@Aspect:声明该类为一个注解类

@Slf4j //lombok 日志支持
@Component //注入到spring环境
@Aspect //定义切面
public class LogAspect {......}

maven依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

@Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为某个 package 下的方法,也可以是自定义注解

//定义一个切点
@Pointcut("execution(* com.springboot.example.controller..*(..))")
public void logPointcut() {......}

切点定义好后,就是围绕这个切点做文章了:

  • @Before: 前置增强,执行方法之前,编入相关代码,本片文章我们使用**@Around**

    @Before("logPointcut()")//上面定义的切点
    public void logBefore(JoinPoint joinPoint) {......}
    
  • @After: 后置增强,执行方法之后,编入相关代码,本片文章我们使用**@Around**

    @Before("logPointcut()")//上面定义的切点
    public void logAfter(JoinPoint joinPoint) {......}
    
  • @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景

  • @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理

    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")//上面定义的切点,异常后进入
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) 
    
  • @Around: 在切入点前后织入代码,并且可以自由的控制何时执行切点

    @Around("logPointcut()")//上面定义的切点
    public Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {}
    

完整代码

@Slf4j
@Component
@Aspect
public class LogAspect {
    //是否格式化输出json,便于排查问题
    @Value("${request.info.log.beautiful.json:1")
    private String beautifulJson;

    @Pointcut("execution(* com.springboot.example.controller..*(..))")
    public void logPointcut() {}

    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Object result = proceedingJoinPoint.proceed();
        RequestInfo requestInfo = new RequestInfo();
        requestInfo.setIp(request.getRemoteAddr());
        requestInfo.setUrl(request.getRequestURL().toString());
        requestInfo.setHttpMethod(request.getMethod());
        requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),
                proceedingJoinPoint.getSignature().getName()));
        requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
        requestInfo.setResult(result);
        requestInfo.setTimeCost(System.currentTimeMillis() - start);
        if (Objects.equals(beautifulJson, "0")) {
            log.info("Request Info      : {}", JSONUtil.toJsonStr(requestInfo));
        } else {
            log.info("Request Info      : {}", JSONUtil.toJsonPrettyStr(requestInfo));
        }

        return result;
    }


    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        RequestErrorInfo requestErrorInfo = new RequestErrorInfo();
        requestErrorInfo.setIp(request.getRemoteAddr());
        requestErrorInfo.setUrl(request.getRequestURL().toString());
        requestErrorInfo.setHttpMethod(request.getMethod());
        requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName()));
        requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
        requestErrorInfo.setException(ExceptionUtil.getMessage(e));

        log.info("Error Request Info      : {}", JSONUtil.toJsonStr(requestErrorInfo));
    }

    /**
     * 获取入参
     *
     * @param proceedingJoinPoint
     * @return
     */
    private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {
        //参数名
        String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames();
        //参数值
        Object[] paramValues = proceedingJoinPoint.getArgs();
        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {
        //参数名
        String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        //参数值
        Object[] paramValues = joinPoint.getArgs();

        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {
        Map<String, Object> requestParams = new HashMap<>();
        try {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            //从获取RequestAttributes中获取HttpServletRequest的信息
            HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
            Map<String, String[]> urlRequestParams = request.getParameterMap();
            if (CollectionUtil.isNotEmpty(urlRequestParams)) {
                requestParams.put("urlParams", urlRequestParams);
            }
        } catch (Exception exception) {
            log.error("获取url参数异常 - {}", exception.getMessage(), exception);
        }

        for (int i = 0; i < paramNames.length; i++) {
            Object value = paramValues[i];

            //如果是文件对象
            if (value instanceof MultipartFile) {
                MultipartFile file = (MultipartFile) value;
                //获取文件名
                value = file.getOriginalFilename();
            }
            if (!(value instanceof HttpServletRequest) && !(value instanceof HttpServletResponse)) {
                requestParams.put(paramNames[i], value);
            }
        }

        return requestParams;
    }

    @Data
    public class RequestInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private Object result;
        private Long timeCost;
    }

    @Data
    public class RequestErrorInfo {
        private String ip;
        private String url;
        private String httpMethod;
        private String classMethod;
        private Object requestParams;
        private String exception;
    }
}

测试

测试代码
@RestController
public class TestController {
    /**
     * 测试get参数
     *
     * @param name 测试参数
     * @return 结果
     */
    @GetMapping(value = "/test/get")
    public Object testGet(@RequestParam(value = "name", required = false) String name) {
        return R.ok(StrUtil.isNotBlank(name) ? name : "Hello world!");
    }

    /**
     * 测试post参数
     *
     * @param user 测试参数
     * @return 结果
     */
    @PostMapping(value = "/test/post")
    public Object testPost(@RequestBody User user) {
        return R.ok(user);
    }
}
访问测试
crul http://localhost:8080/test/get
crul http://localhost:8080/test/post
测试结果

这样是不是很方便帅锅了,当然最重要的是可以很方便的排查是否为参数异常问题,上面只是测试了get参数,post参数需要你们自行测试了。

总结

以上就是本次文章的全部内容了,本片文章只是简单的介绍了aop打印请求日志的基本使用,还可以入库记录重要的三方日志,请大家自行研究,本文章只是给大家参考而已!