Jul 17, 2023
Jul 17, 2023
N/A Views
MD
warning
この記事は2年以上前に更新されたものです。情報が古くなっている可能性があります。

Warning

This article was automatically translated by OpenAI (gpt-4o).It may be edited eventually, but please be aware that it may contain incorrect information at this time.

Let's try using Spring MVC's Functional Endpoints, commonly known as WebMvc.fn, on Embedded Tomcat without Spring Boot.

First, we define the Functional Endpoints. We will write a lambda expression for an endpoint that outputs the classic "Hello World".

package com.example;

import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

public class Routing {
    public RouterFunction<ServerResponse> routes() {
        return RouterFunctions.route()
                .GET("/", request -> ServerResponse.ok().body("Hello World!"))
                .build();
    }
}

The class to start the Application will look like this. It only includes the definition of DispatcherServlet and the setup of Embedded Tomcat. Although it has a bit of boilerplate, it is quite simple.

package com.example;

import java.util.List;
import java.util.Optional;

import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;

import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.servlet.function.support.RouterFunctionMapping;

public class Application {

    public static void main(String[] args) throws Exception {
        startTomcat();
    }

    static StaticWebApplicationContext applicationContext(RouterFunction<ServerResponse> routes) {
        StaticWebApplicationContext applicationContext = new StaticWebApplicationContext();
        applicationContext.registerBean(DispatcherServlet.HANDLER_MAPPING_BEAN_NAME,
                HandlerMapping.class, () -> {
                    RouterFunctionMapping mapping = new RouterFunctionMapping(routes);
                    mapping.setMessageConverters(List.of(
                            new StringHttpMessageConverter(),
                            new ByteArrayHttpMessageConverter(),
                            new AllEncompassingFormHttpMessageConverter(),
                            new MappingJackson2HttpMessageConverter()
                    ));
                    return mapping;
                });
        return applicationContext;
    }


    static void startTomcat() throws Exception {
        Tomcat tomcat = new Tomcat();
        int port = Optional.ofNullable(System.getenv("PORT")).map(Integer::parseInt).orElse(8080);
        tomcat.getConnector().setPort(port);
        Context context = tomcat.addContext("", System.getProperty("java.io.tmpdir"));
        DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext(new Routing().routes()));
        Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet).addMapping("/");
        tomcat.start();
        tomcat.getServer().await();
    }

}

The pom.xml will look like this. We use Spring Boot for dependency management, but we don't need to use Boot in the application.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>vanilla-mvcfn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>vanilla-mvcfn</name>
    <description>vanilla-mvcfn</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Now, you can run the Application class or create an executable jar with ./mvnw clean package and run it. Accessing http://localhost:8080 will return "Hello World!".

$ curl localhost:8080
Hello World!

You can write tests using MockMvc as follows. As of Spring 6.0, you cannot use MockMvc standalone like with @Controller, so you need to go through WebApplicationContext, which is a bit verbose. Once https://github.com/spring-projects/spring-framework/issues/30477 is addressed, it will be easier to write tests.

package com.example;

import org.junit.jupiter.api.Test;

import org.springframework.mock.web.MockServletContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.support.StaticWebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class ApplicationTests {
    MockMvc mockMvc = initMockMvc();

    MockMvc initMockMvc() {
        StaticWebApplicationContext applicationContext = Application.applicationContext(new Routing().routes());
        applicationContext.setServletContext(new MockServletContext());
        applicationContext.refresh();
        // https://github.com/spring-projects/spring-framework/issues/30477
        return MockMvcBuilders.webAppContextSetup(applicationContext).build();
    }

    @Test
    void hello() throws Exception {
        this.mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello World!"));
    }

}

The source code is available at https://github.com/making/vanilla-mvcfn.


By the way, if you use Spring Boot, you can simplify the code as follows.

package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

@Configuration
public class Routing {

    @Bean
    public RouterFunction<ServerResponse> routes() {
        return RouterFunctions.route()
                .GET("/", request -> ServerResponse.ok().body("Hello World!"))
                .build();
    }
}
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Changes to pom.xml and test code are omitted.

You can check the full diff at https://github.com/making/vanilla-mvcfn/compare/boot.

Found a mistake? Update the entry.
Share this article: