Tomcat在SpringBoot中是如何启动的

开发 后端
本文将以Tomcat为例,来看看SpringBoot是如何启动Tomcat的,同时也将展开学习下Tomcat的源码,了解Tomcat的设计。

 [[273866]]

前言

我们知道SpringBoot给我们带来了一个全新的开发体验,我们可以直接把web程序达成jar包,直接启动,这就得益于SpringBoot内置了容器,可以直接启动,本文将以Tomcat为例,来看看SpringBoot是如何启动Tomcat的,同时也将展开学习下Tomcat的源码,了解Tomcat的设计。

从 Main 方法说起

用过SpringBoot的人都知道,首先要写一个main方法来启动 

@SpringBootApplication  
public class TomcatdebugApplication {  
    public static void main(String[] args) {  
        SpringApplication.run(TomcatdebugApplication.class, args);  
    }  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

我们直接点击run方法的源码,跟踪下来,发下最终 的run方法是调用ConfigurableApplicationContext方法,源码如下: 

public ConfigurableApplicationContext run(String... args) {  
        StopWatch stopWatch = new StopWatch();  
        stopWatch.start();  
        ConfigurableApplicationContext context = null 
        Collection<springbootexceptionreporter> exceptionReporters = new ArrayList&lt;&gt;();  
        //设置系统属性『java.awt.headless』,为true则启用headless模式支持  
        configureHeadlessProperty();  
        //通过*SpringFactoriesLoader*检索*META-INF/spring.factories*,  
       //找到声明的所有SpringApplicationRunListener的实现类并将其实例化,  
       //之后逐个调用其started()方法,广播SpringBoot要开始执行了  
        SpringApplicationRunListeners listeners = getRunListeners(args);  
        //发布应用开始启动事件  
        listeners.starting();  
        try {  
        //初始化参数  
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);  
            //创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile),  
        //并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。  
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);  
            configureIgnoreBeanInfo(environment);  
            //打印banner  
            Banner printedBanner = printBanner(environment);  
            //创建应用上下文  
            context = createApplicationContext();  
            //通过*SpringFactoriesLoader*检索*META-INF/spring.factories*,获取并实例化异常分析器  
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,  
                    new Class[] { ConfigurableApplicationContext.class }, context);  
            //为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext,  
        //并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】,  
        //之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成,  
        //这里就包括通过**@EnableAutoConfiguration**导入的各种自动配置类。  
            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);  
            //遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法。  
        //我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展。  
            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;  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

其实这个方法我们可以简单的总结下步骤为 > 1. 配置属性 > 2. 获取监听器,发布应用开始启动事件 > 3. 初始化输入参数 > 4. 配置环境,输出banner > 5. 创建上下文 > 6. 预处理上下文 > 7. 刷新上下文 > 8. 再刷新上下文 > 9. 发布应用已经启动事件 > 10. 发布应用启动完成事件

其实上面这段代码,如果只要分析tomcat内容的话,只需要关注两个内容即可,上下文是如何创建的,上下文是如何刷新的,分别对应的方法就是createApplicationContext() 和refreshContext(context),接下来我们来看看这两个方法做了什么。 

protected ConfigurableApplicationContext createApplicationContext() {  
        Class<!--?--> contextClass = this.applicationContextClass;  
        if (contextClass == null) {  
            try {  
                switch (this.webApplicationType) {  
                case SERVLET:  
                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);  
                    break;  
                case REACTIVE:  
                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);  
                    break;  
                default:  
                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); 
                }  
            }  
            catch (ClassNotFoundException ex) {  
                throw new IllegalStateException(  
                        "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass",  
                        ex);  
            }  
        }  
        return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

这里就是根据我们的webApplicationType 来判断创建哪种类型的Servlet,代码中分别对应着Web类型(SERVLET),响应式Web类型(REACTIVE),非Web类型(default),我们建立的是Web类型,所以肯定实例化 DEFAULT_SERVLET_WEB_CONTEXT_CLASS指定的类,也就是AnnotationConfigServletWebServerApplicationContext类,我们来用图来说明下这个类的关系

通过这个类图我们可以知道,这个类继承的是ServletWebServerApplicationContext,这就是我们真正的主角,而这个类最终是继承了AbstractApplicationContext,了解完创建上下文的情况后,我们再来看看刷新上下文,相关代码如下: 

//类:SpringApplication.java  
private void refreshContext(ConfigurableApplicationContext context) {  
    //直接调用刷新方法  
        refresh(context);  
        if (this.registerShutdownHook) {  
            try {  
                context.registerShutdownHook();  
            }  
            catch (AccessControlException ex) {  
                // Not allowed in some environments.  
            }  
        }  
    }  
//类:SpringApplication.java  
protected void refresh(ApplicationContext applicationContext) {  
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);  
        ((AbstractApplicationContext) applicationContext).refresh();  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

这里还是直接传递调用本类的refresh(context)方法,最后是强转成父类AbstractApplicationContext调用其refresh()方法,该代码如下: 

// 类:AbstractApplicationContext   
public void refresh() throws BeansException, IllegalStateException {  
        synchronized (this.startupShutdownMonitor) {  
            // 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);  
                // Invoke factory processors registered as beans in the context.  
                invokeBeanFactoryPostProcessors(beanFactory);  
                // Register bean processors that intercept bean creation.  
                registerBeanPostProcessors(beanFactory);  
                // Initialize message source for this context.  
                initMessageSource();  
                // Initialize event multicaster for this context.  
                initApplicationEventMulticaster();  
                // Initialize other special beans in specific context subclasses.这里的意思就是调用各个子类的onRefresh()  
                onRefresh();  
                // Check for listener beans and register them.  
                registerListeners();  
                // Instantiate all remaining (non-lazy-init) singletons.  
                finishBeanFactoryInitialization(beanFactory);  
                // Last step: publish corresponding event.  
                finishRefresh();  
            }  
            catch (BeansException ex) {  
                if (logger.isWarnEnabled()) {  
                    logger.warn("Exception encountered during context initialization - " +  
                            "cancelling refresh attempt: " + ex);  
                }  
                // Destroy already created singletons to avoid dangling resources.  
                destroyBeans();  
                // Reset 'active' flag.  
                cancelRefresh(ex);  
                // Propagate exception to caller.  
                throw ex;  
            }  
            finally {  
                // Reset common introspection caches in Spring's core, since we  
                // might not ever need metadata for singleton beans anymore...  
                resetCommonCaches();  
            }  
        }  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

这里我们看到onRefresh()方法是调用其子类的实现,根据我们上文的分析,我们这里的子类是ServletWebServerApplicationContext。 

//类:ServletWebServerApplicationContext  
protected void onRefresh() {  
        super.onRefresh();  
        try {  
            createWebServer();  
        }  
        catch (Throwable ex) {  
            throw new ApplicationContextException("Unable to start web server", ex);  
        }  
    }   
private void createWebServer() {  
        WebServer webServer = this.webServer; 
         ServletContext servletContext = getServletContext();  
        if (webServer == null &amp;&amp; servletContext == null) {  
            ServletWebServerFactory factory = getWebServerFactory();  
            this.webServer = factory.getWebServer(getSelfInitializer());  
        }  
        else if (servletContext != null) {  
            try {  
                getSelfInitializer().onStartup(servletContext); 
            }  
            catch (ServletException ex) {  
                throw new ApplicationContextException("Cannot initialize servlet context", ex);  
            }  
        }  
        initPropertySources();  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

到这里,其实庐山真面目已经出来了,createWebServer()就是启动web服务,但是还没有真正启动Tomcat,既然webServer是通过ServletWebServerFactory来获取的,我们就来看看这个工厂的真面目。

走进Tomcat内部

根据上图我们发现,工厂类是一个接口,各个具体服务的实现是由各个子类来实现的,所以我们就去看看TomcatServletWebServerFactory.getWebServer()的实现。

@Override  
    public WebServer getWebServer(ServletContextInitializer... initializers) {  
        Tomcat tomcat = new Tomcat();  
        File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");  
        tomcat.setBaseDir(baseDir.getAbsolutePath());  
        Connector connector = new Connector(this.protocol);  
        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);  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

根据上面的代码,我们发现其主要做了两件事情,第一件事就是把Connnctor(我们称之为连接器)对象添加到Tomcat中,第二件事就是configureEngine,这连接器我们勉强能理解(不理解后面会述说),那这个Engine是什么呢?我们查看tomcat.getEngine()的源码:   

public Engine getEngine() {  
       Service service = getServer().findServices()[0];  
       if (service.getContainer() != null) {  
           return service.getContainer();  
       }  
       Engine engine = new StandardEngine();  
       engine.setName( "Tomcat" );  
       engine.setDefaultHost(hostname);  
       engine.setRealm(createDefaultRealm());  
       service.setContainer(engine);  
       return engine;  
   } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

根据上面的源码,我们发现,原来这个Engine是容器,我们继续跟踪源码,找到Container接口

上图中,我们看到了4个子接口,分别是Engine,Host,Context,Wrapper。我们从继承关系上可以知道他们都是容器,那么他们到底有啥区别呢?我看看他们的注释是怎么说的。 

 /**  
 If used, an Engine is always the top level Container in a Catalina  
 * hierarchy. Therefore, the implementation's <code>setParent()</code> method  
 * should throw <code>IllegalArgumentException</code> 
 *  
 * @author Craig R. McClanahan  
 */  
public interface Engine extends Container {  
    //省略代码  
 
/**  
 * <p>  
 * The parent Container attached to a Host is generally an Engine, but may  
 * be some other implementation, or may be omitted if it is not necessary.  
 * </p><p>  
 * The child containers attached to a Host are generally implementations  
 * of Context (representing an individual servlet context).  
 *  
 * @author Craig R. McClanahan  
 */  
public interface Host extends Container {  
//省略代码   
 
/*** </p><p>  
 * The parent Container attached to a Context is generally a Host, but may  
 * be some other implementation, or may be omitted if it is not necessary.  
 * </p><p>  
 * The child containers attached to a Context are generally implementations  
 * of Wrapper (representing individual servlet definitions).  
 * </p><p>  
 *  
 * @author Craig R. McClanahan  
 */  
public interface Context extends Container, ContextBind {  
    //省略代码  
 
/**</p><p>  
 * The parent Container attached to a Wrapper will generally be an  
 * implementation of Context, representing the servlet context (and  
 * therefore the web application) within which this servlet executes.  
 * </p><p>  
 * Child Containers are not allowed on Wrapper implementations, so the  
 * <code>addChild()</code> method should throw an  
 * <code>IllegalArgumentException</code> 
 *  
 * @author Craig R. McClanahan  
 */  
public interface Wrapper extends Container {  
    //省略代码  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

上面的注释翻译过来就是,Engine是最高级别的容器,其子容器是Host,Host的子容器是Context,Wrapper是Context的子容器,所以这4个容器的关系就是父子关系,也就是Engine>Host>Context>Wrapper。 我们再看看Tomcat类的源码: 

//部分源码,其余部分省略。  
public class Tomcat {  
//设置连接器  
     public void setConnector(Connector connector) {  
        Service service = getService();  
        boolean found = false 
        for (Connector serviceConnector : service.findConnectors()) {  
            if (connector == serviceConnector) {  
                found = true 
            }  
        }  
        if (!found) {  
            service.addConnector(connector);  
        }  
    }  
    //获取service  
       public Service getService() {  
        return getServer().findServices()[0];  
    }  
    //设置Host容器  
     public void setHost(Host host) {  
        Engine engine = getEngine();  
        boolean found = false 
        for (Container engineHost : engine.findChildren()) {  
            if (engineHost == host) {  
                found = true 
            }  
        }  
        if (!found) {  
            engine.addChild(host);  
        }  
    }  
    //获取Engine容器  
     public Engine getEngine() {  
        Service service = getServer().findServices()[0];  
        if (service.getContainer() != null) {  
            return service.getContainer();  
        }  
        Engine engine = new StandardEngine();  
        engine.setName( "Tomcat" );  
        engine.setDefaultHost(hostname);  
        engine.setRealm(createDefaultRealm());  
        service.setContainer(engine);  
        return engine;  
    }  
    //获取server  
       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));  
        server.setPort( -1 );  
        Service service = new StandardService();  
        service.setName("Tomcat");  
        server.addService(service);  
        return server;  
    }     
    //添加Context容器  
      public Context addContext(Host host, String contextPath, String contextName,  
            String dir) {  
        silence(host, contextName);  
        Context ctx = createContext(host, contextPath);  
        ctx.setName(contextName);  
        ctx.setPath(contextPath);  
        ctx.setDocBase(dir);  
        ctx.addLifecycleListener(new FixContextListener());  
        if (host == null) {  
            getHost().addChild(ctx);  
        } else {  
            host.addChild(ctx);  
        }         
    //添加Wrapper容器  
         public static Wrapper addServlet(Context ctx,  
                                      String servletName,  
                                      Servlet servlet) {  
        // will do class for name and set init params  
        Wrapper sw = new ExistingStandardWrapper(servlet);  
        sw.setName(servletName);  
        ctx.addChild(sw);  
        return sw;  
    }   

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.

阅读Tomcat的getServer()我们可以知道,Tomcat的最顶层是Server,Server就是Tomcat的实例,一个Tomcat一个Server;通过getEngine()我们可以了解到Server下面是Service,而且是多个,一个Service代表我们部署的一个应用,而且我们还可以知道,Engine容器,一个service只有一个;根据父子关系,我们看setHost()源码可以知道,host容器有多个;同理,我们发现addContext()源码下,Context也是多个;addServlet()表明Wrapper容器也是多个,而且这段代码也暗示了,其实Wrapper和Servlet是一层意思。另外我们根据setConnector源码可以知道,连接器(Connector)是设置在service下的,而且是可以设置多个连接器(Connector)。

根据上面分析,我们可以小结下: Tomcat主要包含了2个核心组件,连接器(Connector)和容器(Container),用图表示如下:

一个Tomcat是一个Server,一个Server下有多个service,也就是我们部署的多个应用,一个应用下有多个连接器(Connector)和一个容器(Container),容器下有多个子容器,关系用图表示如下:

Engine下有多个Host子容器,Host下有多个Context子容器,Context下有多个Wrapper子容器。

总结

SpringBoot的启动是通过new SpringApplication()实例来启动的,启动过程主要做如下几件事情: > 1. 配置属性 > 2. 获取监听器,发布应用开始启动事件 > 3. 初始化输入参数 > 4. 配置环境,输出banner > 5. 创建上下文 > 6. 预处理上下文 > 7. 刷新上下文 > 8. 再刷新上下文 > 9. 发布应用已经启动事件 > 10. 发布应用启动完成事件

而启动Tomcat就是在第7步中“刷新上下文”;Tomcat的启动主要是初始化2个核心组件,连接器(Connector)和容器(Container),一个Tomcat实例就是一个Server,一个Server包含多个Service,也就是多个应用程序,每个Service包含多个连接器(Connetor)和一个容器(Container),而容器下又有多个子容器,按照父子关系分别为:Engine,Host,Context,Wrapper,其中除了Engine外,其余的容器都是可以有多个。

下期展望

本期文章通过SpringBoot的启动来窥探了Tomcat的内部结构,下一期,我们来分析下本次文章中的连接器(Connetor)和容器(Container)的作用,敬请期待。

责任编辑:庞桂玉 来源: 中国开源
相关推荐

2019-12-09 15:08:30

JavaTomcatWeb

2019-09-24 09:46:35

Tomcat连接器Lifecycle

2017-09-04 18:48:14

TomcatSpringBoot容器

2009-06-03 15:50:51

eclipse中启动超eclipsetomcat

2017-09-04 14:40:00

LimitLatchTomcat线程

2010-06-02 13:05:52

tomcat和svn

2024-09-06 17:55:27

Springboot开发

2025-02-19 10:18:29

2024-12-17 16:26:31

2020-12-29 05:33:40

TomcatSpringBoot代码

2009-06-05 14:59:31

Eclipse中配置T

2022-04-10 23:42:33

MySQLSQL数据库

2017-10-27 07:11:38

TomcatUPDOWN

2020-04-28 22:58:33

Tomcat架构Service

2018-07-17 14:25:02

SQL解析美团点评MySQL

2022-07-12 07:33:47

ES类似连表查询

2020-12-09 09:33:16

编程语言C语言汇编语言

2018-05-21 08:52:15

Linux应用程序启动时间

2020-07-27 16:10:49

SpringBoottomcaJava

2016-08-03 17:23:47

javascripthtml前端
点赞
收藏

51CTO技术栈公众号