背景
在一些重要接口,可能需要打印出请求的参数信息和响应的结果信息
实现
spring boot 版本 2.4.4
使用到了 AOP 功能,所以需要添加依赖
implementation 'org.springframework.boot:spring-boot-starter-aop'
// hutool 工具使用了里面的一些工具类
implementation 'cn.hutool:hutool-all:5.5.4'
实现只能在方法上写的注解
package cn.mrcode.web.accesslog;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <pre>
* 访问 controller 日志注解
* </pre>
*
* @author mrcode
* @date 2021/2/4 10:13
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLog {
/**
* 该方法名称,业务名称
*
* @return
*/
String value() default "";
/**
* 是否打印请求参数
*
* @return
*/
boolean isPrintReqParams() default true;
/**
* 是否打印响应结果
*
* @return
*/
boolean isPrintRes() default true;
}
Aspect 编写
package cn.mrcode.web.accesslog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
/**
* 访问日志
*
* @author mrcode
* @date 2021/6/9 13:13
*/
@Component
@Aspect
public class AccessLogAspect {
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
// 扫描 controller 结尾中的所有方法
// @Around("execution(* xxx.web.controller..*Controller.*(..))")
@Around("@annotation(AccessLog)") // 或则直接使用扫描所有有注解的方法
public Object access(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
final AccessLog annotation = method.getAnnotation(AccessLog.class);
String nextId = null;
if (annotation != null) {
// 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
nextId = snowflake.nextIdStr();
String sign = method.getDeclaringClass().getName() + "." + method.getName();
String remoteHost = "N/A";
// 在 controller 中获取到访问的 ip
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null && requestAttributes instanceof ServletRequestAttributes) {
final HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
remoteHost = request.getRemoteHost();
}
if (annotation.isPrintReqParams()) {
Console.log(StrUtil.format("{} INFO reqId={},title={},reqIp={}, params={},目标方法 {}",
DateUtil.now(),
nextId,
annotation.value(),
remoteHost,
Arrays.toString(args), sign));
}
}
Object proceed = point.proceed(); // 类似于调用过滤器链一样
if (annotation != null) {
if (annotation.isPrintRes()) {
Console.log(StrUtil.format("{} INFO reqId={},results={}", DateUtil.now(), nextId, proceed));
}
}
return proceed;
}
}
使用方式:在 controller 中想要打印日志的请求接口方法上增加注解
@AccessLog("测试-搜索")
测试结果输出
2021-07-30 10:07:32 INFO reqId=1420929021054685184,title=搜索,reqIp=192.168.1.107, params=[UserInfo(id=1007, name=小区, phone=18500000040, roles=[ROLE_NORMAL], accessToken=d901f1c110ff49eeab97377370b31892), DataSearchRequest()],目标方法 cn.mrcode.controller.data.DataController.search
2021-06-09 16:08:51 reqId=1402538164018614272,results=Result{code=0, msg='OK'}
拓展
- 另外还可以参考 RuoYi 的实现,按配置进行记录到数据库
- 去掉其中有关 Request 相关代码,这个注解可以作用在任何方法上,实现任何方法都可以打印入参和出参
对任何方法进行日志入参出参打印
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import cn.hutool.core.lang.Console;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 访问日志
*
* @author mrcode
* @date 2021/6/9 13:13
*/
@Component
@Aspect
@Slf4j
public class AccessLogAspect {
private static Snowflake snowflake = IdUtil.getSnowflake(1, 1);
@Around("@annotation(AccessLog)")
public Object access(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
final AccessLog annotation = method.getAnnotation(AccessLog.class);
String nextId = null;
if (annotation != null) {
// 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
nextId = snowflake.nextIdStr();
String sign = method.getDeclaringClass().getName() + "." + method.getName();
if (annotation.isPrintReqParams()) {
log.info("tarckId={},title={}, params={},目标方法 {}",
nextId,
annotation.value(),
Arrays.toString(args), sign);
}
}
Object proceed = point.proceed(); // 类似于调用过滤器链一样
if (annotation != null) {
if (annotation.isPrintRes()) {
Console.log(StrUtil.format("tarckId={},results={}", nextId, proceed));
}
}
return proceed;
}
}
对任何方法进行日志入参出参打印 - ThreadLocal
使用 ThreadLocal 来实现在方法内也能获取到 tarckId,主要目的是:为了让方法内部打印日志的时候,能和入口参数一一对应上
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.hsqldb.lib.StringUtil;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import cn.hutool.core.lang.Console;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 访问日志
*
* @author mrcode
* @date 2021/6/9 13:13
*/
@Component
@Aspect
@Slf4j
public class AccessLogAspect {
private static Snowflake snowflake = IdUtil.getSnowflake(1, 1);
private static final ThreadLocal<String> threadIds = ThreadLocal.withInitial(() -> snowflake.nextIdStr());
@Around("@annotation(AccessLog)") // 或则直接使用扫描所有有注解的方法
public Object access(ProceedingJoinPoint point) throws Throwable {
try {
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
final AccessLog annotation = method.getAnnotation(AccessLog.class);
String nextId = null;
if (annotation != null) {
// 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
nextId = threadIds.get();
String sign = method.getDeclaringClass().getName() + "." + method.getName();
if (annotation.isPrintReqParams()) {
log.info("tarckId={},title={}, params={},目标方法 {}",
nextId,
annotation.value(),
Arrays.toString(args), sign);
}
}
Object proceed = point.proceed(); // 类似于调用过滤器链一样
if (annotation != null) {
if (annotation.isPrintRes()) {
Console.log(StrUtil.format("tarckId={},results={}", nextId, proceed));
}
}
return proceed;
} finally {
threadIds.remove();
}
}
/**
* 获取当前线程的请求 ID; 请注意,需要在有 @AccessLog 主键的方法中使用,否则可能会导致内存溢出问题
*
* @return
*/
public static String trackId() {
return threadIds.get();
}
}