Search code examples
javaservletsjettyclassloader

Init servlet instantly after the embedded jetty server starts


I need to run my own logic after the jetty embedded server starts. I'm not starting it from the main class due to classloader issues. An ideal solution seemed to be running my server logic from a servlet initialization. But the init function and also the constructor is not called after the jetty server start. An instance of the servlet is being created during the first HTTP request. Is it possible to tell jetty to initialize my servlet instantly or do I really need to load all classes with my custom classloader and then start the jetty server?

This is the main class:

public class ServerLauncher {
    public static void main(String[] args) {
        JettyServerLauncher.launchHttp("target/server.war", "0.0.0.0", 8080);
        // Starting my own logic here is causing classloader issues, because WebSocket classes are loaded by other classloader than my classes, that is the reason why I moved it into the servlet
    }
}

This is my jetty embedded server launcher:

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.*;

import java.io.File;

public class JettyServerLauncher {
    private static boolean isHttps;
    private static File keyStoreFile;
    private static String warPath;
    private static String host;
    private static int httpPort;
    private static int httpsPort;
    private static String keyStorePath;
    private static String keyStorePass;
    private static boolean needClientAuth;

    public static void launchHttp(String warPath, String host, int httpPort) {
        JettyServerLauncher.isHttps = false;
        JettyServerLauncher.warPath = warPath;
        JettyServerLauncher.host = host;
        JettyServerLauncher.httpPort = httpPort;

        launch();
    }

    public static void launchHttps(String warPath, String host, String keyStorePath, String keyStorePass, int httpPort, int httpsPort, boolean needClientAuth) {
        JettyServerLauncher.isHttps = true;
        JettyServerLauncher.warPath = warPath;
        JettyServerLauncher.host = host;
        JettyServerLauncher.httpPort = httpPort;
        JettyServerLauncher.httpsPort = httpsPort;
        JettyServerLauncher.keyStorePath = keyStorePath;
        JettyServerLauncher.keyStorePass = keyStorePass;
        JettyServerLauncher.needClientAuth = needClientAuth;

        launch();
    }

    private static void launch() {
        Server server = null;

        try {
            System.out.println("Initializing jetty server...");

            if (isHttps) loadKeyStores(keyStorePath);

            // Create jetty server
            server = new Server(httpPort);

            // Setup connectors
            Connector httpConnector = createHttpConnector(server, host, httpPort, httpsPort);
            if (isHttps) {
                Connector httpsConnector = createHttpsConnector(server, host, httpsPort, keyStoreFile, keyStorePass, needClientAuth);
                server.setConnectors(new Connector[]{httpConnector, httpsConnector});
            } else {
                server.setConnectors(new Connector[]{httpConnector});
            }

            // Add handlers for requests to collection of handlers
            HandlerCollection handlers = new ContextHandlerCollection();
            //handlers.addHandler(new SecuredRedirectHandler());
            handlers.addHandler(createWebApp(warPath));

            server.setHandler(handlers);
            server.dump();

            System.out.println("Starting jetty websocket and web server...");
            server.start();
            server.join();
        } catch (Throwable t) {
            t.printStackTrace();
            System.err.println("Server initialization failed!");
            System.out.println("Stopping the server...");
            try {
                server.stop();
            } catch (Exception ignored) {}
        }
    }

    private static WebAppContext createWebApp(String warPath) {
        WebAppContext webApp = new WebAppContext();
        webApp.setContextPath("/");
        webApp.setWar(new File(warPath).getAbsolutePath());
        webApp.setThrowUnavailableOnStartupException(true);

        // Enable support for JSR-356 javax.websocket
        webApp.setAttribute("org.eclipse.jetty.websocket.jsr356", Boolean.TRUE);

        // Jetty will scan project for configuration files... This is very important for loading websocket endpoints by annotation automatically
        webApp.setConfigurations(new Configuration[] {
                new AnnotationConfiguration(),
                new WebInfConfiguration(),
                new WebXmlConfiguration(),
                new MetaInfConfiguration(),
                new FragmentConfiguration(),
                new EnvConfiguration(),
                new PlusConfiguration(),
                new JettyWebXmlConfiguration()
        });

        return webApp;
    }

    private static Connector createHttpConnector(Server server, String host, int httpPort, int httpsPort) {
        HttpConfiguration httpConf = new HttpConfiguration();
        httpConf.setSendServerVersion(false);
        if (isHttps) httpConf.setSecurePort(httpsPort);
        ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConf));
        connector.setPort(httpPort);
        connector.setHost(host);

        return connector;
    }

    private static Connector createHttpsConnector(Server server, String host, int httpsPort, File keyStoreFile, String keyStorePass, boolean needClientAuth) {
        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setKeyStorePath(keyStoreFile.getAbsolutePath());
        sslContextFactory.setKeyStorePassword(keyStorePass);
        sslContextFactory.setNeedClientAuth(needClientAuth);

        // Setup HTTPS Configuration
        HttpConfiguration httpsConf = new HttpConfiguration();
        httpsConf.setSendServerVersion(false);
        httpsConf.setSecureScheme("https");
        httpsConf.setSecurePort(httpsPort);
        httpsConf.setOutputBufferSize(32768);
        httpsConf.setRequestHeaderSize(8192);
        httpsConf.setResponseHeaderSize(8192);
        httpsConf.addCustomizer(new SecureRequestCustomizer()); // adds ssl info to request object

        // Establish the HTTPS ServerConnector
        ServerConnector httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConf));
        httpsConnector.setPort(httpsPort);
        httpsConnector.setHost(host);

        return httpsConnector;
    }

    private static void loadKeyStores(String keyStorePath) {
        keyStoreFile = new File(keyStorePath);
        if (!keyStoreFile.exists()) {
            throw new RuntimeException("Key store file does not exist on path '"+keyStoreFile.getAbsolutePath()+"'");
        }
    }
}

This is my servlet:

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(displayName = "MyServlet", urlPatterns = { "/*" })
public class MyServlet extends HttpServlet {
    @Override
    public void init() {
        // start new Thread with my server logic here (avoid classloader issues)
        // but at least one HTTP request is needed to start it from this place
    }

    @Override
    public void destroy() {}

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        // handle http requests
    }
}

I found this on google, but I don't know how to use it in my case. https://www.eclipse.org/lists/jetty-users/msg02109.html

Thank you for your help.


Solution

  • If you just want the servlet to init on startup, then use the annotation ...

    @WebServlet(
        displayName = "MyServlet", 
        urlPatterns = { "/*" },
        loadOnStartup = 1
    )
    

    Alternatively, you could register a javax.servlet.ServletContextListener that does the contextInitialized(ServletContextEvent sce) behavior you need.

    Tip: if you define a custom the ServletContextListener for embedded use, you can just add it to the WebAppContext from outside of the WAR you are using.

    Example:

    webApp.getServletHandler()
        .addListener(new ListenerHolder(MyContextListener.class));
    

    Also, this block of code is wrong and shows you copy/pasted from an old code snippet (this technique is from circa Jetty 9.0.0 thru 9.2.16)

            webApp.setConfigurations(new Configuration[] {
                    new AnnotationConfiguration(),
                    new WebInfConfiguration(),
                    new WebXmlConfiguration(),
                    new MetaInfConfiguration(),
                    new FragmentConfiguration(),
                    new EnvConfiguration(),
                    new PlusConfiguration(),
                    new JettyWebXmlConfiguration()
            });
    

    In Jetty 9.4.x you never directly configure the webApp.setConfigurations() like that, use the Configuration.ClassList defined on the server instead ...

    From: 9.4.44.v20210927 - embedded/LikeJettyXml.java

    Configuration.ClassList classlist = Configuration.ClassList
        .setServerDefault(server);
    classlist.addAfter(
        "org.eclipse.jetty.webapp.FragmentConfiguration",
        "org.eclipse.jetty.plus.webapp.EnvConfiguration",
        "org.eclipse.jetty.plus.webapp.PlusConfiguration");
    classlist.addBefore(
        "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
        "org.eclipse.jetty.annotations.AnnotationConfiguration");
    

    Starting in Jetty 10.0.0, you never specify the Configuration classes, or their order, as the existence of the support JAR is enough, and internally in Jetty 10 the order is resolved properly.

    But if you need to add Configurations (due to non-standard deployment concerns where the Java ServiceLoader doesn't work), then you still configure the additional Configurations on the server object (but without worrying about the correct order for those configurations)

    From 10.0.7 - embedded/demos/LikeJettyXml.java

    Configurations.setServerDefault(server).add(
        new EnvConfiguration(), 
        new PlusConfiguration(), 
        new AnnotationConfiguration()
    );