Version :SpringBoot 2.7.12 The current execution process: 1. SmartLifecycle bean start with phase order. 2. tomcat start by SmartLifecycleBean 3. run bean's ContextRefreshedEvent callback.

The problem: Tomcat started port listening and received HTTP requests too early before Spring Context had fully initialized all beans. In a high-availability environment, some HTTP requests may receive unintended replies before all beans are initialized, affecting the user experience. Restrict autoStart from being executed manually after autoStart is disabled

Steps to reproduce 1. https://start.spring.io/ generate a project with Spring Web Dependencies。 2. main method code:


@SpringBootApplication
public class TomcatdemoApplication {

    private static TomcatWebServer webServer;
    private static final Log logger = LogFactory.getLog(TomcatdemoApplication.class);
    public static void main(String[] args) {
        ConfigurableApplicationContext context=SpringApplication.run(TomcatdemoApplication.class, args);
        Map<String,Lifecycle> sfMap=context.getBeansOfType(Lifecycle.class);
        context.start();
        //try start tomcat manually,but not effect
        webServer.start();
        }

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                // try disalbe auto start
                webServer= new TomcatWebServer(tomcat, false, getShutdown());
                return webServer;
            }
        };
        return tomcat;
    }
}

  1. demo SmartLifecycle code:

@Service
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DemoTestSmartLifecycle implements SmartLifecycle {
    private static final Log logger = LogFactory.getLog(DemoTestSmartLifecycle.class);
    private volatile boolean running;
    @Override
    public void start() {
        logger.info("start|" + LocalDateTime.now());
        running=true;
    }

    @Override
    public void stop() {
        logger.info("stop|" + LocalDateTime.now());
        running=false;
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    @EventListener
    @Order(Ordered.LOWEST_PRECEDENCE)
    public void onContextRefreshedEvent(ContextRefreshedEvent event) {
        logger.info("onContextRefreshedEvent|"+event);

    }

    @EventListener
    @Order(Ordered.LOWEST_PRECEDENCE)
    public void onContextStartedEvent(ContextStartedEvent event) {
        logger.info("onContextStartedEvent|"+event);
    }
    @Override
    public int getPhase() {
        return Integer.MAX_VALUE-3;
    }
}

  1. log show : main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): -1 (http) with context path '/tomcatdemo' and can't access http://localhost:8080/

some log :

2023-06-18 17:34:36.478  INFO 11196 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : start|2023-06-18T17:34:36.478765700
2023-06-18 17:34:36.504  INFO 11196 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): -1 (http) with context path '/tomcatdemo'
2023-06-18 17:34:36.510  INFO 11196 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : onContextRefreshedEvent|org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7d900ecf, started on Sun Jun 18 17:34:35 GMT+08:00 2023]
2023-06-18 17:34:36.512  INFO 11196 --- [           main] c.l.tomcatdemo.TomcatdemoApplication     : Started TomcatdemoApplication in 0.863 seconds (JVM running for 1.034)
2023-06-18 17:34:36.514  INFO 11196 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : onContextStartedEvent|org.springframework.context.event.ContextStartedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7d900ecf, started on Sun Jun 18 17:34:35 GMT+08:00 2023]

we can know the order is : SmartLifecycle start -> Bean Refresh-> Bean Start.

I hope that the effect of optimization is as follows: 1. I can new TomcatWebServer with disalbe autostart 2. after all beans are initialized, I can start tomcat manually to listen port and receive http request.

Thanks !

the attach file is the demo project to reproduce the problem:

tomcatdemo.zip

Comment From: quaff

You can switch jar to war then deploy it to standalone tomcat.

Comment From: wilkinsona

I'm not sure that I fully understand what you are trying to do, but I think this is working as designed. The WebServer doesn't start listening for HTTP requests until its lifecycle is started. It uses phase WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE - 1024 for this. Any component that you want to have been start()ed before this should use an appropriate phase.

If you have components that need to start after the WebServer and do not want requests to be received until they have been started, you may want to look at Boot's application availability support.

Comment From: lizongbo

I'm not sure that I fully understand what you are trying to do, but I think this is working as designed. The WebServer doesn't start listening for HTTP requests until its lifecycle is started. It uses phase WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE - 1024 for this. Any component that you want to have been start()ed before this should use an appropriate phase.

If you have components that need to start after the WebServer and do not want requests to be received until they have been started, you may want to look at Boot's application availability support.

In the case where autoStart is true,before the callback execution of ContextRefreshedEvent and ContextStartedEvent is complete, tomcat has successfully listened on the HTTP port and can start receiving HTTP requests to execute the appropriate REST methods.

I updated the demo project, simulating sleeping for 10 seconds when receiving the ContextRefreshedEvent, and immediately accessing the http://localhost:8080/tomcatdemo/hello through the browser after starting the demo project, you can see that the log order executed is as follows:

code:

 @EventListener
    @Order(Ordered.LOWEST_PRECEDENCE)
    public void onContextRefreshedEvent(ContextRefreshedEvent event) {
        logger.info("onContextRefreshedEvent|" + event);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.onrefreshed = true;
    }

    @EventListener
    @Order(Ordered.LOWEST_PRECEDENCE)
    public void onContextStartedEvent(ContextStartedEvent event) {
        logger.info("onContextStartedEvent|" + event);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.onStarted = true;
    }

the log:

2023-06-20 01:20:27.205  INFO 1972 --- [           main] c.l.tomcatdemo.TomcatdemoApplication     : Starting TomcatdemoApplication using Java 17.0.7 on quickpc5 with PID 1972 (D:\githubProjects\tomcatdemo\build\classes\java\main started by 86135 in D:\githubProjects\tomcatdemo)
2023-06-20 01:20:27.206  INFO 1972 --- [           main] c.l.tomcatdemo.TomcatdemoApplication     : No active profile set, falling back to 1 default profile: "default"
2023-06-20 01:20:27.656  INFO 1972 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-06-20 01:20:27.661  INFO 1972 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-06-20 01:20:27.661  INFO 1972 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.75]
2023-06-20 01:20:27.723  INFO 1972 --- [           main] o.a.c.c.C.[.[localhost].[/tomcatdemo]    : Initializing Spring embedded WebApplicationContext
2023-06-20 01:20:27.723  INFO 1972 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 494 ms
2023-06-20 01:20:27.823  INFO 1972 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2023-06-20 01:20:27.886  INFO 1972 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : start|2023-06-20T01:20:27.886379100
2023-06-20 01:20:27.898  INFO 1972 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path '/tomcatdemo'
2023-06-20 01:20:27.904  INFO 1972 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : onContextRefreshedEvent|org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@4a8355dd, started on Tue Jun 20 01:20:27 GMT+08:00 2023]
2023-06-20 01:20:30.727  INFO 1972 --- [nio-8080-exec-1] o.a.c.c.C.[.[localhost].[/tomcatdemo]    : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-06-20 01:20:30.727  INFO 1972 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-06-20 01:20:30.728  INFO 1972 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2023-06-20 01:20:30.738  INFO 1972 --- [nio-8080-exec-1] com.lizongbo.tomcatdemo.DemoController   : runHello
2023-06-20 01:20:37.914  INFO 1972 --- [           main] c.l.tomcatdemo.TomcatdemoApplication     : Started TomcatdemoApplication in 10.889 seconds (JVM running for 11.052)
2023-06-20 01:20:37.918  INFO 1972 --- [           main] c.l.tomcatdemo.DemoTestSmartLifecycle    : onContextStartedEvent|org.springframework.context.event.ContextStartedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@4a8355dd, started on Tue Jun 20 01:20:27 GMT+08:00 2023]
2023-06-20 01:20:45.080  INFO 1972 --- [nio-8080-exec-2] com.lizongbo.tomcatdemo.DemoController   : runHello
2023-06-20 01:20:53.456  INFO 1972 --- [nio-8080-exec-3] com.lizongbo.tomcatdemo.DemoController   : runHello

the screenshot is follow: 1. 2e5b7eb13d769afbb702f69a03dc95a 2. 3b627368f527706647dc6514ae22d6f

3. d80f312258c7c88f02175ab4c78aa05

so I can see this run order:

  1. 2023-06-20 01:20:27.886 DemoTestSmartLifecycle
  2. 2023-06-20 01:20:27.898 Tomcat started on port(s): 8080 (http) with context path '/tomcatdemo'
  3. 2023-06-20 01:20:27.904 onContextRefreshedEvent begin
  4. 2023-06-20 01:20:30.728 DispatcherServlet Completed initialization
  5. 2023-06-20 01:20:30.738 DemoController runHello ,but onrefreshed is false and onStarted is false
  6. 2023-06-20 01:20:37.918 onContextStartedEvent begin
  7. 2023-06-20 01:20:45.080 DemoController runHello ,but onrefreshed is true but onStarted is also false
  8. 2023-06-20 01:20:53.456 DemoController runHello ,and onrefreshed is true and onStarted is true

Between 2023-06-20 01:20:27.898 and 2023-06-20 01:20:37.918, if a large number of HTTP requests are received, their execution results may not be as expected because the app has not fully initialized and warmed up.

the new log screenshot:

SpringBoot Support starting tomcat manually after spring context refresh

the java source files :

TomcatdemoApplication.java.txt DemoController.java.txt

DemoTestSmartLifecycle.java.txt

the full project zip file:

tomcatdemo.zip

Comment From: lizongbo

When I want to inherit the TomcatWebServer class to add custom control logic, but since the member variables in the TomcatWebServer class are private, and some fields are final, I can't inherit TomcatWebServer to achieve the effect I expect, so if I can adjust these member variables to protected, I can implement high-availability logic on my own, thank you!

SpringBoot Support starting tomcat manually after spring context refresh

Comment From: wilkinsona

ConfigurableApplicationContext context = SpringApplication.run(TomcatdemoApplication.class, args);
Map<String, Lifecycle> sfMap = context.getBeansOfType(Lifecycle.class);
context.start();

Calling start() after run() doesn't really make sense. When the context is refreshed, DefaultLifecycleProcessor.onRefresh() is called and it starts all beans. This is done immediately before the ContextRefreshedEvent is published. Calling start() on the context without having called stop() will do nothing other than publishing a ContextStartedEvent. If you want components to be started before the web server starts accepting connections, you should assign them a phase that's earlier than that of WebServerStartStopLifecycle.

You should review your use of Lifecycle, SmartLifecycle, and the context refreshed and context started events. Both events are too late to use a signal that some initialization should be performed as they will be published after the web server has started accepting requests. You could either perform any initialization as part of bean creation (or through a @PostConstruct or InitializingBean callback) or you should implement Lifecycle or SmartLifecycle with an appropriate phase to ensure that the components is started before the web server (and stopped after it).