最近看一个工程中将UUID打印在日志中、看到那个时分我想到的便是仅有恳求流水编号、什么意思呢、你能够理解为我调用一个接口他就会生成一个编号、这个编号就代表我之前恳求的仅有标识、后续出现问题能够快速定位日志信息。

开端-改造

我看他人改成中的打印很繁琐、每个log.xxx()的时分都要传这个编号、所以肯定是要优化一下的!哈哈哈哈!

这边封装了一个东西类、主要还是要懂ThreadLocal 线程本地变量 !简单理解每个线程都有一份、能做到独立互不干涉。

package com.stall.config;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 日志恳求流水、用日志追踪
 *
 * @Author 突突突突突
 * @blog https:///user/844892408381735
 * @Date 2023/3/24 13:24
 */
public class RequestLogManagement {
  public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
  
    /**
   *  初始进口、后续打印调用
   * @param describe 进口描绘
   */
  public static void init(String describe) {
    Map<String, Object> threadLocalMap = new HashMap<>();
    String requestUUID = UUID.randomUUID().toString();
    threadLocalMap.put("describe", describe);
    threadLocalMap.put("uuid", requestUUID);
    threadLocal.set(threadLocalMap);
   }

  public static String getRequestUUID() {
    return threadLocal.get() == null 
     ? "" : String.valueOf(threadLocal.get().get("uuid"));
   }

  public static String getRequestDescribe() {
    return threadLocal.get() == null 
     ? "" : String.valueOf(threadLocal.get().get("describe"));
   }
  public static void remove() {
    threadLocal.remove();
   }
}

死办法-每个log都手动打印

/**
 * 登录认证
 *
 * @Author 突突突突突
 * @blog https:///user/844892408381735
 * @Date 2023/3/24 13:49
 */
@Slf4j
@RestController
@RequestMapping("/auth")
public class WxLoginController {

  @Resource
  private AuthService authService;

  @PostMapping("/wx/login")
  public R<Object> wxLogin(String code) {
      RequestLogManagement.init("微信登录接口");
    try {
        log.info("{}、开端调用微信登录接口",RequestLogManagement.getRequestUUID());
      authService.wxLogin(code);
      return R.success();
     } catch (InterfaceException e) {
      log.error("{}、收到恳求反常信息",RequestLogManagement.getRequestUUID(), e);
      return R.custom(e.getCode(), e.getMessage());
     } catch (Exception e) {
      log.error("{}、收到恳求反常信息",RequestLogManagement.getRequestUUID(), e);
      return R.failed();
     }finally {
      RequestLogManagement.remove();
     }
   }
}

从上面的日志打印就能发现问题一些问题吧、假如我很多接口这个RequestLogManagement.init("微信登录接口");log.info("{}、xxxxxx调用",RequestLogManagement.getRequestUUID());RequestLogManagement.remove();这些内容中很多重复的操作、首要咱们解决进口开端描绘/进口完毕铲除数据、用眼睛一看就知道用什么解决这个问题、那便是AOP的办法、在Controller接口恳求的办法中的前后进行增强处理。

便是说知道用AOP的办法后、在写牛点自定义一个注解用于AOP能够精确的切入到对应办法。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLog {
  /**
   * 日志描绘
   */
  String value();
}
@Slf4j
@Aspect
@Component
public class RequestLogOperationAspect {
  /**
   * 预备环绕的办法
   */
  @Pointcut("@annotation(com.stall.config.aop.RequestLog)")
  public void execRequestLogService() {
   }

  @Around("execRequestLogService()")
  public Object RequestLogAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    //目标目标
    Class<?> clazz = proceedingJoinPoint.getTarget().getClass();
    //办法签名
    String method = proceedingJoinPoint.getSignature().getName();
    //办法参数
    Object[] thisArgs = proceedingJoinPoint.getArgs();
    //办法参数类型
    Class<?>[] parameterTypes = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getParameterTypes();
    //办法
    Method thisMethod = clazz.getMethod(method, parameterTypes);
    //自定义日志接口
    RequestLog methodAnnotation = Objects.requireNonNull(AnnotationUtils.findAnnotation(thisMethod, RequestLog.class));
    //  通用日志打印
    RequestLogManagement.init(methodAnnotation.value());
    log.info("[{}][{}]恳求开端、恳求参数:{}",RequestLogManagement.getRequestUUID(), methodAnnotation.value(), Arrays.toString(thisArgs));
    Object proceed = null;
    try {
      proceed = proceedingJoinPoint.proceed();
     } finally {
      log.info("[{}][{}]恳求完毕、恳求参数:{}",RequestLogManagement.getRequestUUID(), methodAnnotation.value(), proceed);
        // 铲除数据
      RequestLogManagement.remove();
     }
    return proceed;
   }
}

然后改造好后的代码、咱们在进口上加一个注解就ok了。

@RequestLog(value = "微信登录接口")
@PostMapping("/wx/login")
public R<Object> wxLogin(String code) {
 try {
  log.info("{}、开端调用微信登录接口",RequestLogManagement.getRequestUUID());
  authService.wxLogin(code);
  return R.success();
  } catch (InterfaceException e) {
  log.error("{}、收到恳求反常信息",RequestLogManagement.getRequestUUID(), e);
  return R.custom(e.getCode(), e.getMessage());
  } catch (Exception e) {
  log.error("{}、收到恳求反常信息",RequestLogManagement.getRequestUUID(), e);
  return R.failed();
  }
}

MDC-不需要每个log都手动打印

但是现在解决了那个问题还有这个log.info("{}、xxxxxx调用",RequestLogManagement.getRequestUUID());我总不能说我每次打印日志我都要加一个RequestLogManagement.getRequestUUID()

所以身为大聪明的我又想到AOP的办法、去增强log目标中的所有办法、所以我打开百度找阿找!!!我就发现一个牛很多的写法、便是MDC类目标中可能放入参数、而这个参数能够被日志底层运用、相当于在咱们打印日志的时分能够向日志中塞入一个值、类似插槽相同的概念、用就加、不必就不加!!!

MDC底层也是靠ThreadLocal来完成的、他泛型是Map类型、就相当于能放键值对的形式的数据、而MDC就相当所以咱们刚刚写RequestLogManagement的一个东西类、提供外部直接调用、要注意的便是一个MDCorg.slf4j.MDC一个是org.jboss.logging.MDC尽管说都能运用、但是里面的办法不相同、最终运用org.slf4j.MDC这个就能够。

来先把RequestLogOperationAspect.RequestLogAround(.)这个办法改造了、这个是咱们写的Controller切入履行的进口。

//自定义日志接口
RequestLog methodAnnotation = Objects.requireNonNull(AnnotationUtils.findAnnotation(thisMethod, RequestLog.class));
//  通用日志打印
RequestLogManagement.init(methodAnnotation.value());
// 将UUID放入到MDC目标中
MDC.put("requestId", RequestLogManagement.getRequestUUID());
log.info("[{}]恳求开端、恳求参数:{}", methodAnnotation.value(), Arrays.toString(thisArgs));
Object proceed = null;
try {
 proceed = proceedingJoinPoint.proceed();
} finally {
 log.info("[{}]恳求完毕、恳求参数:{}", methodAnnotation.value(), proceed);
 RequestLogManagement.remove();
 // 履行完成后铲除。
 MDC.clear();
}
logging:
  pattern:
   console: "${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{requestId}]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"

修正日志的打印格式、主要看%X{requestId}、当前的name便是MDC.put中的key的名称。

默许打印日志

往程序日志中加上唯一标识、让你快速定位到相关日志请求信息

修正后的打印日志

往程序日志中加上唯一标识、让你快速定位到相关日志请求信息
不论咱们自己写的RequestLogManagement还是MDC这两种办法都不能在子线程中获取到、解决办法便是在线程外将值赋值出去、然后由子线程重新塞入到自己线程副本的ThreadLocal中。

Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
new Thread(new Runnable() {
 @Override
 public void run() {
  MDC.setContextMap(copyOfContextMap);
  for (int i = 0; i < 10; i++) {
   log.info(">>>>>>>>>i={}", i);
   }
  MDC.clear();
  }
}).start();

小结

以上办法主要适用单机环境、如分布式服务之间的调用、肯定有其他的更好更牛的链路的办法。

把上面办法集成到你的单机项目中再配合之前写的 linux下检查项目日志的办法就能快速找到恳求流水对应的日志信息。