使用 Spring AOP 实现日志记录
利用 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切片即可自动的实现日志记录功能。