SpringBoot打印请求参数与响应参数

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,547评论 4 374
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,787评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,175评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,752评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,169评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,056评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,195评论 2 321
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,980评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,724评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,872评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,344评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,667评论 3 264
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,379评论 3 245
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,202评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,992评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,189评论 2 286
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,987评论 2 279

推荐阅读更多精彩内容