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