Search code examples
springspring-bootspring-mvcembedded-jetty

Show a static HTML page while the Spring context initializes


Spring Boot 2.3.7, Embedded Jetty, JSP and WAR packaging. I want to show my some static HTML page while spring context initializes. It should be visible when application starts and before spring context refreshed. I tried to use this manual as example https://www.nurkiewicz.com/2015/09/displaying-progress-of-spring.html but this doesn't work. I need to start embedded jetty directly when jetty is initialized. But spring boot starts embedded jetty only when context refreshed. How should I do this?


Solution

  • I created a Jetty warpper warmup class:

    public final class WarmupServer {
    
        private final String contextPath;
        private final String displayName;
        private final DefaultApplicationArguments arguments;
    
        private final String[] welcomeFiles;
        private final Resource baseResource;
    
        private Server server;
    
        public WarmupServer(String contextPath,
                            String displayName,
                            Resource baseResource,
                            String[] welcomeFiles,
                            String... runArgs) {
            this.contextPath = StringUtils.defaultIfBlank(contextPath, "/");
            this.displayName = StringUtils.defaultIfBlank(displayName, "Warmup");
            this.baseResource = ObjectUtils.defaultIfNull(baseResource, Resource.newClassPathResource("/static"));
            this.welcomeFiles = ArrayUtils.isEmpty(welcomeFiles) ? new String[]{"html/warmup.html"} : welcomeFiles;
            this.arguments = new DefaultApplicationArguments(ArrayUtils.nullToEmpty(runArgs));
        }
    
        public Server start() throws Exception {
            if (server != null && server.isStarted()) {
                throw new IllegalStateException("Server already started");
            }
            server = new Server();
            server.setHandler(buildServletHandler());
    
            final String configPath = parseArg(OPT_CONFIG);
            if (StringUtils.isBlank(configPath)) {
                throw new RuntimeException(OPT_CONFIG + " argument is not set");
            }
            final Config config = ConfigUtils.parseFile(new File(configPath), DEFAULT_CONFIG_FILE_NAME);
    
            configureHttpConnector(config);
            configureHttpsConnector(config);
    
            server.start();
            return server;
        }
    
        public void registerWarmupServerStopLifecycle(ConfigurableApplicationContext context) {
            context.getBeanFactory()
                    .registerSingleton(WarmupStopLifecycle.class.getSimpleName(), new WarmupStopLifecycle(server));
        }
    
        private ServletContextHandler buildServletHandler() {
            final ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
            handler.addServlet(DefaultServlet.class, "/");
            handler.setDisplayName(displayName);
            handler.setContextPath(contextPath);
            handler.setWelcomeFiles(welcomeFiles);
            handler.setBaseResource(baseResource);
            return handler;
        }
    
        private void configureHttpConnector(Config config) {
            final int httpPort = NumberUtils.toInt(parseArg(OPT_HTTP_PORT), config.getInt(OPT_HTTP_PORT));
            final ServerConnector connector = new ServerConnector(server);
            connector.setPort(httpPort);
            server.addConnector(connector);
        }
    
        private void configureHttpsConnector(Config config) {
            final int httpsPort = NumberUtils.toInt(
                    parseArg(OPT_HTTPS_PORT), config.getInt(OPT_HTTPS_PORT));
            final String keyStorePath = StringUtils.defaultIfBlank(
                    parseArg(OPT_KEYSTORE_FILE), config.getString(OPT_KEYSTORE_FILE));
    
            final boolean sslEnabled = StringUtils.isNotBlank(keyStorePath)
                    && Files.isReadable(Paths.get(keyStorePath));
    
            if (sslEnabled) {
                final HttpConfiguration configuration = new HttpConfiguration();
                configuration.setSecurePort(httpsPort);
                configuration.setSecureScheme(HTTPS_SCHEME);
    
                final ServerConnector httpsConnector = new HttpsConnector()
                        .createConnector(server, configuration, config.getConfig(JETTY_HTTPS_CONFIG), httpsPort);
                server.addConnector(httpsConnector);
            }
        }
    
        private String parseArg(String optionName) {
            final List<String> values = arguments.getOptionValues(optionName);
            return CollectionUtils.isEmpty(values) ? StringUtils.EMPTY : values.get(0);
        }
    
          
        public static WarmupServer start(String contextPath,
                                         String displayName,
                                         Resource baseResource,
                                         String[] welcomeFiles,
                                         String... runArgs) throws Exception {
            final WarmupServer server = new WarmupServer(contextPath, displayName, baseResource, welcomeFiles, runArgs);
            server.start();
            return server;
        }
    
    }
    

    This wrapper parses command line arguments and creates a Jetty handler and HTTP and (or) HTTPS connectors by using provided command line arguments.

    And the simple Spring's Lifecycle implementation class:

    @RequiredArgsConstructor
    class WarmupStopLifecycle implements SmartLifecycle {
    
        private static final Logger logger = LogManager.getFormatterLogger();
    
        private final Server warmupServer;
        private volatile boolean isRunning;
    
        @Override
        public void start() {
            try {
                warmupServer.stop();
                isRunning = true;
            } catch (Exception e) {
                logger.error("Failed to stop warmup server", e);
                throw new RuntimeException("Failed to stop warmup server", e);
            }
        }
    
        @Override
        public void stop() {
        }
    
        @Override
        public boolean isRunning() {
            return isRunning;
        }
    
        /**
         * Returns phase of this lifecycle.
         * A phase MUST be before the Spring web server starts.
         * See {@code org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle} phase.
         */
        @Override
        public int getPhase() {
            return Integer.MAX_VALUE - 2;
        }
    }
    

    So usage of this:

    @SpringBootApplication
    public class SpringApplication {
    
        public static void main(String[] args) throws Exception {
            final WarmupServer warmupServer = WarmupServer.start(
                    "/my_context_path", "My Warmup server handler", args);
    
            new SpringApplicationBuilder()
                    .sources(SpringApplication.class)
                    .initializers(warmupServer::registerWarmupServerStopLifecycle)
                    .run(args);
        }
    }
    

    WarmupServer starts immediately after the application runs and will be stopped before starting the Spring's web server.