logback的MDC機(jī)制
1. MDC 介紹:
? MDC(Mapped Diagnostic Context,映射調(diào)試上下文)磅废,即將一些運(yùn)行時(shí)的上下文數(shù)據(jù)通過logback打印出來纳像,是 一種方便在多線程條件下記錄日志的功能。和SiftingAppender一起拯勉,可以實(shí)現(xiàn)根據(jù)運(yùn)行時(shí)的上下文數(shù)據(jù)爹耗,將日志保存到不同的文件中。
2. demo:
2.1 logback配置文件:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%class:%line] - %X{logFileName} %X{logSign} %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE-INSERT" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator>
<key>logFileName</key>
<defaultValue>defName</defaultValue>
</discriminator>
<sift>
<appender name="FILE-${logFileName}" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>
D:/temp/downloadLogs/${logFileName}/gps.log
</File>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%msg%n</pattern>
</layout>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>
D:/temp/downloadLogs/${logFileName}/gps-%d{yyyyMMdd}.log
</fileNamePattern>
<maxHistory>2</maxHistory>
</rollingPolicy>
</appender>
</sift>
</appender>
<logger name="com.chelaile.log.insert" level = "INFO">
<appender-ref ref="FILE-INSERT"/>
<appender-ref ref="console"/>
</logger>
</configuration>
2.2 寫文件的線程:
package com.chelaile.log.insert;
import org.slf4j.MDC;
import java.util.Map;
public abstract class MdcRunnable implements Runnable {
/**
* 為了線程池中的線程在復(fù)用的時(shí)候也能獲得父線程的MDC中的信息谜喊,
* 子線程第一次初始化的時(shí)候沒事,因?yàn)橥ㄟ^ThreadLocal
* 已經(jīng)可以獲得MDC中的內(nèi)容了
*/
private final Map mdcContext = MDC.getCopyOfContextMap();
//ExecutorService.execute(new Runnable())的時(shí)候倦始,在Runnable構(gòu)造的時(shí)候斗遏,用這個(gè)方法得到一個(gè)Map,保存起來鞋邑,這時(shí)的context是父線程的诵次。
//然后在執(zhí)行run方法的時(shí)候账蓉,放到MDC中去——子線程的context map中去
@Override
public final void run() {
// 線程重用的時(shí)候,把父線程中的context map內(nèi)容帶入當(dāng)前線程的context map中逾一,
// 因?yàn)榫€程已經(jīng)初始化過了铸本,不會(huì)像初始化時(shí)那樣通過拷貝父線程ThreadLocal到子線程
// 的ThreadLocal操作來完成線程間context map的傳遞。
// 真正執(zhí)行到這個(gè)run方法的時(shí)候遵堵,已經(jīng)到了子線程中了箱玷,所以要在初始化的時(shí)候用
// MDC.getCopyOfContextMap()來獲得父線程contest map,那時(shí)候還在父線程域中
if (mdcContext != null) {
MDC.setContextMap(mdcContext);//有了這一步其實(shí)不用MDC.clear陌宿,因?yàn)檫@一步會(huì)將子線程中的context map重置為父線程的context map
// //源代碼:
// public void setContextMap(Map<String, String> contextMap) {
// lastOperation.set(WRITE_OPERATION);
// //將原來的
// Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
// newMap.putAll(contextMap);
//
// // the newMap replaces the old one for serialisation's sake
// copyOnThreadLocal.set(newMap);
// }
}
try {
runWithMdc();
} finally {
// MDC.clear();//如果都是用new Thread方法建立的線程可以不用加锡足,因?yàn)橹缶€程會(huì)消亡。
//如果用ThreadPool線程池的話壳坪,線程是可以重用的舶得,如果之前的線程的MDC內(nèi)容沒有清除掉的話,
// 再次重線程池中獲取到這個(gè)線程爽蝴,會(huì)取出之前的數(shù)據(jù)(臟數(shù)據(jù))沐批,會(huì)導(dǎo)致一些不可預(yù)期的錯(cuò)誤,
// 所以當(dāng)前線程結(jié)束后一定要清掉蝎亚。
}
}
protected abstract void runWithMdc();
}
package com.chelaile.log.insert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class LogRunnable extends MdcRunnable {
private volatile boolean flag = true;
private static final Logger log = LoggerFactory.getLogger(LogRunnable.class);
private String code;
// 阻塞隊(duì)列
private LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
public LogRunnable(String code) {
this.code = code;
}
@Override
protected void runWithMdc() {
String str;
System.out.println("code=" + code + ",啟動(dòng),logFileName=" + MDC.get("logFileName"));
while (flag || queue.size() > 0) {
try {
str = queue.poll(5, TimeUnit.SECONDS);
if (str == null || "null".equals(str) || "".equals(str)) {
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
continue;
}
log.info(str);
MDC.put("logFileName",code);
}
System.out.println("code=" + code + ",結(jié)束,logFileName=" + MDC.get("logFileName"));
}
public void close() {
this.flag = false;
}
public void setQueue(String str) {
queue.add(str);
}
public String getCode() {
return code;
}
}
2.3 測(cè)試代碼:
package com.chelaile.test;
import com.chelaile.SpringStart;
import com.chelaile.log.insert.LogRunnable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.MDC;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringStart.class)
public class LogRunnableTest {
private Map<Integer, LogRunnable> logRunnableMap = new HashMap<>();
@Test
public void testLog() {
ExecutorService logExecService = Executors.newFixedThreadPool(1);
MDC.put("logFileName","默認(rèn)");
MDC.put("logSign","標(biāo)記");
for (int i = 0; i < 5; i++) {
LogRunnable logRunnable = new LogRunnable("test" + i);
logExecService.execute(logRunnable);
logRunnableMap.put(i,logRunnable);
}
for (int i = 0; i < 5; i++) {
LogRunnable logRunnable = logRunnableMap.get(i);
logRunnable.setQueue("測(cè)試數(shù)據(jù)" + i);
logRunnable.setQueue("我是測(cè)試" + i);
logRunnable.setQueue("測(cè)試測(cè)試" + i);
logRunnable.close();
}
while(true){} //測(cè)試的時(shí)候九孩,不加 while(true){} 的話,由于主線程結(jié)束颖对,日志沒有寫入文件線程就終止了
}
}
2.4輸出:
2.4.1控制臺(tái)輸出:
code=test0,啟動(dòng),logFileName=默認(rèn)
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - 默認(rèn) 標(biāo)記 測(cè)試數(shù)據(jù)0
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test0 標(biāo)記 我是測(cè)試0
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test0 標(biāo)記 測(cè)試測(cè)試0
code=test0,結(jié)束,logFileName=test0
code=test1,啟動(dòng),logFileName=默認(rèn)
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - 默認(rèn) 標(biāo)記 測(cè)試數(shù)據(jù)1
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test1 標(biāo)記 我是測(cè)試1
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test1 標(biāo)記 測(cè)試測(cè)試1
code=test1,結(jié)束,logFileName=test1
code=test2,啟動(dòng),logFileName=默認(rèn)
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - 默認(rèn) 標(biāo)記 測(cè)試數(shù)據(jù)2
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test2 標(biāo)記 我是測(cè)試2
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test2 標(biāo)記 測(cè)試測(cè)試2
code=test2,結(jié)束,logFileName=test2
code=test3,啟動(dòng),logFileName=默認(rèn)
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - 默認(rèn) 標(biāo)記 測(cè)試數(shù)據(jù)3
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test3 標(biāo)記 我是測(cè)試3
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test3 標(biāo)記 測(cè)試測(cè)試3
code=test3,結(jié)束,logFileName=test3
code=test4,啟動(dòng),logFileName=默認(rèn)
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - 默認(rèn) 標(biāo)記 測(cè)試數(shù)據(jù)4
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test4 標(biāo)記 我是測(cè)試4
2018-07-19 15:44:12 INFO [com.chelaile.log.insert.LogRunnable:38] - test4 標(biāo)記 測(cè)試測(cè)試4
code=test4,結(jié)束,logFileName=test4
2.4.2生成的日志目錄結(jié)構(gòu):
3.遇到的問題:
? 第一次logback.xml文件沒有配置layout捻撑,因?yàn)橹幌雽懭雓afka中的數(shù)據(jù),不想加多余的東西缤底,導(dǎo)致測(cè)試內(nèi)容沒能寫入文件顾患,加上之后就OK了
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%msg%n</pattern>
</layout>
4. MDC分析 :
Slf4j MDC :
? 對(duì)外接口,就是對(duì)參數(shù)進(jìn)行校驗(yàn)个唧,然后調(diào)用MDCAdapter的方法實(shí)現(xiàn)江解。
Logback MDC :
? LogbackMDCAdapter類實(shí)現(xiàn)MDCAdapter接口,實(shí)現(xiàn) put徙歼、get犁河、remove等方法。
? copyOnThreadLocal:存儲(chǔ)每個(gè)線程的多個(gè)變量
? 當(dāng)在logback.xml中配置了%X{key} 或 SiftingAppender的<discriminator>的<key>魄梯,在需要輸出日志的時(shí)候桨螺,從MDC中獲取對(duì)應(yīng)的key值,然后append到日志字符串中或生成文件路徑酿秸,然后輸出灭翔。
5. ThreadLocal 實(shí)現(xiàn)分析:
? 在每個(gè)線程Thread內(nèi)部有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個(gè)threadLocals就是用來存儲(chǔ)實(shí)際的變量副本的辣苏,鍵值為當(dāng)前ThreadLocal變量肝箱,value為變量副本(即T類型的變量)哄褒。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
? 初始時(shí),在Thread里面煌张,threadLocals為空呐赡,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化骏融,并且以當(dāng)前ThreadLocal變量為鍵值链嘀,以ThreadLocal要保存的副本變量為value,存到threadLocals绎谦。
ThreadLocal類:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//key用this管闷,是因?yàn)橥粋€(gè)線程中如果有多個(gè)ThreadLocal,都會(huì)存儲(chǔ)在該map中
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocal總結(jié):
1.實(shí)際的通過ThreadLocal創(chuàng)建的副本是存儲(chǔ)在每個(gè)線程自己的threadLocals中的窃肠;
2.為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對(duì)象包个,因?yàn)槊總€(gè)線程中可有多個(gè)threadLocal變量