利用 Spring AOP 实现日志记录
功能概述
-
切面定义:
- 使用 @Aspect 注解标记该类为一个切面。
- 使用 @Component 注解将该类注册为一个 Spring Bean。
- 使用 @Order(0) 注解指定该切面的执行顺序,值越小优先级越高。
-
切点定义:
- 使用 @Pointcut 注解定义一个切点 controllerLog(),匹配 com.szx.exam.controller 包及其子包下的所有控制器方法。
前置日志记录: - logBefore 方法负责在控制器方法执行前记录请求的相关信息。
- 获取当前请求的 HttpServletRequest 对象。
- 记录请求的 URL、HTTP 方法、客户端 IP、客户端名称、控制器类名、方法名以及方法参数。
- 使用 lombok 注解引入日志记录器 log,并在 finally 块中输出日志信息。
- 使用 @Pointcut 注解定义一个切点 controllerLog(),匹配 com.szx.exam.controller 包及其子包下的所有控制器方法。
-
环绕通知:
- 使用 @Around 注解定义一个环绕通知 around,在控制器方法执行前后进行操作。
- 记录方法开始执行的时间。
- 调用 logBefore 方法记录前置日志。
- 执行控制器方法,并记录返回结果。
- 记录方法执行时间。
- 清除 TraceId。
引入依赖
<!--aop-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency><!--fastjson-->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.0</version>
</dependency>
因为接口中有json输出,所以需要额外引入一个fastjson
代码编写
新建 ControllerLogAspect 类 ,编写如下代码
import com.alibaba.fastjson.JSON;
import com.szx.exam.util.RequestKit;
import com.szx.exam.util.TraceIdKit;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;@Aspect
@Order(0)
@Component
@Slf4j
public class ControllerLogAspect {// 拦截com.szx.exam下面的所有controller@Pointcut("within(com.szx.exam.controller..*)")public void controllerLog() {}private void logBefore(ProceedingJoinPoint pjd) {Map<String, String> logMap = new LinkedHashMap<>();try {Object[] args = pjd.getArgs();ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes == null) {throw new Exception("requestAttributes is null");}HttpServletRequest httpServletRequest = requestAttributes.getRequest();TraceIdKit.getTraceId(httpServletRequest);logMap.put("URL", httpServletRequest.getRequestURI());logMap.put("HTTP_METHOD", httpServletRequest.getMethod());logMap.put("IP", RequestKit.getIp(httpServletRequest));logMap.put("ClientName",RequestKit.getClientName(httpServletRequest));String className = pjd.getTarget().getClass().getName();logMap.put("CLASS", className);String methodName = pjd.getSignature().getName();logMap.put("METHOD", methodName);StringBuilder sb = new StringBuilder();for (Object object : args) {try {if (sb.length() > 0) {sb.append(", ");}sb.append(JSON.toJSONString(object));} catch (Exception ignored) {sb.append("null, ");}}logMap.put("ARGS", sb.toString());} catch (Exception e) {log.error("logBefore", e);} finally {for (Map.Entry<String, String> entry : logMap.entrySet()) {log.info("{}: {}", entry.getKey(), entry.getValue());}}}@Around("controllerLog()")public Object around(ProceedingJoinPoint pjd) throws Throwable {long start = System.currentTimeMillis();logBefore(pjd);try {Object result = pjd.proceed();log.info("RESP: {}", JSON.toJSONString(result));return result;} finally {log.info("COST: {} ms", System.currentTimeMillis() - start);TraceIdKit.clearTraceId();}}}
额外使用了两个工具类
RequestKit
import org.apache.commons.lang3.StringUtils;import javax.servlet.http.HttpServletRequest;
import java.net.URLEncoder;/*** 请求转化**/
@SuppressWarnings("unused")
public class RequestKit {/*** 获取真实 IP 地址** @param request 通用请求* @return IP 地址*/public static String getIp(HttpServletRequest request) {try {String ipAddress = request.getHeader("x-forwarded-for");if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("WL-Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getRemoteAddr();}if (ipAddress != null && ipAddress.length() > 15) {if (ipAddress.indexOf(",") > 0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));}}return ipAddress;} catch (Exception e) {throw new IllegalArgumentException("get ip exception: " + e.getMessage());}}/*** 获取客户端信息** @param request 通用请求* @return 客户端信息*/public static String getClientName(HttpServletRequest request) {String clientName = "--";String userAgent = request.getHeader("User-Agent");if (StringUtils.isEmpty(userAgent)) {return clientName;}if (userAgent.toLowerCase().contains("windows")) {clientName = "windows";}if (userAgent.toLowerCase().contains("mac")) {clientName = "mac";}if (userAgent.toLowerCase().contains("x11")) {clientName = "unix";}if (userAgent.toLowerCase().contains("android")) {clientName = "android";}if (userAgent.toLowerCase().contains("iphone")) {clientName = "iphone";}return clientName;}public static String encode(String value) {try {return URLEncoder.encode(value, "UTF-8");} catch (Exception e) {return value;}}
}
TraceIdKit
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;import javax.servlet.http.HttpServletRequest;
import java.util.UUID;@SuppressWarnings("unused")
public class TraceIdKit {private static final String TRACE_ID = "traceId";public static String getTraceId() {String traceId = MDC.get(TRACE_ID);if (StringUtils.isBlank(traceId)) {traceId = generateTraceId();MDC.put(TRACE_ID, traceId);}return traceId;}public static String getTraceIdHeader() {return TRACE_ID;}public static void getTraceId(HttpServletRequest httpServletRequest) {String traceId = httpServletRequest.getHeader(TRACE_ID);if (StringUtils.isBlank(traceId)) {traceId = getTraceId();}MDC.put(TRACE_ID, traceId);}public static void clearTraceId() {MDC.clear();}private static String generateTraceId() {UUID uuid = UUID.randomUUID();return uuid.toString().replace("-", "");}}
编写完成后,重启项目,当我们访问一个接口时,就会有对应的日志输出了
测试
编写一个测试接口
@RestController
@RequestMapping("/exam")
public class ExamController {@GetMapping("getAllList")public Result getAllList(String name) {return Result.ok("success");}@PostMapping("add")public Result add(@RequestBody HashMap<String,Object> map) {return Result.ok(map);}
}
测试访问,查看日志输出结果
结合 logback,查看记录的日志文件
我们后面的开发中,就不用为每个接口都去处理日志信息,通过aop切片即可自动的实现日志记录功能。