一个工程中可能提供很多的资源(或者说提供给外部很多的URL访问),而工程一般是分层处理,Controller-->Service-->DAO(HTTP请求其他的资源)的处理顺序。有时候,我们需要根据日志打印去看一下某用户的这次请求到底是发生了什么错误。我们知道系统不可能只有一个人在访问,假如很多人在访问的话,日志打印的是很乱的,想要找到自己需要的信息非常难,被淹没在巨量的日志中了。此时,我们想如果,能通过一个字符串就能将一次请求调用了哪些方法按照顺序搜索出来就好了。而Slf4j的MDC ( Mapped Diagnostic Contexts )机制就是专门为了此需求而生的。其使用方式非常简单,配置文件的pattern中中添加一个{key},在请求方法入口设置一个key=某字符串,logger日志就能输出此字符串。logger的所有日志方法不需要做任何改动。如下所示。
<pattern>%contextName: %X{METHOD-INVOKE-KEY} : %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'(Caller(.+?)(?=\())|\r|\n|\s*|\t', ''} - %msg%n</pattern>
/**
* @author xiongshiyan at 2018/7/5
*/
public class LoggerMDCTest {
private static final Logger logger = LoggerFactory.getLogger(LoggerMDCTest.class);
@Test
public void testMDC(){
for (int i=0 ; i<10 ; i++) {
new Thread(this::fun).start();
}
ThreadUtil.sleeps(1);
}
private void fun(){
String key = "METHOD-INVOKE-KEY";
//方法拦截器入口处设置,logback日志配置需要设置 %X{threadUUID}
MDC.put(key , CommonUtil.getUUID());
//使用的时候,所有调用链上的打印都会加上threadUUID
fun1();
//子线程不会被追踪
new Thread(()->logger.error("ccccccccccccccccc")).start();
//拦截器finally中需要清除
MDC.remove(key);
//MDC.clear();
}
private void fun1() {
logger.info("fun1");
fun2();
}
private void fun2() {
logger.info("fun2");
fun3();
}
private void fun3() {
logger.error("fun3");
}
}
其打印如下
test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.938 [Thread-0] INFO (LoggerMDCTest.java:35) - fun1
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.938 [Thread-5] INFO (LoggerMDCTest.java:35) - fun1
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] INFO (LoggerMDCTest.java:35) - fun1
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] INFO (LoggerMDCTest.java:35) - fun1
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] INFO (LoggerMDCTest.java:40) - fun2
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] INFO (LoggerMDCTest.java:40) - fun2
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] ERROR (LoggerMDCTest.java:45) - fun3
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] ERROR (LoggerMDCTest.java:45) - fun3
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.938 [Thread-8] INFO (LoggerMDCTest.java:35) - fun1
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.938 [Thread-7] INFO (LoggerMDCTest.java:35) - fun1
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.938 [Thread-6] INFO (LoggerMDCTest.java:35) - fun1
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.938 [Thread-1] INFO (LoggerMDCTest.java:35) - fun1
test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.938 [Thread-0] INFO (LoggerMDCTest.java:40) - fun2
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.954 [Thread-6] INFO (LoggerMDCTest.java:40) - fun2
test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.954 [Thread-0] ERROR (LoggerMDCTest.java:45) - fun3
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.954 [Thread-6] ERROR (LoggerMDCTest.java:45) - fun3
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.938 [Thread-5] INFO (LoggerMDCTest.java:40) - fun2
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.954 [Thread-5] ERROR (LoggerMDCTest.java:45) - fun3
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.938 [Thread-9] INFO (LoggerMDCTest.java:35) - fun1
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.938 [Thread-2] INFO (LoggerMDCTest.java:35) - fun1
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.954 [Thread-9] INFO (LoggerMDCTest.java:40) - fun2
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.954 [Thread-2] INFO (LoggerMDCTest.java:40) - fun2
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.954 [Thread-9] ERROR (LoggerMDCTest.java:45) - fun3
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.954 [Thread-2] ERROR (LoggerMDCTest.java:45) - fun3
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.954 [Thread-8] INFO (LoggerMDCTest.java:40) - fun2
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.954 [Thread-1] INFO (LoggerMDCTest.java:40) - fun2
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.954 [Thread-8] ERROR (LoggerMDCTest.java:45) - fun3
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.954 [Thread-1] ERROR (LoggerMDCTest.java:45) - fun3
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.954 [Thread-7] INFO (LoggerMDCTest.java:40) - fun2
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.954 [Thread-7] ERROR (LoggerMDCTest.java:45) - fun3
test: : 2018-07-20 17:18:09.954 [Thread-10] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.954 [Thread-11] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.954 [Thread-12] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.954 [Thread-14] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-15] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-16] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-18] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-19] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-13] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test: : 2018-07-20 17:18:09.969 [Thread-17] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
由上可知:同一个线程的fun1、fun2、fun3打印了相同的一个随机字符串。
MDC的基本原理是:通过一个ThreadLocal保存设置的key值,在打印的时候从ThreadLocal中获取到打印出来。由此可知,如果一个请求中的某些方法调用是在另外的线程中执行,那MDC是获取不到该值的。vertx-web的handler就是如此。
在我们的工程中,一般都是在所有的请求之前添加一个类似拦截器的组件,在该组件中设置一个随机字符串,然后开始真正的调用(这些调用中就能打印出此随机字符串),然后finally方法中清除该字符串,防止内存泄露。一个例子:
try {
//所有handler之前设置一个tag
methodInvokeKeySetting(vertxRequest.getHeader(METHOD_INVOKE_KEY));
//方法调用链开始
done.handle(null);
} catch (Exception e) {
throw e;
} finally {
//finally删除该key
MDC.remove(METHOD_INVOKE_KEY);
}
/**
* 日志串联header设置,方法调用链都加上此tag,并透明传递到http
*/
private void methodInvokeKeySetting(String methodInvokeKey) {
logger.info("METHOD_INVOKE_KEY header : " + methodInvokeKey);
String string = methodInvokeKey;
if(null == string || "".equals(string)){
string = CommonUtil.randomString(8);
}
logger.info("将要设置的随机串为 : " + string);
MDC.put(METHOD_INVOKE_KEY , string);
StringMapInfoHolderUtil.put(METHOD_INVOKE_KEY , string);
}
工程与工程之间如何串联呢?通过header透明传递该字符串。
如上所示:
StringMapInfoHolderUtil.put(METHOD_INVOKE_KEY , string);方法也是利用ThreadLocal保存一个值,然后我们在http请求的时候从threadlocal中获取该值设置到header,在被请求的工程中获取该header即可。如此两个工程间就可以透明传递一个串联的字符串了。
//在所有的handler执行之前先设置header
before(RoutingContext routingContext): handlers(routingContext){
try {
//所有handler之前设置一个tag
String header = routingContext.request().getHeader(METHOD_INVOKE_KEY);
logger.info("METHOD_INVOKE_KEY header : " + header);
String string = header;
if(null == string || "".equals(string)){
string = CommonUtil.randomString(8);
}
logger.info("将要设置的随机串为 : " + string);
MDC.put(METHOD_INVOKE_KEY , string);
} catch (Exception e){
logger.error(e.getMessage() , e);
}
}
//finally 删除
after(RoutingContext routingContext): handlers(routingContext){
try {
MDC.remove(METHOD_INVOKE_KEY);
} catch (Exception e){
logger.error(e.getMessage() , e);
}
}
以上也演示了一种解决vertx的两个handler运行在不同的线程上的一种方案,通过aspectj增强handler,这样两个handler就能获取到同一个header。
public class Test1Handler implements AbstractHandler{
private static final Logger logger = LoggerFactory.getLogger(Test1Handler.class);
@Override
public void handle(RoutingContext routingContext) {
logger.info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
System.out.println("Test1Handler : " + Thread.currentThread().getName() + ":" +routingContext.request().getHeader("METHOD-INVOKE-KEY"));
routingContext.next();
}
}
/**
* @author xiongshiyan at 2018/7/20 , contact me with email [email protected] or phone 15208384257
*/
public class Test2Handler implements AbstractHandler{
private static final Logger logger = LoggerFactory.getLogger(Test2Handler.class);
@Override
public void handle(RoutingContext routingContext) {
logger.info("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
System.out.println("Test2Handler : " + Thread.currentThread().getName() + ":" +routingContext.request().getHeader("METHOD-INVOKE-KEY"));
ccccc();
routingContext.response().end("结束了");
}
private void ccccc() {
logger.info("ccccccccccccccccccccccccccccccccccccccccccc");
ddddd();
}
private void ddddd() {
logger.info("dddddddddddddddddddddddddddddddddddddddddddd");
}
}
打印如下:
local: 2018-07-20 17:38:48.845 [vert.x-worker-thread-0] INFO (PreHandlerAspectJ.aj:22) - METHOD_INVOKE_KEY header : header_from_other
local: 2018-07-20 17:38:48.847 [vert.x-worker-thread-0] INFO (PreHandlerAspectJ.aj:27) - 将要设置的随机串为 : header_from_other
local: header_from_other 2018-07-20 17:38:48.848 [vert.x-worker-thread-0] INFO (Test1Handler.java:15) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Test1Handler : vert.x-worker-thread-0:header_from_other
local: 2018-07-20 17:38:48.863 [vert.x-worker-thread-1] INFO (PreHandlerAspectJ.aj:22) - METHOD_INVOKE_KEY header : header_from_other
local: 2018-07-20 17:38:48.867 [vert.x-worker-thread-1] INFO (PreHandlerAspectJ.aj:27) - 将要设置的随机串为 : header_from_other
local: header_from_other 2018-07-20 17:38:48.868 [vert.x-worker-thread-1] INFO (Test2Handler.java:15) - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Test2Handler : vert.x-worker-thread-1:header_from_other
local: header_from_other 2018-07-20 17:38:48.869 [vert.x-worker-thread-1] INFO (Test2Handler.java:24) - ccccccccccccccccccccccccccccccccccccccccccc
local: header_from_other 2018-07-20 17:38:48.869 [vert.x-worker-thread-1] INFO (Test2Handler.java:29) - dddddddddddddddddddddddddddddddddddddddddddd
两个handler运行在不同的线程,但是获取到同一个header。
需要注意的是,MDC只在logback和log4j中得到了比较好的支持。
附:通用的一个threadlocal工具类
/**
* 在同一个线程保存信息的工具类
* @author xiongshiyan at 2017/12/27
*/
public class InfoHolder<T> {
protected ThreadLocal<T> holder = new ThreadLocal<>();
/**
* 往当前线程添加
* @param t t
*/
public void set(T t){
holder.set(t);
}
/**
* 获取当前线程的
*/
public T get(){
return holder.get();
}
/**
* 清空
*/
public void clear(){
holder.remove();
}
}
map类型的子类
/**
* Map类型值的工具类
* @author xiongshiyan at 2017/12/27
*/
public class MapInfoHolder<K,V> extends InfoHolder<Map<K,V>> {
/**
* 往当前线程保存一对值
* @param key key
* @param value value
*/
public void add(K key ,V value){
Map<K, V> map = holder.get();
if(null == map){
map = new HashMap<>();
}
map.put(key,value);
holder.set(map);
}
/**
* 根据key获取当前线程的值
* @param key key
*/
public V get(K key){
Map<K, V> map = holder.get();
if (null == map){
return null;
}
return map.get(key);
}
public void remove(K key){
Map<K, V> map = holder.get();
if (null == map){
return ;
}
map.remove(key);
}
}
设计了两个工具类:一个是可以设置任意类型值的通用InfoHolder,一个是可以设置key-value值的。
/**
* 任意类型值的工具类
* @author xiongshiyan at 2017/12/27
*/
public class NormalInfoHolderUtil {
private static final InfoHolder INFO_HOLDER = new InfoHolder();
@SuppressWarnings("unchecked")
public static <T> void set(T object){
INFO_HOLDER.set(object);
}
@SuppressWarnings("unchecked")
public static <T> T get(){
return (T) INFO_HOLDER.get();
}
public static void clear(){
INFO_HOLDER.clear();
}
}
/**
* Map<String,String></>类型值的工具类
* @author xiongshiyan at 2017/12/27
*/
public class StringMapInfoHolderUtil {
private static final MapInfoHolder<String , String> MAP_INFO_HOLDER = new MapInfoHolder<>();
public static void put(String key , String value){
MAP_INFO_HOLDER.add(key , value);
}
public static String get(String key){
return MAP_INFO_HOLDER.get(key);
}
public static void remove(String key){
MAP_INFO_HOLDER.remove(key);
}
public static void clear(){
MAP_INFO_HOLDER.clear();
}
}
以上两个工具类基本就能满足常用的threadlocal相关需求了。