亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

SpringBoot?main方法結(jié)束程序不停止的原因分析及解決方法

 更新時間:2025年07月30日 08:49:28   作者:rhyme  
Spring?Boot啟動內(nèi)嵌Tomcat時,main方法啟動非daemon線程執(zhí)行await()死循環(huán),使JVM保持運(yùn)行,通過發(fā)送關(guān)機(jī)指令可終止程序,下面通過本文給大家介紹SpringBoot?main方法結(jié)束程序不停止的原因分析及解決方法,感興趣的朋友一起看看吧

前言

對于Java開發(fā)來說,天天都在用SpringBoot,每次啟動都執(zhí)行了main方法,該方法應(yīng)該是最容易讓人忽視的地方之一,不過幾行代碼,為什么執(zhí)行完后JVM不結(jié)束呢?

本文以內(nèi)嵌tomcat為例進(jìn)行說明,并分享一些debug和畫圖的技巧。

原因

先說結(jié)論,是因為main方法啟動了一個線程,這個線程是非daemon的,并且run方法執(zhí)行的任務(wù)是TomcatWebServer.this.tomcat.getServer().await();(死循環(huán)),即非daemon線程+任務(wù)不停止=程序不退出。

debug源碼

技巧

在debug時,有的源碼是抽象方法,我們可以用快捷鍵F7跳轉(zhuǎn)到具體正在執(zhí)行的實現(xiàn)類方法,另外Alt+F9可以強(qiáng)制到達(dá)光標(biāo)的位置。

流程

下面將debug對應(yīng)的源碼,有興趣的朋友可以跟著動手試試。

SpringBoot啟動入口,調(diào)用靜態(tài)run方法。

/** 一般demo
 * @date 2021/9/12 9:09
 * @author www.cnblogs.com/theRhyme
 */
@SpringBootApplication
public class CommonDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CommonDemoApplication.class, args);
    }
}

調(diào)用重載的run方法

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
		return run(new Class<?>[] { primarySource }, args);
	}

創(chuàng)建SpringApplication對象調(diào)用run方法

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

由于該run方法很長,這里只貼到與本文main方法結(jié)束為何程序不退出的代碼,對整個啟動流程有興趣的可以去看這篇:Springboot啟動原理和自動配置原理解析 這里我們注意refreshContext。

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
            ……

refreshContext調(diào)用了一個抽象方法,我們在debug模式使用F7進(jìn)入具體的實現(xiàn)類。

protected void refresh(ConfigurableApplicationContext applicationContext) {
		applicationContext.refresh();
	}

這里就初始化一些資源(placeholder,beanFactory,BeanPostProcessor,MessageSource,ApplicationEventMulticaster),注意onRefresh方法。

@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
			// Prepare this context for refreshing.
			prepareRefresh();
			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);
			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);
				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);
				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				beanPostProcess.end();
				// Initialize message source for this context.
				initMessageSource();
				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();
				// Initialize other special beans in specific context subclasses.
				onRefresh();
                ……

進(jìn)入onRefresh,這里會創(chuàng)建WebServer:

@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

這里是具體創(chuàng)建webServer的步驟,注意getTomcatWebServer。

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

創(chuàng)建TomcatWebServer對象。

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
		return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
	}

設(shè)置一些屬性,并執(zhí)行initialize方法。

public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
		initialize();
	}

初始化并啟動tomcat容器,然后就開起非daemon await線程。

private void initialize() throws WebServerException {
		logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();
				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
						// Remove service connectors so that protocol binding doesn't
						// happen when the service is started.
						removeServiceConnectors();
					}
				});
				// Start the server to trigger initialization listeners
				this.tomcat.start();
				// We can re-throw failure exception directly in the main thread
				rethrowDeferredStartupExceptions();
				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
				}
				catch (NamingException ex) {
					// Naming is not enabled. Continue
				}
				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();
			}
			catch (Exception ex) {
				stopSilently();
				destroySilently();
				throw new WebServerException("Unable to start embedded Tomcat", ex);
			}
		}
	}

創(chuàng)建非daemon線程設(shè)置線程名等參數(shù)并啟動。

private void startDaemonAwaitThread() {
		Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
			@Override
			public void run() {
				TomcatWebServer.this.tomcat.getServer().await();
			}
		};
		awaitThread.setContextClassLoader(getClass().getClassLoader());
		awaitThread.setDaemon(false);
		awaitThread.start();
	}

至此由于awaitThread.setDaemon(false);TomcatWebServer.this.tomcat.getServer().await();,啟動該線程awaitThread后,main方法后續(xù)雖然執(zhí)行完畢,但是程序不會退出。

await方法

這里單獨(dú)看一下TomcatWebServer.this.tomcat.getServer().await();。

該方法的Java doc:

/**
 * Wait until a proper shutdown command is received, then return.
 * This keeps the main thread alive - the thread pool listening for http
 * connections is daemon threads.
 */

指的是通過等候關(guān)閉命令這個動作來保持main線程存活,而HTTP線程作為daemon線程會在main線程結(jié)束時終止。

任務(wù)一直運(yùn)行的原因:源碼如下,debug會進(jìn)入getPortWithOffset()的值是-1的分支(注意這里不是server.port端口號),然后會不斷循環(huán)Thread.sleep( 10000 )直到發(fā)出關(guān)機(jī)指令修改stopAwait的值為true。

@Override
    public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        if (getPortWithOffset() == -2) {
            // undocumented yet - for embedding apps that are around, alive.
            return;
        }
        if (getPortWithOffset() == -1) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }
        ……

stopAwait的值只會在org.apache.catalina.core.StandardServer#stopAwait中被修改,源碼如下:

public void stopAwait() {
        stopAwait=true;
        Thread t = awaitThread;
        if (t != null) {
            ServerSocket s = awaitSocket;
            if (s != null) {
                awaitSocket = null;
                try {
                    s.close();
                } catch (IOException e) {
                    // Ignored
                }
            }
            t.interrupt();
            try {
                t.join(1000);
            } catch (InterruptedException e) {
                // Ignored
            }
        }
    }

而該方法會在容器生命周期結(jié)束方法org.apache.catalina.core.StandardServer#stopInternal中被調(diào)用。

非daemon線程的意義

setDaemon介紹

上面將線程設(shè)置為非daemon線程:awaitThread.setDaemon(false)。

java.lang.Thread#setDaemon源碼如下:

/**
     * Marks this thread as either a {@linkplain #isDaemon daemon} thread
     * or a user thread. The Java Virtual Machine exits when the only
     * threads running are all daemon threads.
     *
     * <p> This method must be invoked before the thread is started.
     *
     * @param  on
     *         if {@code true}, marks this thread as a daemon thread
     *
     * @throws  IllegalThreadStateException
     *          if this thread is {@linkplain #isAlive alive}
     *
     * @throws  SecurityException
     *          if {@link #checkAccess} determines that the current
     *          thread cannot modify this thread
     */
public final void setDaemon(boolean on) {
    checkAccess();
    if (isAlive()) {
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

根據(jù)上面的Java doc注釋可知:標(biāo)記該線程是否是daemon線程,而JVM退出僅當(dāng)只剩下daemon線程。

所以非daemon線程存活,JVM是不會退出的。

例子

如下代碼,我們在main方法中啟動了一個非daemon線程,并且調(diào)用了阻塞方法java.io.InputStream#read()

// https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": start");
        Thread awaitThread =
                new Thread("non-daemon") {
                    @Override
                    public void run() {
                        try {
                            System.out.println(Thread.currentThread().getName() + ": start");
                            System.in.read();
                            System.out.println(Thread.currentThread().getName() + ": end");
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                };
        awaitThread.setDaemon(false);
        awaitThread.start();
        System.out.println(Thread.currentThread().getName() + ": end");
    }

啟動程序后,再不進(jìn)行鍵盤輸入的情況下,程序不會停止,運(yùn)行結(jié)果如下:

main: start
main: end
non-daemon: start

main線程結(jié)束,但是程序不退出。

-1的原因

上面留了個問題,為什么getPortWithOffset()的返回值是-1。

如下getPort()的值為-1,此時相當(dāng)于直接調(diào)用了getPort()方法。

https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
@Override
    public int getPortWithOffset() {
        // Non-positive port values have special meanings and the offset should
        // not apply.
        int port = getPort();
        if (port > 0) {
            return port + getPortOffset();
        } else {
            return port;
        }
    }

getPort直接取的是port屬性。

@Override
    public int getPort() {
        return this.port;
    }

注意這里的port不是我們指定的server.port這個屬性,而是關(guān)閉命令監(jiān)聽的端口。

    /**
     * The port number on which we wait for shutdown commands.
     */
    private int port = 8005;

為什么是8005而不是-1呢?那是在哪被修改了呢?

port屬性提供的修改方式是setPort(),而使用Alt+F7找到在getServer中被修改為-1。

server.setPort( -1 );打一個斷點(diǎn),重新debug,可以知道具體修改的時機(jī)。

之前我們debug過方法createWebServer,是具體創(chuàng)建webServer的步驟,但是我們這里要進(jìn)入getWebServer

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
			ServletWebServerFactory factory = getWebServerFactory();
			createWebServer.tag("factory", factory.getClass().toString());
			this.webServer = factory.getWebServer(getSelfInitializer());
            ……

配置tomca實例參數(shù),但是要注意這里的tomcat.getService()方法。

public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

內(nèi)部調(diào)用getServer()。

public Service getService() {
        return getServer().findServices()[0];
    }

至此,就是這里就將server.setPort( -1 );。

public Server getServer() {
        if (server != null) {
            return server;
        }
        System.setProperty("catalina.useNaming", "false");
        server = new StandardServer();
        initBaseDir();
        // Set configuration source
        ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
        // https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
        server.setPort( -1 );
        Service service = new StandardService();
        service.setName("Tomcat");
        server.addService(service);
        return server;
    }

調(diào)用鏈

技巧

如果我們想畫一個方法本次被調(diào)用(線程內(nèi)部)的流程圖,那么我們可以debug進(jìn)入該方法,Alt+F8執(zhí)行如下代碼,打印出方法調(diào)用棧對應(yīng)的mermaid js 內(nèi)容,然后使用文本繪圖工具進(jìn)行渲染。

// https://www.cnblogs.com/theRhyme
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
List<String> methodChain = Arrays.stream(stackTrace)
        .filter(e -> !e.getClassName().startsWith("java.") && !e.getClassName().startsWith("jdk.") && !e.getMethodName().contains("<"))
        .map(e -> e.getClassName() + "." + e.getMethodName())
        .collect(Collectors.toList());
StringBuilder mermaidCode = new StringBuilder("graph TD\n");
for (int i = methodChain.size() - 1; i > 0; i--) {
    mermaidCode.append(String.format("    %s --> %s\n",
            methodChain.get(i),
            methodChain.get(i-1)));
}
System.out.println(mermaidCode);

這種方式比較適合線程內(nèi)部展示具體方法的被調(diào)用關(guān)系,可以自定義根據(jù)包名等條件過濾掉不想要展示的類,但是對于跨線程的調(diào)用卻不起作用,因為原理是線程自身的調(diào)用棧。

具體內(nèi)容

如圖,debug到org.springframework.boot.web.embedded.tomcat.TomcatWebServer#startDaemonAwaitThread內(nèi)部,執(zhí)行上面的代碼。

輸出內(nèi)容:

graph TD
    org.springframework.boot.devtools.restart.RestartLauncher.run --> cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main
    cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main --> org.springframework.boot.SpringApplication.run
    org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
    org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
    org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.refreshContext
    org.springframework.boot.SpringApplication.refreshContext --> org.springframework.boot.SpringApplication.refresh
    org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.SpringApplication.refresh
    org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh
    org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh --> org.springframework.context.support.AbstractApplicationContext.refresh
    org.springframework.context.support.AbstractApplicationContext.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh
    org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer
    org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer
    org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer
    org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize
    org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread
    org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread --> idea.debugger.rt.GeneratedEvaluationClass.invoke

把內(nèi)容放入文本繪圖中,即可得到如下流程圖:

到此這篇關(guān)于SpringBoot main方法結(jié)束程序不停止的原因分析及解決方法的文章就介紹到這了,更多相關(guān)SpringBoot main方法結(jié)束程序不停止內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Matlab及Java實現(xiàn)小時鐘效果

    Matlab及Java實現(xiàn)小時鐘效果

    這篇文章主要為大家詳細(xì)介紹了Matlab及Java實現(xiàn)小時鐘效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-05-05
  • 淺談Spring Boot中Redis緩存還能這么用

    淺談Spring Boot中Redis緩存還能這么用

    這篇文章主要介紹了淺談Spring Boot中Redis緩存還能這么用,這種方式是Spring Cache提供的統(tǒng)一接口,實現(xiàn)既可以是Redis,也可以是Ehcache或者其他支持這種規(guī)范的緩存框架,感興趣的小伙伴們可以參考一下
    2019-06-06
  • 解決java.util.zip.ZipException: Not in GZIP format報錯問題

    解決java.util.zip.ZipException: Not in GZIP&nbs

    這篇文章主要介紹了解決java.util.zip.ZipException: Not in GZIP format報錯問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Springboot整合Druid實現(xiàn)對訪問的監(jiān)控方式

    Springboot整合Druid實現(xiàn)對訪問的監(jiān)控方式

    這篇文章主要介紹了Springboot整合Druid實現(xiàn)對訪問的監(jiān)控方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-05-05
  • Spring Boot如何通過自定義注解實現(xiàn)日志打印詳解

    Spring Boot如何通過自定義注解實現(xiàn)日志打印詳解

    這篇文章主要給大家介紹了關(guān)于Spring Boot如何通過自定義注解實現(xiàn)日志打印的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-10-10
  • SpringCloud3.x集成BigQuery的代碼實現(xiàn)

    SpringCloud3.x集成BigQuery的代碼實現(xiàn)

    Google BigQuery 是一種高性能、可應(yīng)用于大數(shù)據(jù)分析的公主云數(shù)據(jù)庫服務(wù),Spring Cloud 提供了完善的工具和核心功能,可以進(jìn)行泛動分布應(yīng)用構(gòu)建,本文給大家介紹了SpringCloud3.x集成BigQuery的代碼實現(xiàn),需要的朋友可以參考下
    2025-01-01
  • Spring Boot  Excel文件導(dǎo)出下載實現(xiàn)代碼

    Spring Boot Excel文件導(dǎo)出下載實現(xiàn)代碼

    這篇文章帶領(lǐng)我們直接實現(xiàn)Excel文件的直接導(dǎo)出下載,后續(xù)開發(fā)不需要開發(fā)很多代碼,直接繼承已經(jīng)寫好的代碼,增加一個Xml配置就可以直接導(dǎo)出。具體實現(xiàn)代碼大家跟隨小編一起通過本文學(xué)習(xí)吧
    2018-11-11
  • Spring+Mybatis 實現(xiàn)aop數(shù)據(jù)庫讀寫分離與多數(shù)據(jù)庫源配置操作

    Spring+Mybatis 實現(xiàn)aop數(shù)據(jù)庫讀寫分離與多數(shù)據(jù)庫源配置操作

    這篇文章主要介紹了Spring+Mybatis 實現(xiàn)aop數(shù)據(jù)庫讀寫分離與多數(shù)據(jù)庫源配置操作,需要的朋友可以參考下
    2017-09-09
  • Spring Boot熱加載jar實現(xiàn)動態(tài)插件的思路

    Spring Boot熱加載jar實現(xiàn)動態(tài)插件的思路

    本文主要介紹在 Spring Boot 工程中熱加載 jar 包并注冊成為 Bean 對象的一種實現(xiàn)思路,在動態(tài)擴(kuò)展功能的同時支持在插件中注入主程序的 Bean 實現(xiàn)功能更強(qiáng)大的插件
    2021-10-10
  • Springboot項目保存本地系統(tǒng)日志文件的實現(xiàn)方法

    Springboot項目保存本地系統(tǒng)日志文件的實現(xiàn)方法

    這篇文章主要介紹了Springboot項目保存本地系統(tǒng)日志文件的實現(xiàn)方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-04-04

最新評論