背景

在一些重要接口,可能需要打印出请求的参数信息和响应的结果信息

实现

spring boot 版本 2.4.4

使用到了 AOP 功能,所以需要添加依赖

  1. implementation 'org.springframework.boot:spring-boot-starter-aop'
  2. // hutool 工具使用了里面的一些工具类
  3. implementation 'cn.hutool:hutool-all:5.5.4'

实现只能在方法上写的注解

  1. package cn.mrcode.web.accesslog;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. /**
  8. * <pre>
  9. * 访问 controller 日志注解
  10. * </pre>
  11. *
  12. * @author mrcode
  13. * @date 2021/2/4 10:13
  14. */
  15. @Target(ElementType.METHOD)
  16. @Retention(RetentionPolicy.RUNTIME)
  17. @Documented
  18. public @interface AccessLog {
  19. /**
  20. * 该方法名称,业务名称
  21. *
  22. * @return
  23. */
  24. String value() default "";
  25. /**
  26. * 是否打印请求参数
  27. *
  28. * @return
  29. */
  30. boolean isPrintReqParams() default true;
  31. /**
  32. * 是否打印响应结果
  33. *
  34. * @return
  35. */
  36. boolean isPrintRes() default true;
  37. }

Aspect 编写

  1. package cn.mrcode.web.accesslog;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. import org.aspectj.lang.annotation.Around;
  4. import org.aspectj.lang.annotation.Aspect;
  5. import org.aspectj.lang.reflect.MethodSignature;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.context.request.RequestAttributes;
  8. import org.springframework.web.context.request.RequestContextHolder;
  9. import org.springframework.web.context.request.ServletRequestAttributes;
  10. import java.lang.reflect.Method;
  11. import java.util.Arrays;
  12. import javax.servlet.http.HttpServletRequest;
  13. import cn.hutool.core.date.DateUtil;
  14. import cn.hutool.core.lang.Console;
  15. import cn.hutool.core.lang.Snowflake;
  16. import cn.hutool.core.util.IdUtil;
  17. import cn.hutool.core.util.StrUtil;
  18. /**
  19. * 访问日志
  20. *
  21. * @author mrcode
  22. * @date 2021/6/9 13:13
  23. */
  24. @Component
  25. @Aspect
  26. public class AccessLogAspect {
  27. Snowflake snowflake = IdUtil.getSnowflake(1, 1);
  28. // 扫描 controller 结尾中的所有方法
  29. // @Around("execution(* xxx.web.controller..*Controller.*(..))")
  30. @Around("@annotation(AccessLog)") // 或则直接使用扫描所有有注解的方法
  31. public Object access(ProceedingJoinPoint point) throws Throwable {
  32. Object[] args = point.getArgs();
  33. MethodSignature signature = (MethodSignature) point.getSignature();
  34. Method method = signature.getMethod();
  35. final AccessLog annotation = method.getAnnotation(AccessLog.class);
  36. String nextId = null;
  37. if (annotation != null) {
  38. // 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
  39. nextId = snowflake.nextIdStr();
  40. String sign = method.getDeclaringClass().getName() + "." + method.getName();
  41. String remoteHost = "N/A";
  42. // 在 controller 中获取到访问的 ip
  43. final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  44. if (requestAttributes != null && requestAttributes instanceof ServletRequestAttributes) {
  45. final HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
  46. remoteHost = request.getRemoteHost();
  47. }
  48. if (annotation.isPrintReqParams()) {
  49. Console.log(StrUtil.format("{} INFO reqId={},title={},reqIp={}, params={},目标方法 {}",
  50. DateUtil.now(),
  51. nextId,
  52. annotation.value(),
  53. remoteHost,
  54. Arrays.toString(args), sign));
  55. }
  56. }
  57. Object proceed = point.proceed(); // 类似于调用过滤器链一样
  58. if (annotation != null) {
  59. if (annotation.isPrintRes()) {
  60. Console.log(StrUtil.format("{} INFO reqId={},results={}", DateUtil.now(), nextId, proceed));
  61. }
  62. }
  63. return proceed;
  64. }
  65. }

使用方式:在 controller 中想要打印日志的请求接口方法上增加注解

  1. @AccessLog("测试-搜索")

测试结果输出

  1. 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
  2. 2021-06-09 16:08:51 reqId=1402538164018614272,results=Result{code=0, msg='OK'}

拓展

  • 另外还可以参考 RuoYi 的实现,按配置进行记录到数据库
  • 去掉其中有关 Request 相关代码,这个注解可以作用在任何方法上,实现任何方法都可以打印入参和出参

对任何方法进行日志入参出参打印

  1. import org.aspectj.lang.ProceedingJoinPoint;
  2. import org.aspectj.lang.annotation.Around;
  3. import org.aspectj.lang.annotation.Aspect;
  4. import org.aspectj.lang.reflect.MethodSignature;
  5. import org.springframework.stereotype.Component;
  6. import java.lang.reflect.Method;
  7. import java.util.Arrays;
  8. import cn.hutool.core.lang.Console;
  9. import cn.hutool.core.lang.Snowflake;
  10. import cn.hutool.core.util.IdUtil;
  11. import cn.hutool.core.util.StrUtil;
  12. import lombok.extern.slf4j.Slf4j;
  13. /**
  14. * 访问日志
  15. *
  16. * @author mrcode
  17. * @date 2021/6/9 13:13
  18. */
  19. @Component
  20. @Aspect
  21. @Slf4j
  22. public class AccessLogAspect {
  23. private static Snowflake snowflake = IdUtil.getSnowflake(1, 1);
  24. @Around("@annotation(AccessLog)")
  25. public Object access(ProceedingJoinPoint point) throws Throwable {
  26. Object[] args = point.getArgs();
  27. MethodSignature signature = (MethodSignature) point.getSignature();
  28. Method method = signature.getMethod();
  29. final AccessLog annotation = method.getAnnotation(AccessLog.class);
  30. String nextId = null;
  31. if (annotation != null) {
  32. // 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
  33. nextId = snowflake.nextIdStr();
  34. String sign = method.getDeclaringClass().getName() + "." + method.getName();
  35. if (annotation.isPrintReqParams()) {
  36. log.info("tarckId={},title={}, params={},目标方法 {}",
  37. nextId,
  38. annotation.value(),
  39. Arrays.toString(args), sign);
  40. }
  41. }
  42. Object proceed = point.proceed(); // 类似于调用过滤器链一样
  43. if (annotation != null) {
  44. if (annotation.isPrintRes()) {
  45. Console.log(StrUtil.format("tarckId={},results={}", nextId, proceed));
  46. }
  47. }
  48. return proceed;
  49. }
  50. }

对任何方法进行日志入参出参打印 - ThreadLocal

使用 ThreadLocal 来实现在方法内也能获取到 tarckId,主要目的是:为了让方法内部打印日志的时候,能和入口参数一一对应上

  1. import org.aspectj.lang.ProceedingJoinPoint;
  2. import org.aspectj.lang.annotation.Around;
  3. import org.aspectj.lang.annotation.Aspect;
  4. import org.aspectj.lang.reflect.MethodSignature;
  5. import org.hsqldb.lib.StringUtil;
  6. import org.springframework.stereotype.Component;
  7. import java.lang.reflect.Method;
  8. import java.util.Arrays;
  9. import cn.hutool.core.lang.Console;
  10. import cn.hutool.core.lang.Snowflake;
  11. import cn.hutool.core.util.IdUtil;
  12. import cn.hutool.core.util.StrUtil;
  13. import lombok.extern.slf4j.Slf4j;
  14. /**
  15. * 访问日志
  16. *
  17. * @author mrcode
  18. * @date 2021/6/9 13:13
  19. */
  20. @Component
  21. @Aspect
  22. @Slf4j
  23. public class AccessLogAspect {
  24. private static Snowflake snowflake = IdUtil.getSnowflake(1, 1);
  25. private static final ThreadLocal<String> threadIds = ThreadLocal.withInitial(() -> snowflake.nextIdStr());
  26. @Around("@annotation(AccessLog)") // 或则直接使用扫描所有有注解的方法
  27. public Object access(ProceedingJoinPoint point) throws Throwable {
  28. try {
  29. Object[] args = point.getArgs();
  30. MethodSignature signature = (MethodSignature) point.getSignature();
  31. Method method = signature.getMethod();
  32. final AccessLog annotation = method.getAnnotation(AccessLog.class);
  33. String nextId = null;
  34. if (annotation != null) {
  35. // 使用雪花算法生成一个请求 ID, 在日志中使用该 ID 来关联响应结果信息
  36. nextId = threadIds.get();
  37. String sign = method.getDeclaringClass().getName() + "." + method.getName();
  38. if (annotation.isPrintReqParams()) {
  39. log.info("tarckId={},title={}, params={},目标方法 {}",
  40. nextId,
  41. annotation.value(),
  42. Arrays.toString(args), sign);
  43. }
  44. }
  45. Object proceed = point.proceed(); // 类似于调用过滤器链一样
  46. if (annotation != null) {
  47. if (annotation.isPrintRes()) {
  48. Console.log(StrUtil.format("tarckId={},results={}", nextId, proceed));
  49. }
  50. }
  51. return proceed;
  52. } finally {
  53. threadIds.remove();
  54. }
  55. }
  56. /**
  57. * 获取当前线程的请求 ID; 请注意,需要在有 @AccessLog 主键的方法中使用,否则可能会导致内存溢出问题
  58. *
  59. * @return
  60. */
  61. public static String trackId() {
  62. return threadIds.get();
  63. }
  64. }