如何使用AOP打印请求、响应参数? | Java Debug 笔记

1,192 阅读3分钟

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接

前言

在开发过程中,我们会对请求方发送过来的报文(请求报文)与被调用方响应的报文(响应报文),进行日志打印。但是需要在每个接口中都要自己手动进行打印,显得有些麻烦,况且可能还有些人会忘记打印请求报文与响应报文。增加了排查问题的难度。接下来,我将介绍如何使用AOP的方式实现请求报文、响应报文的日志打印。

加入依赖

<!--引入lombok依赖-->
     <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

<!--引入AOP依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

创建切面

创建一个名为 WebLogAspect 类,作为切面类

package com.gongj.test.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;

@Aspect
@Configuration
@Slf4j
public class WebLogAspect {

    ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 定义切点 切点为com.gongj.mall.product.scenario.controller下所有的类
     * 其中类里的所有方法为连接点
     */
    @Pointcut("execution(* com.gongj.test.controller..*.*(..))")
    public void webLog(){}

    /**
     * 环绕通知
     */
    @Around(value = "webLog()")
    public Object webLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // className的值:com.gongj.test.controller.UserController
        String className = joinPoint.getTarget().getClass().getName();
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        // 方法名称示例:test.controller.UserController.getUser():
        String methodName = new StringBuffer(className.replaceFirst("test.", ""))
                .append(".")
                .append(signature.getMethod().getName())
                .append("():").toString();
        log.info("==========> {} 方法原始报文:{}",methodName, 			    			         objectMapper.writeValueAsString(joinPoint.getArgs()));
        Object proceed = joinPoint.proceed();
        log.info("==========> {} 方法响应报文:{}",methodName,objectMapper.writeValueAsString(proceed));
        return proceed;
    }
}

其中几个方法进行介绍:

  • Object getTarget:获取被代理的对象
  • Signature getSignature:返回目标方法的签名对象
  • Object[] getArgs:返回目标方法的参数
  • Object proceed:执行目标方法

实践

  • 创建一个Controller,提供一个对外的方法。UserDTO 内就两个属性:userNameuserEmail
@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/getUser")
    public UserDTO getUser(UserDTO userDTO){
        return userDTO;
    }
}
  • 调用接口,控制台输出如下信息

    ==========> test.controller.UserController.getUser(): 方法原始报文:[{"userName":"文件","userEmail":"199@163.com"}]
    
    ==========> test.controller.UserController.getUser(): 方法响应报文:{"userName":"文件","userEmail":"199@163.com"}
    

    感觉是不是大功告成了呢?再举一个例子,在UserController增加一个方法:logDownload,该方法的作用是下载文件。

    @GetMapping("/logDownload")
        public void logDownload(String name, HttpServletResponse response) throws Exception {
            // 文件路径 请各位自行修改
            File file = new File("D:\\gongj\\龚杰文档\\学习计划\\定义.md");
    
            response.setContentType("application/force-download");
            // URLEncoder.encode(file.getName(),"UTF-8") 防止中文名字乱码
            response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(file.getName(),"UTF-8"));
    
            byte[] buffer = new byte[1024];
            try (FileInputStream fis = new FileInputStream(file);
                 BufferedInputStream bis = new BufferedInputStream(fis)) {
    
                OutputStream os = response.getOutputStream();
    
                int i = bis.read(buffer);
                while (i != -1) {
                    os.write(buffer, 0, i);
                    i = bis.read(buffer);
                }
            }
        }
    

    请求logDownload方法,出现以下异常:

image-20210512234231325.png

image-20210512234816811.png

getOutputStream() has already been called for this response
此响应已经调用了 getOutputStream()

从报错堆栈信息可得知它是在执行 log.info("==========> {} 方法原始报文:{}",methodName, objectMapper.writeValueAsString(joinPoint.getArgs()))这段代码的时候发生了异常,具体是 objectMapper.writeValueAsString这代码。这还没开始调用具体方法呢,就报错啦???

源码:

  • ResponseFacade
public ServletOutputStream getOutputStream() throws IOException {
    	// 调用
        ServletOutputStream sos = this.response.getOutputStream();
        if (this.isFinished()) {
            this.response.setSuspended(true);
        }

        return sos;
    }
  • Response
 public ServletOutputStream getOutputStream() throws IOException {
     // usingWriter = false
        if (this.usingWriter) {
            throw new IllegalStateException(sm.getString("coyoteResponse.getOutputStream.ise"));
        } else {
            // getOutputStream 方法会将 usingOutputStream 修改为 true
            this.usingOutputStream = true;
            if (this.outputStream == null) {
                this.outputStream = new CoyoteOutputStream(this.outputBuffer);
            }

            return this.outputStream;
        }
    }

然后一直走就会走到ResponseFacade.getWriter方法

 public PrintWriter getWriter() throws IOException {
     // 调用
        PrintWriter writer = this.response.getWriter();
        if (this.isFinished()) {
            this.response.setSuspended(true);
        }

        return writer;
    }
  • Response
 public PrintWriter getWriter() throws IOException {
     //usingOutputStream 为true 抛出异常
        if (this.usingOutputStream) {
            throw new IllegalStateException(sm.getString("coyoteResponse.getWriter.ise"));
        } else {
            if (ENFORCE_ENCODING_IN_GET_WRITER) {
                this.setCharacterEncoding(this.getCharacterEncoding());
            }
			// getWriter方法 会将 usingWriter 修改为 true
            this.usingWriter = true;
            this.outputBuffer.checkConverter();
            if (this.writer == null) {
                this.writer = new CoyoteWriter(this.outputBuffer);
            }

            return this.writer;
        }
    }

源码了调试一波(对不起,我晕了),放弃了,等技术牛掰了再来补充吧。

  • 解决
@Around(value = "webLog()")
    public Object webLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // className的值:com.gongj.test.controller.UserController
        String className = joinPoint.getTarget().getClass().getName();
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        // 方法名称示例:test.controller.UserController.getUser():
        String methodName = new StringBuffer(className.replaceFirst("com.gongj.", ""))
                .append(".")
                .append(signature.getMethod().getName())
                .append("():").toString();

        //获取参数值
        Object[] args = joinPoint.getArgs();
        //获取参数名称
        String[] parameterNames = signature.getParameterNames();
        //参数值和参数名称是顺序是一致的
        Map<String, Object> map = new LinkedHashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            // 获取每个参数值
            if (Objects.nonNull(args[i])) {
                //过滤掉参数类型为 HttpServletResponse
                if (args[i] instanceof HttpServletResponse) {
                    continue;
                }
            }
            // 添加到LinkedHashMap中
            map.put(parameterNames[i], args[i]);
        }
        log.info("==========> {} 方法原始报文:{}",methodName, objectMapper.writeValueAsString(map));
        Object proceed = joinPoint.proceed();
        log.info("==========> {} 方法响应报文:{}",methodName,objectMapper.writeValueAsString(proceed));
        return proceed;
    }

既然转换不了,那就不转了,反正转了也没用(我妥协了)。如果类型为 HttpServletResponse,就跳过,不放入Map中。

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。