最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring Boot中的日志log原理與自定義日志格式

 更新時(shí)間:2026年02月25日 16:57:18   作者:圓師傅  
這篇文章主要介紹了Spring Boot中的日志log原理與自定義日志格式,本文將介紹一種日志追蹤的方式,來全鏈路追蹤請(qǐng)求的處理,并設(shè)置相應(yīng)的格式,輸出更加直觀、易處理的日志,需要的朋友可以參考下

在企業(yè)級(jí)微服務(wù)架構(gòu)中,日志不僅僅是代碼的運(yùn)行記錄,更是排查問題的“第一現(xiàn)場(chǎng)”。尤其在復(fù)雜的分布式鏈路中,如果沒有一個(gè)唯一標(biāo)識(shí)串聯(lián)起所有的調(diào)用,面對(duì)海量日志,排查報(bào)錯(cuò)簡(jiǎn)直如同大海撈針。
本文將介紹一種日志追蹤的方式,來全鏈路追蹤請(qǐng)求的處理。并設(shè)置相應(yīng)的格式,輸出更加直觀、易處理的日志。

1. Spring boot的日志原理

為什么我們?cè)趩?dòng)Spring boot應(yīng)用的時(shí)候,控制臺(tái)能夠輸出日志。這里的日志是如何輸出的呢?
Spring Boot 巧妙地利用了事件發(fā)布/監(jiān)聽機(jī)制(Event/Listener) 來盡早拉起日志系統(tǒng)。
Spring Application在Run的過程中,會(huì)有不同的狀態(tài),在到達(dá)不同的狀態(tài)后,SpringApplicationRunListener會(huì)監(jiān)聽相應(yīng)的事件:

public interface SpringApplicationRunListener {
	default void starting() {
	}
	default void environmentPrepared(ConfigurableEnvironment environment) {
	}
	default void contextPrepared(ConfigurableApplicationContext context) {
	}
	default void contextLoaded(ConfigurableApplicationContext context) {
	}
	default void started(ConfigurableApplicationContext context) {
	}
	default void running(ConfigurableApplicationContext context) {
	}
	default void failed(ConfigurableApplicationContext context, Throwable exception) {
	}
}

這里一共有7個(gè)方法,分別對(duì)應(yīng)SpringApplication在Run過程中對(duì)應(yīng)的7個(gè)狀態(tài)。如果我們想要在某個(gè)狀態(tài)后自定義一些操作,可以實(shí)現(xiàn)相應(yīng)的接口。

EventPublishingRunListener這個(gè)類就是實(shí)現(xiàn)了上述SpringApplicationRunListener的具體類,在不同的場(chǎng)景下,產(chǎn)生并發(fā)布不同的ApplicationRunEvent。而這些Event的LIstener,將會(huì)對(duì)相應(yīng)的Event做出相應(yīng)的處理。

這里,日志系統(tǒng)的主角就登場(chǎng)了org.springframework.boot.context.logging.LoggingApplicationListener,這個(gè)類實(shí)現(xiàn)了GenericApplicationListener,而GenericApplicationListener又繼承了ApplicationListener,而ApplicationLIstener的方法:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	/**
	 * Handle an application event.
	 * @param event the event to respond to
	 */
	void onApplicationEvent(E event);
}

正是處理這個(gè)Event的地方。

翻看Spring boot的源碼,啟動(dòng)主要分兩步:

  • 創(chuàng)建一個(gè)SpringApplication對(duì)象
  • run方法
    在new SpringApplication()方法中,通過spi方式,設(shè)置了org.springframework.boot.context.logging.LoggingApplicationListener,而在這個(gè)Listener中的onApplicationEvent方法中,接受ApplicationEvent作為參數(shù),進(jìn)行處理:
@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationStartingEvent) {
			onApplicationStartingEvent((ApplicationStartingEvent) event);
		}
		else if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		else if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent((ApplicationPreparedEvent) event);
		}
		else if (event instanceof ContextClosedEvent
				&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
			onContextClosedEvent();
		}
		else if (event instanceof ApplicationFailedEvent) {
			onApplicationFailedEvent();
		}
	}

從源碼可以看到,在眾多ApplicationEvent中,LoggingApplicationListener主要關(guān)注這5個(gè)Event:

  • ApplicationStartingEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationPreparedEvent
  • ContextClosedEvent
  • ApplicationFailedEvent

1.1 ApplicationStartingEvent

從名字就可以知道,ApplicationStartingEvent是Spring上下文正在啟動(dòng)的事件,那么這個(gè)事件是在什么時(shí)候產(chǎn)生的呢?
翻看源碼

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}
		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

在代碼中,首先獲取到所有的ApplicationRunListener,然后調(diào)用其starting()方法。

那么這個(gè)ApplicationStartingEvent事件產(chǎn)生時(shí),LoggingApplicationListener的onApplicationEvent做了哪些事呢?

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
		this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		this.loggingSystem.beforeInitialize();
	}

第一行代碼就是根據(jù)classLoader獲取日志系統(tǒng)。Spring中支持多個(gè)不同的日志系統(tǒng),支持logback,java log等。然后再根據(jù)不同的日志系統(tǒng),做相應(yīng)的初始化操作。

那么,是如何獲得哪些不同的日志系統(tǒng)的呢?

/**
	 * Detect and return the logging system in use. Supports Logback and Java Logging.
	 * @param classLoader the classloader
	 * @return the logging system
	 */
	public static LoggingSystem get(ClassLoader classLoader) {
		String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
		if (StringUtils.hasLength(loggingSystem)) {
			if (NONE.equals(loggingSystem)) {
				return new NoOpLoggingSystem();
			}
			return get(classLoader, loggingSystem);
		}
		return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
				.map((entry) -> get(classLoader, entry.getValue())).findFirst()
				.orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
	}
	private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {
		try {
			Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);
			Constructor<?> constructor = systemClass.getDeclaredConstructor(ClassLoader.class);
			constructor.setAccessible(true);
			return (LoggingSystem) constructor.newInstance(classLoader);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

從DEBUG的結(jié)果可以看到,當(dāng)前是默認(rèn)的SYSTEMS中的日志系統(tǒng),有3個(gè)。那么這3個(gè)日志系統(tǒng)是如何加載進(jìn)來的呢?

private static final Map<String, String> SYSTEMS;

	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}

靜態(tài)變量與靜態(tài)代碼塊!可以看到,這個(gè)SYSTEMS是一個(gè)不可變的Map,其中設(shè)置3個(gè)不同的值,正是我們調(diào)試時(shí)看到的3個(gè)日志系統(tǒng)。

Spring boot在過濾后調(diào)用findFirst方法,第一個(gè)是logbackLoggingSystem,這個(gè)正是Springboot中的默認(rèn)日志系統(tǒng)。

那日志系統(tǒng)的初始化之前都做哪些事呢?
首先就是要獲取日志上下文LoggerContext,這里是使用Logback的日志系統(tǒng)獲取的,如果獲取到的Context不上Logback的,會(huì)有相應(yīng)提醒:

private LoggerContext getLoggerContext() {
		ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
		Assert.isInstanceOf(LoggerContext.class, factory,
				String.format(
						"LoggerFactory is not a Logback LoggerContext but Logback is on "
								+ "the classpath. Either remove Logback or the competing "
								+ "implementation (%s loaded from %s). If you are using "
								+ "WebLogic you will need to add 'org.slf4j' to "
								+ "prefer-application-packages in WEB-INF/weblogic.xml",
						factory.getClass(), getLocation(factory)));
		return (LoggerContext) factory;
	}

由于Spring boot的日志是Slf4J+Logback的日志系統(tǒng),因此這里一些通用的操作,就交給到Slf4JLoggingSystem去做,比如configureJdkLoggingBridgeHandler等。

1.2 ApplicationEnvironmentPreparedEvent

顧名思義,ApplicationEnvironmentPreparedEvent是在EnvironmentPrepared的時(shí)候觸發(fā)產(chǎn)生的事件:

LIstener中又是如何處理的呢?

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		if (this.loggingSystem == null) {
			this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		}
		initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
	}
	/**
	 * Initialize the logging system according to preferences expressed through the
	 * {@link Environment} and the classpath.
	 * @param environment the environment
	 * @param classLoader the classloader
	 */
	protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
		new LoggingSystemProperties(environment).apply();
		this.logFile = LogFile.get(environment);
		if (this.logFile != null) {
			this.logFile.applyToSystemProperties();
		}
		this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
		initializeEarlyLoggingLevel(environment);
		initializeSystem(environment, this.loggingSystem, this.logFile);
		initializeFinalLoggingLevels(environment, this.loggingSystem);
		registerShutdownHookIfNecessary(environment, this.loggingSystem);
	}

由于這個(gè)時(shí)候,已經(jīng)拿到environment,那么一些環(huán)境變量相關(guān)的值都可以設(shè)置到日志系統(tǒng)中了。比如日志級(jí)別。它從準(zhǔn)備好的 Environment 中提取與日志相關(guān)的配置(如 logging.level.*, logging.config, logging.file.name 等)。

初始化日志系統(tǒng)(LoggingSystem.initialize())。
真正去尋找并解析具體的配置文件(比如你自定義的 logback-spring.xml)。
這是日志系統(tǒng)真正開始“工作”的分水嶺。在這個(gè)事件之后,應(yīng)用打印的日志就會(huì)完全按照你 logback-spring.xml 中配置的格式(比如帶上 %X{traceId})輸出了。

1.3 ApplicationPreparedEvent

上面是Application的Environment已經(jīng)準(zhǔn)備好,這個(gè)就是SpringApplication已經(jīng)準(zhǔn)備好的事件了,仍舊是在run這段代碼中:

日志系統(tǒng)的處理:

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
		ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
		if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
			beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
		}
		if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
			beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
		}
		if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
			beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
		}
	}

這一步主要是將日志系統(tǒng)與 Spring 容器打通。

它會(huì)將當(dāng)前正在使用的 LoggingSystem 實(shí)例、LogFile 實(shí)例等,注冊(cè)為 Spring 容器中的單例 Bean(Singleton)。

為什么要這樣做?是為了讓后續(xù) Spring 容器里的其他 Bean(比如 Spring Boot Actuator)能夠直接 @Autowired 注入并操作底層的日志系統(tǒng)。

1.4 ContextClosedEvent 和ApplicationFailedEvent

這里已經(jīng)是日志清理相應(yīng)的資源了
應(yīng)用正常關(guān)閉(ContextClosedEvent)或者啟動(dòng)過程中拋出異常導(dǎo)致失?。ˋpplicationFailedEvent)。
LoggingApplicationListener 的處理動(dòng)作:調(diào)用 LoggingSystem.cleanUp()。

2.Spring boot自帶日志的局限性

  • 缺乏上下文:無法區(qū)分當(dāng)前日志屬于哪個(gè)請(qǐng)求。
  • 多線程斷流:一旦進(jìn)入子線程或線程池,主線程的上下文信息(如用戶信息、請(qǐng)求 ID)就會(huì)丟失。
  • 格式不統(tǒng)一:不同模塊日志風(fēng)格迥異,難以通過 ELK 等工具進(jìn)行結(jié)構(gòu)化分析。

3.自定義日志格式

由于默認(rèn)格式的缺點(diǎn),因此我們可以做一些適當(dāng)?shù)呐渲?,來?yōu)化日志的輸出,便于采集和分析。
使用spring-logback.xml文件來進(jìn)行配置,這里做一個(gè)最簡(jiǎn)單的適配:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!-- 定義LOG_HOME為當(dāng)前目錄 -->
    <property name="LOG_HOME" value="./logs"/>
    <appender name="consoleLog_local" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出(配色):%d表示日期,%thread表示線程名,%-5level:級(jí)別從左顯示5個(gè)字符寬度%msg:日志消息,%n是換行符-->
            <pattern>[traceId-%X{traceId}] %yellow(%d{yyyy-MM-dd HH:mm:ss}) %red([%thread]) %highlight(%-5level) %boldMagenta(%logger{50}) - %cyan(%msg%n)
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <pattern>
                    <pattern>
                        {
                        <!--打印時(shí)間-->
                        "time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
                        <!--日志級(jí)別-->
                        "level": "%level",
                        <!--類中的第幾行-->
                        "caller": "%X{traceId}",
                        <!--日志打印的信息-->
                        "msg": "%message",
                        <!--堆棧異常信息-->
                        "stack_trace":"%xEx",
                        <!--類名 -->
                        "class": "%c",
                        <!--類中第幾行 -->
                        "method":"%M"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    <!--根據(jù)日志級(jí)別分離日志,分別輸出到不同的文件-->
    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <pattern>
                    <pattern>
                        {
                        <!--打印時(shí)間-->
                        "time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
                        <!--日志級(jí)別-->
                        "level": "%level",
                        <!--類中的第幾行-->
                        "caller": "%X{traceId}",
                        <!--日志打印的信息-->
                        "msg": "%message",
                        <!--堆棧異常信息-->
                        "stack_trace":"%xEx",
                        <!--類名 -->
                        "class": "%c",
                        <!--類中第幾行 -->
                        "method":"%M"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
        <!--滾動(dòng)策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按時(shí)間保存日志 修改格式可以按小時(shí)、按天、月來保存-->
            <fileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--保存時(shí)長-->
            <MaxHistory>90</MaxHistory>
            <!--文件大小-->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>
    <root level="info">
        <springProfile name="!local">
            <appender-ref ref="fileInfoLog"/>
            <appender-ref ref="consoleLog"/>
        </springProfile>
        <springProfile name="local">
            <appender-ref ref="consoleLog_local"/>
            <appender-ref ref="fileInfoLog"/>
        </springProfile>
    </root>
</configuration>

這里主要設(shè)置了日志存放的位置以及大小,在文件中和在控制臺(tái)輸出的格式。當(dāng)我們把這個(gè)文件添加到resource目錄下時(shí),啟動(dòng)日志就會(huì)如:

4. 請(qǐng)求追蹤-多線程

在線程池或者進(jìn)入子線程,我們?cè)O(shè)置的唯一ID就會(huì)丟失,這樣不利于排查問題。因此需要把每個(gè)請(qǐng)求所鞋帶的唯一ID,透?jìng)鞯阶泳€程或者線程池。

4.1 為什么會(huì)丟失?

ID會(huì)丟失的根本原因是MDC(Mapped Diagnostic Context)底層依賴的是 ThreadLocal,而 ThreadLocal 的數(shù)據(jù)是與具體的“線程實(shí)例”強(qiáng)綁定的。

什么是MDC?
MDC 的全稱是 Mapped Diagnostic Context(映射診斷上下文)。 你可以把它生動(dòng)地理解為 “主線程的隨身背包”或者“行李牌”。

它最早由 Log4j 提出,現(xiàn)在 SLF4J(Spring Boot 默認(rèn)的日志門面)和 Logback/Log4j2(日志實(shí)現(xiàn))都對(duì)它提供了完美支持。

  • 數(shù)據(jù)結(jié)構(gòu):本質(zhì)上是一個(gè) Map<String, String>。
  • 物理存儲(chǔ):它的底層完全依賴于 ThreadLocal。

這意味著 MDC 中的數(shù)據(jù)是線程隔離的。A 線程看不見 B 線程的 MDC 數(shù)據(jù)。
這也意味著 MDC 的生命周期通常綁定在當(dāng)前線程的單次任務(wù)執(zhí)行周期內(nèi)。

核心 API:

  • MDC.put(“key”, “value”):往當(dāng)前線程的 map 里存數(shù)據(jù)。
  • MDC.get(“key”):取數(shù)據(jù)。
  • MDC.remove(“key”):刪掉某條數(shù)據(jù)。
  • MDC.clear():清空當(dāng)前線程的所有數(shù)據(jù)(極其重要,防止內(nèi)存泄漏和污染)。

由于物理存儲(chǔ)依賴ThreadLocal,因此A 線程看不見 B 線程的 MDC 數(shù)據(jù),這就造成了丟失的現(xiàn)象。

4.2 手動(dòng)進(jìn)行傳遞

既然線程間MDC的數(shù)據(jù)是隔離的,那么我們就可以手動(dòng)進(jìn)行傳遞。
創(chuàng)建一個(gè)TraceIdUtil.java類

import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
@Slf4j
public class TraceIdUtil {
    public static final String TRACE_ID = "traceId";
    public static String getTraceId() {
        String traceId = null;
        try {
            traceId = MDC.get(TRACE_ID);
        } catch (Exception e) {
            log.error("mdc get error", e);
        }
        return traceId == null ? "" : traceId;
    }
    public static void setTraceId(String traceId) {
        try {
            MDC.put(TRACE_ID, traceId);
        } catch (Exception e) {
            log.error("mdc put error", e);
        }
    }
    public static void removeTraceId() {
        try {
            MDC.remove(TRACE_ID);
        } catch (Exception e) {
            log.error("mdc remove error", e);
        }
    }
    public static void clear() {
        try {
            MDC.clear();
        } catch (Exception e) {
            log.error("mdc clear error", e);
        }
    }
    public static String generateTraceId() {
        return "random-" + UUID.randomUUID().toString().replace("-", "");
    }
    public static void setContextMap(Map<String, String> contextMap) {
        try {
            MDC.setContextMap(contextMap);
        } catch (Exception e) {
            log.error("mdc setContextMap error", e);
        }
    }
    public static Map<String, String> getCopyOfContextMap() {
        try {
            return MDC.getCopyOfContextMap();
        } catch (Exception e) {
            log.error("mdc getCopyOfContextMap error", e);
        }
        return null;
    }
}

再添加一個(gè)ThreadMdcUtil.java類:

import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.Callable;
public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {
            TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
        }
    }
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                TraceIdUtil.clear();
            } else {
                TraceIdUtil.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                TraceIdUtil.clear();
            }
        };
    }
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                TraceIdUtil.clear();
            } else {
                TraceIdUtil.setContextMap(context);
            }
            //設(shè)置traceId
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                TraceIdUtil.clear();
            }
        };
    }
}

在線程池中使用的話,可以使用如下的工具類ThreadPoolExecutorMdcExecutor.java

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorMdcExecutor extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }
    public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }
    public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
    }
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
    }
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
    }
}

這樣就可以主動(dòng)把ID傳入子線程或者線程池,就不會(huì)丟失了。

如何展示呢?我這里封裝了一個(gè)starter,直接使用自定義starter就可以使用了。

4.3 創(chuàng)建自己的starter

這里我創(chuàng)建了一個(gè)自定義的starter,同時(shí)在filter中增加id追蹤。
LogMdcFilter.java

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import static com.yaoyuan.bi.utils.Constant.REQUEST_HEADER_KEY_ID;
public class LogMdcFilter implements Filter {
    public static final String UNIQUE_ID_NAME = "traceId";
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String requestId = Optional.of(servletRequest)
                .filter(HttpServletRequest.class::isInstance)
                .map(HttpServletRequest.class::cast)
                .map(req -> req.getHeader(REQUEST_HEADER_KEY_ID))
                .filter(header -> !header.trim().isEmpty())
                .orElseGet(() -> UUID.randomUUID().toString().replace("-", ""));
        MDC.put(UNIQUE_ID_NAME, requestId);
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            MDC.remove(UNIQUE_ID_NAME);
        }
    }
}

配置使filter生效

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<LogMdcFilter> logFilterRegistration() {
        FilterRegistrationBean<LogMdcFilter> registration = new FilterRegistrationBean<>();
        // 注入過濾器
        registration.setFilter(new LogMdcFilter());
        // 攔截規(guī)則
        registration.addUrlPatterns("/*");
        // 過濾器名稱
        registration.setName("logMDCFilter");
        // 過濾器順序
        registration.setOrder(0);
        return registration;
    }
}

當(dāng)然了,不要忘了使用上面的spring-logback.xml文件來格式化日志。

這部分代碼托管于這個(gè)倉庫。

5.總結(jié)

本文從源碼層面探討了spring的日志系統(tǒng)運(yùn)作原理,以及在使用中的局限性,并使用了一個(gè)可行的方案解決了追蹤ID丟失的問題,并且最后自定義了一個(gè)spring-boot-starter,用來處理日志全鏈路追蹤。

當(dāng)然,這個(gè)手動(dòng)傳遞ID的方法不夠優(yōu)雅,阿里開源的TransmittableThreadLocal(TTL)可以實(shí)現(xiàn)零代碼侵入的全量鏈路打通。這里挖個(gè)坑,有空再來填。

到此這篇關(guān)于Spring Boot中的日志log原理與自定義日志格式的文章就介紹到這了,更多相關(guān)springboot日志log內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java獲取文件大小的幾種方法

    java獲取文件大小的幾種方法

    這篇文章主要介紹了java獲取文件大小的幾種方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-10-10
  • Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計(jì)原則解讀

    Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計(jì)原則解讀

    Java是一種面向?qū)ο蟮母呒?jí)編程語言,具有跨平臺(tái)兼容性、自動(dòng)內(nèi)存管理等特點(diǎn),它支持多線程、異常處理,并擁有豐富的標(biāo)準(zhǔn)庫和強(qiáng)大的社區(qū)生態(tài),微服務(wù)架構(gòu)是將應(yīng)用分解為多個(gè)小型服務(wù)的設(shè)計(jì)風(fēng)格
    2024-11-11
  • springboot之redis cache TTL選項(xiàng)的使用

    springboot之redis cache TTL選項(xiàng)的使用

    這篇文章主要介紹了springboot之redis cache TTL選項(xiàng)的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • SpringBoot+ MySQL多線程查詢與聯(lián)表查詢性能對(duì)比

    SpringBoot+ MySQL多線程查詢與聯(lián)表查詢性能對(duì)比

    本文主要介紹了SpringBoot+ MySQL多線程查詢與聯(lián)表查詢性能對(duì)比,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2025-05-05
  • SpringMvc+Mybatis+Pagehelper分頁詳解

    SpringMvc+Mybatis+Pagehelper分頁詳解

    這篇文章主要介紹了SpringMvc+Mybatis+Pagehelper分頁詳解,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下的相關(guān)資料
    2017-01-01
  • 輕松掌握J(rèn)ava單例模式

    輕松掌握J(rèn)ava單例模式

    這篇文章主要幫助大家輕松掌握J(rèn)ava單例模式 ,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-10-10
  • Java番外雜談之每天掃的二維碼你了解它內(nèi)含的信息嗎

    Java番外雜談之每天掃的二維碼你了解它內(nèi)含的信息嗎

    二維碼已經(jīng)成為我們?nèi)粘I钪斜夭豢缮俚慕M成部分了,登錄需要掃一掃二維碼、買東西付錢需要掃一掃二維碼、開會(huì)簽到也需要掃一掃二維碼,那么如此使用的二維碼技術(shù),背后的原理是怎樣的呢?本文將結(jié)合二維碼的發(fā)展歷程以及典型應(yīng)用場(chǎng)景,分析二維碼背后的技術(shù)原理
    2022-02-02
  • Maven中pom.xml配置文件詳細(xì)介紹

    Maven中pom.xml配置文件詳細(xì)介紹

    這篇文章主要介紹了Maven中pom.xml配置文件詳細(xì)介紹,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • Spring JDBC的使用方法詳解

    Spring JDBC的使用方法詳解

    這篇文章主要介紹了Spring JDBC的使用方法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-09-09
  • SpringBoot防止大量請(qǐng)求攻擊的實(shí)現(xiàn)

    SpringBoot防止大量請(qǐng)求攻擊的實(shí)現(xiàn)

    在有些特定的時(shí)候需要加上IP訪問時(shí)間限制,防止一個(gè)IP多次訪問請(qǐng)求,本文主要介紹了SpringBoot防止大量請(qǐng)求攻擊的實(shí)現(xiàn),感興趣的可以了解一下
    2021-11-11

最新評(píng)論

克东县| 阿城市| 南木林县| 荔浦县| 营山县| 广丰县| 沛县| 香格里拉县| 二连浩特市| 双城市| 曲麻莱县| 石城县| 洛南县| 靖江市| 华宁县| 金湖县| 胶南市| 衡东县| 正安县| 林口县| 荥阳市| 丘北县| 江北区| 西吉县| 山西省| 昌图县| 建昌县| 新源县| 青河县| 从江县| 定日县| 乐平市| 涿鹿县| 邛崃市| 宜黄县| 于田县| 和平县| 阳谷县| 翁牛特旗| 新兴县| 阜城县|