背景
在一些重要接口,可能需要打印出请求的参数信息和响应的结果信息
实现
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)@Documentedpublic @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@Aspectpublic 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 中获取到访问的 ipfinal 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.search2021-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@Slf4jpublic 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@Slf4jpublic 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();}}
