当前位置 : 首页 » 文章分类 :  开发  »  SLF4J

SLF4J

Simple Logging Facade for Java (SLF4J) 简单日志门面

Simple Logging Facade for Java (SLF4J)
https://www.slf4j.org/


@Slf4j 注解

为了避免每个类中都要写一遍如下获取日志门面的代码

private static final Logger logger = LoggerFactory.getLogger(Application.class);

可以在类上写上加 @Slf4j 注解,然后直接用 log 实例打印日志

@Slf4j
public class LoggerTest {
  @Test
  public void test2(){
     log.debug("debug");//默认日志级别为info
     log.info("info");
     log.warn("warn");
     log.error("error");
  }
}

@Slf4j注解后找不到log变量

如果注解 @Slf4j 后找不到变量 log,需要IDEA安装lombok插件
Settings -> Plugins -> Search in repositories 输入 lombok 搜索安装。


门面模式

slf4j是门面模式的典型应用。
门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。

门面模式的核心为Facade即门面对象,门面对象核心为几个点:

  • 知道所有子角色的功能和责任
  • 将客户端发来的请求委派到子系统中,没有实际业务逻辑
  • 不参与子系统内业务逻辑的实现

为什么要用日志门面?

我们自己的系统中使用了logback这个日志系统
我们的系统使用了A.jar,A.jar中使用的日志系统为log4j
我们的系统又使用了B.jar,B.jar中使用的日志系统为slf4j-simple
这样,我们的系统就不得不同时支持并维护logback、log4j、slf4j-simple三种日志框架,非常不便。

解决这个问题的方式就是引入一个适配层,由适配层决定使用哪一种日志系统,而调用端只需要做的事情就是打印日志而不需要关心如何打印日志,slf4j或者commons-logging就是这种适配层

slf4j只是一个日志标准,并不是日志系统的具体实现。


SLF4J是如何找到具体的日志实现的?

public class SomeService {
  private static final Logger logger = LoggerFactory.getLogger(SomeService.class);
}

我们代码中通过 slf4j 的 LoggerFactory.getLogger() 中获取 logger 时, slf4j 是如何找到具体的日志实现(log4j 或 logback 等)的呢?

org.slf4j.LoggerFactory 的关键源码如下:

package org.slf4j;

public final class LoggerFactory {
  // 要在 classpath 中查找的类
  private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

  // 在 classpath 下搜索 StaticLoggerBinder
  static Set<URL> findPossibleStaticLoggerBinderPathSet() {
      Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
      try {
          ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
          Enumeration<URL> paths;
          if (loggerFactoryClassLoader == null) {
              paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
          } else {
              paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
          }
          while (paths.hasMoreElements()) {
              URL path = paths.nextElement();
              staticLoggerBinderPathSet.add(path);
          }
      } catch (IOException ioe) {
          Util.report("Error getting resources from path", ioe);
      }
      return staticLoggerBinderPathSet;
  }

  private final static void bind() {
      try {
          Set<URL> staticLoggerBinderPathSet = null;
          // skip check under android, see also
          // http://jira.qos.ch/browse/SLF4J-328
          if (!isAndroid()) {
              // 去 classpath 下查找所有 StaticLoggerBinder 类
              staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
              // 若有多个,打印 warning 信息
              reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
          }
          // the next line does the binding 获取单例
          StaticLoggerBinder.getSingleton();
          INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
          // 报告实际的日志实现绑定
          reportActualBinding(staticLoggerBinderPathSet);
          fixSubstituteLoggers();
          replayEvents();
          // release all resources in SUBST_FACTORY
          SUBST_FACTORY.clear();
      } catch (NoClassDefFoundError ncde) {
          String msg = ncde.getMessage();
          if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
              INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
              Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
              Util.report("Defaulting to no-operation (NOP) logger implementation");
              Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
          } else {
              failedBinding(ncde);
              throw ncde;
          }
      } catch (java.lang.NoSuchMethodError nsme) {
          String msg = nsme.getMessage();
          if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
              INITIALIZATION_STATE = FAILED_INITIALIZATION;
              Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
              Util.report("Your binding is version 1.5.5 or earlier.");
              Util.report("Upgrade your binding to version 1.6.x.");
          }
          throw nsme;
      } catch (Exception e) {
          failedBinding(e);
          throw new IllegalStateException("Unexpected initialization failure", e);
      }
  }

  // 初始化,首次 getLogger 时被调用
  private final static void performInitialization() {
      bind();
      if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
          versionSanityCheck();
      }
  }

  // 是否有多个 StaticLoggerBinder 类
  private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
      return binderPathSet.size() > 1;
  }

  // 打印在 classpath 下发现多个 StaticLoggerBinder 类的 warning 信息
  private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
      if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
          Util.report("Class path contains multiple SLF4J bindings.");
          for (URL path : binderPathSet) {
              Util.report("Found binding in [" + path + "]");
          }
          Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
      }
  }

  // 打印实际的 日志实现 绑定
  private static void reportActualBinding(Set<URL> binderPathSet) {
      // binderPathSet can be null under Android
      if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
          Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
      }
  }
}

跟进 org.slf4j.LoggerFactory 源码,发现在初始化时会执行一个 bind() 方法,其中会去当前 classpath 中找 org/slf4j/impl/StaticLoggerBinder.class, 而所有 slf4j 的实现,在提供的 jar 包路径下,一定是有 org/slf4j/impl/StaticLoggerBinder.class 存在的, 所以只要引入了 slf4j 的实现,比如 slf4j-simple、logback、slf4j-log4j12,slf4j 就能自动找到他们。


classpath中同时有多个 StaticLoggerBinder 实现类

如果在系统中同时引入多个slf4j的实现,启动时会出现警告有多个实现,并自动选择一个实现类。
例如:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/user/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/user/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.10.0/log4j-slf4j-impl-2.10.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

如上我项目 classpath 中同时有 logback 和 log4j 的StaticLoggerBinder.class类实现,slf4j最终自动选择了logback日志实现。
这个时候需要注意了,虽然代码中还是能正常打印日志,但可能想用的是log4j,其实用的是logback,一些log4j特有的功能可能就不起作用。本人就遇到过由于自动选择了logback实现类导致log4j日志文件无法创建的问题。

Java日志框架:slf4j作用及其实现原理
https://www.cnblogs.com/xrq730/p/8619156.html

为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API
https://www.hollischuang.com/archives/3000


classpath有多个日志实现时到底选择哪个?

当项目里同时存在多个日志框架(用来实现SLF4J)的时候, slf4j 会选择哪一个呢?
这个是通过jvm的类加载机制来控制的,会选择classpath 路径里面出现在前面的哪一个日志框架

jvm包括三种类加载器:
第一种:启动类加载器(Bootstrap ClassLoader),加载java的核心类。
第二种:扩展类加载器(Extension ClassLoader),负责加载jre的扩展目录中的jar包。
第三种:应用程序类加载器(Application ClassLoader),负责加载用户类路径(ClassPath)上所指定的类库,

jvm 加载包名和类名相同的类时,先加载classpath中jar路径放在前面的,包名类名都相同,那jvm没法区分了,如果使用ide一般情况下是会提示发生冲突而报错,若不报错,只有第一个包被引入(在classpath路径下排在前面的包),第二个包会在classloader加载类时判断重复而忽略。

比如同时有 logback 和 log4j 时, logback 的 StaticLoggerBinder 类路径按字母顺序更靠前,所以会被优先加载,这也就是为什么不排除 logback 的话总是会优先选择 logback

/Users/user/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class
/Users/user/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.10.0/log4j-slf4j-impl-2.10.0.jar!/org/slf4j/impl/StaticLoggerBinder.class

slf4j 的实现发现
https://segmentfault.com/a/1190000016245661


SLF4J MDC

SLF4J 本身提供了一个 MDC 类,提供四个静态方法
put(String key, String val), 将 key-val 放入当前线程的诊断上下文中Map中
get(String key), 从当前线程的诊断上下文Map中获取key的值
remove(String key), 从当前线程的诊断上下文Map中删除key
clear(), 删除当前线程的所有MDC值
这几个方法都是调用其中的 MDCAdapter mdcAdapter 实现类来进行具体的MDC操作的,这些实现类由具体的日志实现比如 log4j, logback 来提供。
slf4j 相当于具体日志实现的一个代理。

通过 StaticMDCBinder 绑定具体日志实现的MDC

提供 MDC 功能的日志实现,例如 log4j 和 logback ,都有一个 StaticMDCBinder 单例,提供一个 public MDCAdapter getMDCA() 方法供 slf4j 的 MDC 初始化时调用,来获取具体日志实现的 mdcAdapter

例如 log4j 的 StaticMDCBinder 单例如下

public final class StaticMDCBinder {
    public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();

    private StaticMDCBinder() {
    }

    public MDCAdapter getMDCA() {
        return new Log4jMDCAdapter();
    }

    public String getMDCAdapterClassStr() {
        return Log4jMDCAdapter.class.getName();
    }
}

SLF4J MDC 主要源码

package org.slf4j;

import java.io.Closeable;
import java.util.Map;

import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.helpers.Util;
import org.slf4j.impl.StaticMDCBinder;
import org.slf4j.spi.MDCAdapter;

public class MDC {
    static MDCAdapter mdcAdapter;

    // 从具体日志实现中获得 mdcAdapter
    private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
        try {
            return StaticMDCBinder.getSingleton().getMDCA();
        } catch (NoSuchMethodError nsme) {
            // binding is probably a version of SLF4J older than 1.7.14
            return StaticMDCBinder.SINGLETON.getMDCA();
        }
    }

    // 初始化适配器 mdcAdapter
    static {
        try {
            mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
        } catch (NoClassDefFoundError ncde) {
            mdcAdapter = new NOPMDCAdapter();
            String msg = ncde.getMessage();
            if (msg != null && msg.contains("StaticMDCBinder")) {
                Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
                Util.report("Defaulting to no-operation MDCAdapter implementation.");
                Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
            } else {
                throw ncde;
            }
        } catch (Exception e) {
            // we should never get here
            Util.report("MDC binding unsuccessful.", e);
        }
    }

    public static void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.put(key, val);
    }

    public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        return mdcAdapter.get(key);
    }

    public static void remove(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.remove(key);
    }

    public static void clear() {
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.clear();
    }
}

Slf4j MDC 使用和 基于 Logback 的实现分析
https://ketao1989.github.io/2015/04/29/LogBack-Implemention-And-Slf4j-Mdc/


Log4j MDC 实现

log4j 提供一个 Log4jMDCAdapter 类,作为 slf4j MDCAdapter 接口的实现,当系统的日志实现是 log4j 时,就是通过这个类实际进行 MDC 操作的。

package org.apache.logging.slf4j;

import java.util.Map;

import org.apache.logging.log4j.ThreadContext;
import org.slf4j.spi.MDCAdapter;

public class Log4jMDCAdapter implements MDCAdapter {

    @Override
    public void put(final String key, final String val) {
        ThreadContext.put(key, val);
    }

    @Override
    public String get(final String key) {
        return ThreadContext.get(key);
    }

    @Override
    public void remove(final String key) {
        ThreadContext.remove(key);
    }

    @Override
    public void clear() {
        ThreadContext.clearMap();
    }
}

主要跟踪一下其 put 方法实现 ThreadContext.put(key, val); ,发现最终是放到了 ThreadContextMap contextMap; 中,最终找到 DefaultThreadContextMap

public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyStringMap {
    // 是否使用 InheritableThreadLocal
    public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";

    private final ThreadLocal<Map<String, String>> localMap;

    public DefaultThreadContextMap(final boolean useMap) {
        this.useMap = useMap;
        this.localMap = createThreadLocalMap(useMap);
    }

    // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured.
    // (This method is package protected for JUnit tests.)
    static ThreadLocal<Map<String, String>> createThreadLocalMap(final boolean isMapEnabled) {
        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
        final boolean inheritable = managerProps.getBooleanProperty(INHERITABLE_MAP);
        if (inheritable) {
            return new InheritableThreadLocal<Map<String, String>>() {
                @Override
                protected Map<String, String> childValue(final Map<String, String> parentValue) {
                    return parentValue != null && isMapEnabled //
                    ? Collections.unmodifiableMap(new HashMap<>(parentValue)) //
                            : null;
                }
            };
        }
        // if not inheritable, return plain ThreadLocal with null as initial value
        return new ThreadLocal<>();
    }

    @Override
    public void put(final String key, final String value) {
        if (!useMap) {
            return;
        }
        Map<String, String> map = localMap.get();
        map = map == null ? new HashMap<String, String>(1) : new HashMap<>(map);
        map.put(key, value);
        localMap.set(Collections.unmodifiableMap(map));
    }
}

可以看到其中是个 ThreadLocal<Map<String, String>> localMap, 即线程本地Map,用来实现不同线程间的 MDC 隔离。

有个可配置的参数 isThreadContextMapInheritable 当是 true 时会使用 InheritableThreadLocal 代替 ThreadLocal,可以在子线程中继承父线程的变量。


Logback MDC 实现

package ch.qos.logback.classic.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.slf4j.spi.MDCAdapter;

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;

    // keeps track of the last operation performed
    final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();

    private Integer getAndSetLastOperation(int op) {
        Integer lastOp = lastOperation.get();
        lastOperation.set(op);
        return lastOp;
    }

    private boolean wasLastOpReadOrNull(Integer lastOp) {
        return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;
    }

    private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
        if (oldMap != null) {
            // we don't want the parent thread modifying oldMap while we are
            // iterating over it
            synchronized (oldMap) {
                newMap.putAll(oldMap);
            }
        }

        copyOnThreadLocal.set(newMap);
        return newMap;
    }

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }

        Map<String, String> oldMap = copyOnThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }

    public void remove(String key) {
        if (key == null) {
            return;
        }
        Map<String, String> oldMap = copyOnThreadLocal.get();
        if (oldMap == null)
            return;

        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp)) {
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.remove(key);
        } else {
            oldMap.remove(key);
        }
    }

    /**
     * Clear all entries in the MDC.
     */
    public void clear() {
        lastOperation.set(WRITE_OPERATION);
        copyOnThreadLocal.remove();
    }

    /**
     * Get the context identified by the <code>key</code> parameter.
     * <p/>
     */
    public String get(String key) {
        final Map<String, String> map = copyOnThreadLocal.get();
        if ((map != null) && (key != null)) {
            return map.get(key);
        } else {
            return null;
        }
    }
}

Slf4j MDC 使用和 基于 Logback 的实现分析
https://ketao1989.github.io/2015/04/29/LogBack-Implemention-And-Slf4j-Mdc/


线程池中使用MDC

比如我们有个 spring boot web 后端服务,其中通过 session_id 来跟踪一次请求,session_id 被放在 MDC 中打印到 log 中。
应用中有个公共的线程池 executoService, 任何地方都可以向里面提交异步任务执行, 如果想在多线程任务中打出来的 log 里面有和主线程相同的 session_id ,有以下几种方式:

每次提交任务时手动put

1、每次往线程池提交任务时手动拷贝父线程的 session_id 到子线程的 MDC 中,

ExecutorService executor = Executors.newFixedThreadPool(4);
String requestId = MDC.get("session_id");
executoService.execute(() -> {
  MDC.put("session_id", requestId);
  // 任务体
});

或者直接把整个 MDC 的 map 覆盖到子线程中

ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
executoService.execute(() -> {
  MDC.setContextMap(mdcContextMap);
  // 任务体
});

重写线程池的beforeExecute/afterExecute

2、重写 ThreadPoolExecutor 的 beforeExecute(Thread, Runnable) 和 afterExecute(Runnable, Throwable) 方法
在 beforeExecute 中拷贝父线程的 MDC 给到子线程
在 afterExecute 中清除

自定义线程池

3、实现自己的线程池

import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.*;

/**
 * A SLF4J MDC-compatible {@link ThreadPoolExecutor}.
 * <p/>
 * In general, MDC is used to store diagnostic information (e.g. a user's session id) in per-thread variables, to facilitate
 * logging. However, although MDC data is passed to thread children, this doesn't work when threads are reused in a
 * thread pool. This is a drop-in replacement for {@link ThreadPoolExecutor} sets MDC data before each task appropriately.
 * <p/>
 * Created by jlevy.
 * Date: 6/14/13
 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

    final private boolean useFixedContext;
    final private Map<String, Object> fixedContext;

    /**
     * Pool where task threads take MDC from the submitting thread.
     */
    public static MdcThreadPoolExecutor newWithInheritedMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(null, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    /**
     * Pool where task threads take fixed MDC from the thread that creates the pool.
     */
    @SuppressWarnings("unchecked")
    public static MdcThreadPoolExecutor newWithCurrentMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                          TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(MDC.getCopyOfContextMap(), corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue);
    }

    /**
     * Pool where task threads always have a specified, fixed MDC.
     */
    public static MdcThreadPoolExecutor newWithFixedMdc(Map<String, Object> fixedContext, int corePoolSize,
                                                        int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                                        BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(fixedContext, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    private MdcThreadPoolExecutor(Map<String, Object> fixedContext, int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.fixedContext = fixedContext;
        useFixedContext = (fixedContext != null);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> getContextForTask() {
        return useFixedContext ? fixedContext : MDC.getCopyOfContextMap();
    }

    /**
     * All executions will have MDC injected. {@code ThreadPoolExecutor}'s submission methods ({@code submit()} etc.)
     * all delegate to this.
     */
    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, getContextForTask()));
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, Object> context) {
        return new Runnable() {
            @Override
            public void run() {
                Map previous = MDC.getCopyOfContextMap();
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    runnable.run();
                } finally {
                    if (previous == null) {
                        MDC.clear();
                    } else {
                        MDC.setContextMap(previous);
                    }
                }
            }
        };
    }
}

How to use MDC with thread pools?
https://stackoverflow.com/questions/6073019/how-to-use-mdc-with-thread-pools


上一篇 VIM

下一篇 JWT(JSON Web Token)

阅读
评论
3,585
阅读预计18分钟
创建日期 2019-01-15
修改日期 2019-10-17
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论