是否可以设置 Undertow 来为 Spring REST 端点提供服务?

Is it possible to set up Undertow to serve Spring REST endpoints?

我的主要目标是设置 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 但我首先尝试使用 Undertow,因为它的文档看起来更好。

Undertow 听起来不错,但我遇到的所有文档和教程在 / 上返回“Hello World”后都停止了。也许我能找到的最好的是 StubbornJava/RestServer.java,其中所有端点都是硬编码的,例如:

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

我找不到任何显示如何或是否有可能 link 使用基本 Undertow 结构的 Spring MVC / REST 控制器注释。

我已经有一个应用程序,其中包含在 Spring 注释中定义的一组端点。

我对 Spring 和 Undertow 的了解有很大一部分是关于如何将两者结合起来的,但我可以从 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 中看到 Pivotal 是如何做到的,因为它可能赢了在我的情况下可以复制。 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;
}

我的猜测是我必须以编程方式获取带注释的 Spring REST 控制器并为每个控制器创建所需的 Undertow 资源。

Undertow 邮件列表似乎也无法搜索到。

我不得不硬着头皮浏览 Spring 源代码,看看 Pivotal 是如何将它们联系在一起的。

提取相关位后,我重构了我得到的内容并将其归结为要点。这首先是集成测试 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")));
    }

我在一个实用程序中做了 Undertow 管道 class。我将它与 Spring 完全分开 - 这就是为什么我将 javax.servlet.ServletContainerInitializer 的 Spring 实现作为参数发送。

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();
}

您必须实施 Spring 的 AbstractAnnotationConfigDispatcherServletInitializer 并将其传递到 Undertow 以供 ServletContainerInitializer 在 servlet 容器启动阶段调用。

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);
    }
}

此 Spring servlet 初始化程序将通过您实现 AnnotationConfigWebApplicationContext(在 getRootConfigClasses 方法中)调用 Spring 上下文初始化:

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

以这种方式在 Undertow 中启动整个 Spring REST 服务器大约需要 1 秒。 并且通过 RESTassured 进行测试,这一切都是完美的。

我创建了一个使用 Spring 和 Undertow 构建的示例项目。它也与 JSP 和 Open Api 3.0 集成。您可以在这里查看:https://github.com/essentialprogramming/undertow-spring-web