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

Posted

技术标签:

【中文标题】是否可以设置 Undertow 来服务 Spring REST 端点?【英文标题】:Is it possible to set up Undertow to serve Spring REST endpoints? 【发布时间】:2019-03-24 07:03:32 【问题描述】:

我的主要目标是在我的应用程序中嵌入 Redhat's Undertow,而不使用任何 web.xml 和 Spring Boot。 Undertow 看起来与 servlet 容器非常接近,可以满足我的要求,同时又超高性能和精简。至于其他微框架,我也查看了SparkJava,但我首先尝试了 Undertow,因为它的文档看起来更好。

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

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

我找不到任何显示如何或是否可以将 Spring MVC / REST 控制器注释与基本的 Undertow 结构链接起来的东西。

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

在我对 Spring 和 Undertow 的了解中,关于如何将两者结合起来,我缺少一大块,但我可以从 Baeldung / Configuring Spring Boot 看到 Spring 提供了一种在 Boot 中使用 Undertow 的方法。我只是不需要 Spring Boot。而且我真的不热衷于挖掘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 邮件列表也无法搜索。

【问题讨论】:

【参考方案1】:

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

【讨论】:

【参考方案2】:

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

在提取相关部分后,我重构了我得到的内容并将其归结为基本要素。这首先是集成测试类。

    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 管道。我将它与 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,以便在 servlet 容器启动阶段由 ServletContainerInitializer 调用。

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 实现调用 Spring 上下文初始化(在 getRootConfigClasses 方法中):

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

以这种方式在 Undertow 中启动整个 Spring REST 服务器大约需要 1 秒。 有了 RESTassured 进行测试,一切都很顺利。

【讨论】:

您的代码中可能存在拼写错误:'server = ServletUtils.buildUndertowServer' 应该是 'server = Undertow.buildUndertowServer'? 非常有用的代码,但在我的情况下,我必须将 MySpringWebApplicationContext 直接提供给 ServletContainerInitializerInfo,而不使用 MySpringServletInitializer。不知道原因,我只是尝试过 :-) 否则我在 MySpringWebApplicationContext 中的方法不会被调用(你没有任何方法)。

以上是关于是否可以设置 Undertow 来服务 Spring REST 端点?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 undertow 服务器中将端口设置为 80 失败? (爪哇)

Undertow - 如何设置反向代理来监控我的 REST API 的 HTTP/HTTPS 流量

SpringBoot2使用Undertow来提高应用性能(spring-boot-starter-undertow)

压缩 Undertow 服务器响应

开微服务项目tomcat更换成undertow

undertow 多个 web 服务 url