3

My main aim is to set up Redhat's Undertow embedded in my app without any web.xml and without Spring Boot. Undertow looks like it's close enough to a servlet container to fulfill my requirements and at the same time super-performant and lean. As far as other microframeworks go, I also looked at SparkJava but am trying out Undertow first because its docs look better.

So Undertow sounds great but all the docs and tutorials I come across stop after returning "Hello World" on /. Perhaps the best I could find is StubbornJava/RestServer.java where all the endpoints are hard-coded, e.g.:

public static final RoutingHandler ROUTES = new RoutingHandler()
    .get("/users", timed("listUsers", UserRoutes::listUsers))

What I can't find is anything showing how or if it's even possible to link up the Spring MVC / REST controller annotations with the basic Undertow structure.

I already have an app with a set of endpoints defined in Spring annotations.

I have a big piece missing from my knowledge of Spring and Undertow about how to combine the two, but I can see from Baeldung / Configuring Spring Boot that Spring provides a way to use Undertow in Boot. I just don't need Spring Boot. And I'm really not enthusiastic about digging into the Spring source to see how Pivotal did it, since it probably won't be replicable in my situation. This is the way to implement it in Boot:

@Bean
public UndertowEmbeddedServletContainerFactory embeddedServletContainerFactory() {
    UndertowEmbeddedServletContainerFactory factory = 
      new UndertowEmbeddedServletContainerFactory();

    factory.addBuilderCustomizers(new UndertowBuilderCustomizer() {
        @Override
        public void customize(io.undertow.Undertow.Builder builder) {
            builder.addHttpListener(8080, "0.0.0.0");
        }
    });

    return factory;
}

My guess is that I'd have to programmatically grab the annotated Spring REST controllers and create the required Undertow resources for each.

It seems like the Undertow mailing list is unsearchable too.

Adam
  • 5,215
  • 5
  • 51
  • 90

2 Answers2

3

I had to bite the bullet and wade through the Spring source code to see how Pivotal had tied it all together.

After pulling out the relevant bits, I refactored what I'd got and boiled it down to the essentials. This first is the integration test class.

    private static Undertow server;

    @BeforeAll
    public static void startServer() throws ServletException {
        server = ServletUtils.buildUndertowServer(
                8080,
                "localhost",
                "",
                SpringServletContainerInitializer.class,
                Collections.singleton(
                        MySpringServletInitializer.class),
                MyTests.class.getClassLoader());
        server.start();
    }

    @AfterAll
    public static void stopServer() {
        try {
            if (server != null) {
                server.stop();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testIsAvailable() {
        Response response = get("/mystuff/isAvailable");
        response.then().statusCode(200);
        ResponseBody body = response.getBody();
        assertThat("Body", body.asString(), is(equalTo("ok")));
    }

And I did the Undertow plumbing in a utility class. I totally separated it out from Spring - which is why I send in the Spring implementation of javax.servlet.ServletContainerInitializer as a parameter.

import io.undertow.servlet.api.ServletContainerInitializerInfo;
import javax.servlet.ServletContainerInitializer;

public static Undertow buildUndertowServer(
        int port,
        String address,
        String contextPath,
        Class<? extends ServletContainerInitializer>
                servletContainerInitializerClass,
        Set<Class<?>> initializers,
        ClassLoader classLoader
) throws ServletException {

    ServletContainerInitializerInfo servletContainerInitializerInfo =
            new ServletContainerInitializerInfo(
                    servletContainerInitializerClass,
                    initializers);
    DeploymentInfo deployment = Servlets.deployment();
    deployment.addServletContainerInitializer(
            servletContainerInitializerInfo);
    deployment.setClassLoader(classLoader);
    deployment.setContextPath(contextPath);
    deployment.setDisplayName("adam");
    deployment.setDeploymentName("int-test");
    deployment.setServletStackTraces(ServletStackTraces.ALL);
    DeploymentManager manager =
            Servlets.newContainer().addDeployment(deployment);
    manager.deploy();
    Undertow.Builder builder = Undertow.builder();
    builder.addHttpListener(port, address);
    HttpHandler httpHandler = manager.start();
    httpHandler = Handlers.path().addPrefixPath(contextPath, httpHandler);
    builder.setHandler(httpHandler);
    return builder.build();
}

You have to implement Spring's AbstractAnnotationConfigDispatcherServletInitializer and pass that into Undertow to be invoked by the ServletContainerInitializer during the servlet container start-up phase.

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class MySpringServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {
                MySpringWebApplicationContext.class
        };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        logger.info("starting up");
        super.onStartup(servletContext);
    }
}

This Spring servlet initializer will invoke the Spring context initialization via your implementation of AnnotationConfigWebApplicationContext (in the getRootConfigClasses method there):

@PropertySource("file:target/application-int.properties")
@Configuration
@ComponentScan(basePackages = { "org.adam.rest" })
@EnableWebMvc
public class MySpringWebApplicationContext
        extends AnnotationConfigWebApplicationContext {
}

Starting up the whole Spring REST server in Undertow this way takes about 1 sec. And with RESTassured to do the testing, it is all peachy.

Adam
  • 5,215
  • 5
  • 51
  • 90
  • Possible typo in your code: 'server = ServletUtils.buildUndertowServer' should be 'server = Undertow.buildUndertowServer'? – xtian Jul 23 '20 at 16:54
  • Really useful code, but in my case I had to feed MySpringWebApplicationContext directly to ServletContainerInitializerInfo without using MySpringServletInitializer. Don't know the reason why, I just tried :-) Otherwise my methods in MySpringWebApplicationContext wouldn't get called (you don't have any). – xtian Jul 23 '20 at 17:21
0

I created a sample project built with Spring and Undertow. It's integrated with JSP and Open Api 3.0 as well. You can check it out here: https://github.com/essentialprogramming/undertow-spring-web