Spring Boot 使用 Aspect 完整記錄(log) Restful API 的request、response,且可以寫成一行log
前言
網路上可以找到各種寫log的作法,有透過filter、interceptor、aspect
但總會有些缺陷,無法完整的記錄
像filter,只取得到request
interceptor,取得到request、response、method,但拿不到入參
不然就是response部份寫在@AfterReturning()
裡,如下:
@AfterReturning(returning = "ret", pointcut = "logPointCut()")
public void doAfterReturning(Object ret) throws Throwable {
logger.info("response: " + ret);
}
導致log強迫拆成多行來寫
經過多方資料的拼湊後,總算用aspect成功寫出完整的log
且可以直接一條log完整記錄!
輸出範例
實際log記錄方式,依需求自行調整,僅提供範例寫法
為便於閱讀,範例裡仍寫成多行
但可以寫成只有一行log完整記錄request、response詳細內容
GET(範例)
API:http://localhost:8080/hello?id=123&name=andy&age=18
[2021-06-18 10:17:25.988] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - ------------------------------------------
[2021-06-18 10:17:25.991] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - [0:0:0:0:0:0:0:1] [GET] [http://localhost:8080/hello] [DemoController.hello()] [46 ms]
[2021-06-18 10:17:25.992] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - -----------------REQUEST------------------
[2021-06-18 10:17:26.118] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - request parameter: {
"id" : [ "123" ],
"name" : [ "andy" ],
"age" : [ "18" ]
}
[2021-06-18 10:17:26.119] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - request body:
[2021-06-18 10:17:26.121] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - -----------------RESPONSE-----------------
[2021-06-18 10:17:26.148] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - RESPONSE: "hello!"
[2021-06-18 10:17:26.149] [http-nio-8080-exec-3] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - ------------------------------------------
POST(範例)
API:http://localhost:8080/user
,新增user
[2021-06-18 10:19:47.857] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - ------------------------------------------
[2021-06-18 10:19:47.857] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - [0:0:0:0:0:0:0:1] [POST] [http://localhost:8080/user] [Cm0099Controller.createUser()] [84 ms]
[2021-06-18 10:19:47.857] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - -----------------REQUEST------------------
[2021-06-18 10:19:47.857] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - request parameter: { }
[2021-06-18 10:19:47.857] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - request body: {"name":"andy","age":18,"phone":0912345678}
[2021-06-18 10:19:47.858] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - -----------------RESPONSE-----------------
[2021-06-18 10:19:47.881] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - RESPONSE: {
"id" : "1",
"name" : "andy",
"age" : "18",
"phone" : "0912345678"
}
[2021-06-18 10:19:47.881] [http-nio-8080-exec-2] [INFO ] [com.example.demo.core.logging.HttpLogAspect] - ------------------------------------------
作法
SpringBootApplication增加@ServletComponentScan
打開Spring Boot 入口程式,增加@ServletComponentScan
裝飾器,使Spring Boot可以使用WebFilter
裝飾器
@SpringBootApplication
+@ServletComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
增加LogFilter,以wrapper request
因request的流在進入aop時被關閉了,若由request取body,會拋exception
故包裝一份在aop寫log時可以取body
@WebFilter
public class LogFilter extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
httpServletRequest = new ContentCachingRequestWrapper(httpServletRequest, 1024); // 最大長度取1024,依需要自行調整數字
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
增加LogAspect,實際寫log
相關說明都在註解裡了
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper mapper;
@Pointcut("bean(*Controller)") // 抓取所有Controller,可依實際需求調整
public void logPointCut() { }
@Around("logPointCut()")
public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 僅取到Controller名字,去掉前面package的路徑。實務上同一專案不同路徑卻同一命名機會應該頗低
// 若仍想印完整,拿掉`lastPointIndex`,直接印`joinPoint.getSignature().getDeclaringTypeName()`即可
int lastPointIndex = joinPoint.getSignature().getDeclaringTypeName().lastIndexOf(".") + 1;
String class_method = joinPoint.getSignature().getDeclaringTypeName().substring(lastPointIndex) + "." +
joinPoint.getSignature().getName() + "()";
Object responseBody = joinPoint.proceed();
logger.info("------------------------------------------");
// [IP] [方法(GET/POST)] [API網址] [controller.method()] [執行時間]
logger.info("[{}] [{}] [{}] [{}] [{} ms]", request.getRemoteAddr(), request.getMethod(), request.getRequestURL(), class_method, System.currentTimeMillis() - startTime);
logger.info("-----------------REQUEST------------------");
logger.info("request parameter: {}", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request.getParameterMap())); // 若想要一行呈現,拿掉`writerWithDefaultPrettyPrinter()`,寫成`mapper.writeValueAsString(參數)`即可
logger.info("request body: {}", getPayload(request));
logger.info("-----------------RESPONSE-----------------");
logger.info("RESPONSE: {}",this.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(responseBody)); // / 若想要一行呈現,拿掉`writerWithDefaultPrettyPrinter()`,寫成`mapper.writeValueAsString(參數)`即可
logger.info("------------------------------------------");
return responseBody;
}
private String getPayload(HttpServletRequest request) {
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
try {
int length = Math.min(buf.length, 1024);// 最多只印出1024長度
return new String(buf, 0, length, wrapper.getCharacterEncoding());
} catch (UnsupportedEncodingException ex) {
return "[unknown]";
}
}
}
return "";
}
}
心得
網路上查到Aspect的解法,都會提到:
// ...(doBefore)...
Object object = proceedingJoinPoint.proceed();
// ...(doAfter)...
return object;
但卻沒人說明到proceedingJoinPoint.proceed();
回傳的object,就是responseBody
!
一直在想辦法由HttpServletResponse res = attributes.getResponse();
去找Body,但都找不到
而Java直接print object.toString()
,也是object代號而已= =
結果答案就眼前。真是眾裡尋他千百度,就在燈火闌珊處…
參考資料
Spring AOP @Around output request parameters and return parameters
spring boot aop打印http請求回復日誌包含請求體