задний план
В текущей системе микросервиса есть много сервисных приложений, а цепочка вызовов сложна, а сложность соответствующего устранения неполадок также увеличивается. Когда произойдет исключение в приложении, нам нужно быстро найти журнал проблемы, что требует от нас, чтобы проследить ссылку запроса и генерировать идентификатор, который может идентифицировать весь жизненный цикл запроса, когда запрос достигает системы.
MDC (отображенные диагностические контексты) Введение
MDC — важный инструмент для реализации распределенной многопоточной передачи данных журнала в системе журнала, подобной Slf4J.Пользователи могут использовать MDC для печати некоторых данных контекста времени выполнения.В настоящее время только log4j и logback обеспечивают встроенную поддержку MDC.
Анализ исходного кода
MDC поддерживает контекст потокаMap<String,String>
Свойства, можно понимать как контейнеры на уровне нитей в обратном плане,%X{key}
Получить значение в контексте MDCИнтерфейс предоставлен MDC
public interface MDCAdapter {
// 获取当前线程MDC上下文中指定key的值
void put(String var1, String var2);
// 往当前线程MDC上下文中
String get(String var1);
// 移除当前线程MDC上下文中指定key的键值
void remove(String var1);
// 清空MDC上下文
void clear();
// 获取MDC上下文
Map<String, String> getCopyOfContextMap();
// 设置MDC上下文呢
void setContextMap(Map<String, String> var1);
}
Реализация логбэка LogbackMDCAdapter
public class LogbackMDCAdapter implements MDCAdapter {
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
private static final int WRITE_OPERATION = 1;
private static final int MAP_COPY_OPERATION = 2;
final ThreadLocal<Integer> lastOperation = new ThreadLocal();
public LogbackMDCAdapter() {
}
...
Вы можете видеть, что LogbackMDC объявляет карту типа ThreadLocal. ThreadLocal предоставляет экземпляры локальных переменных потока. Разница между ним и обычными переменными заключается в том, что каждый поток, использующий переменную, будет инициализировать полностью независимую копию экземпляра, а это означает, что переменные ThreadLocal изолированы между потоками и могут совместно использоваться методами или классами.
put() в LogbackMDCAdapter
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
} else {
Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
Integer lastOp = this.getAndSetLastOperation(1);
if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
oldMap.put(key, val);
} else {
Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
}
}
}
Проверьте и установите значение ключа для контейнера ThreadLocal.
duplicateAndInsertNewMap()
private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
if (oldMap != null) {
synchronized(oldMap) {
newMap.putAll(oldMap);
}
}
this.copyOnThreadLocal.set(newMap);
return newMap;
}
Создайте потокобезопасный HashMap в качестве контейнера и поместите его в ThreadLocal.
Схема и реализация
Объявить фильтр для перехвата запросов
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean registFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DyeFilter());
registration.addUrlPatterns("/*");
registration.setName("DyeFilter");
registration.setOrder(1);
return registration;
}
}
Создайте объект контекста и назначьте его MDC
@WebFilter(filterName = "DyeFilter")
public class DyeFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
initGlobalContext(request);
try {
chain.doFilter(req, resp);
} finally {
ContextUtil.clearContext();
}
}
private static void initGlobalContext(HttpServletRequest servletRequest) {
GlobalContext context = new GlobalContext();
String contextStr = servletRequest.getHeader(ContextConstant.REQUEST_CONTEXT);
if (!StringUtils.isEmpty(contextStr)){
context = JSON.parseObject(contextStr,GlobalContext.class);
}else{
context.setTraceId(UUID.randomUUID().toString());
context.setClientIp(IpUtil.getClientAddress(servletRequest));
}
ContextUtil.setCurrentContext(context);
}
@Override
public void init(FilterConfig config){
}
@Override
public void destroy() {
}
}
Создайте GlobalContext в бизнес-входе, и в последующей цепочке вызовов построенный GlobalContext будет получен из заголовка запроса.
public class ContextUtil {
private static ThreadLocal<GlobalContext> currentThreadLocal = ThreadLocal.withInitial(GlobalContext::new);
public static void setCurrentContext(GlobalContext context) {
currentThreadLocal.set(context);
String traceId = context.getTraceId();
if (traceId != null && traceId.length() > 0 && MDC.get(ContextConstant.TRACK_ID) == null) {
MDC.put(ContextConstant.TRACK_ID, traceId);
}
}
public static GlobalContext getCurrentContext() {
return currentThreadLocal.get();
}
public static void clearContext() {
MDC.clear();
currentThreadLocal.remove();
}
}
Получите GlobalContext и передайте его через заголовок запроса
public class FeignConfig {
@Bean
public RequestInterceptor header(){
return this::setRequestHeader;
}
private void setRequestHeader(RequestTemplate requestTemplate){
GlobalContext context = ContextUtil.getCurrentContext();
if (context!=null){
requestTemplate.header(ContextConstant.REQUEST_CONTEXT, JSONParser.quote(JSON.toJSONString(context)));
}
}
}
Вот только пример звонка через Feign.Типов звонков между сервисами много.Главная идея в том,чтобы прозрачно передать GlobalContext на следующий сервис.
Переменные конфигурации контейнера MDC в обратном плане файла конфигурации%X{trackId}
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %X{trackId} [%15.15t] %class{39}.%method[%L] : %m%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>f
</encoder>
</appender>l
ДЕМО адрес:GitHub.com/Chen Xuan код…