--- title: Spring Bootを使わずEmbedded TomcatでWebMvc.fnを使う tags: ["Java", "Spring Boot", "Spring MVC", "WebMvc.fn"] categories: ["Programming", "Java", "org", "springframework", "web", "servlet", "function"] date: 2023-07-17T16:25:50Z updated: 2023-07-17T16:33:03Z --- Spring MVCの[Functional Endpoints](https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html)、通称WebMvc.fnをSpring BootなしでEmbedded Tomcat上で使ってみます。 まずは、Functional Endpointsを定義します。定番のHello Worldを出力するエンドポイントのみをラムダ式で記述します。 ```java 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 routes() { return RouterFunctions.route() .GET("/", request -> ServerResponse.ok().body("Hello World!")) .build(); } } ``` Applicationを起動するためのクラスは次のようになります。`DispatcherServlet`の定義とEmbedded Tomcatのセットアップのみです。少しboilerplateが多いですが、至ってシンプルです。 ```java 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 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(); } } ``` `pom.xml`は次のようになります。dependency managementにSpring Bootを使いますが、アプリにはBootは使用しなくて良いです。 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.1.1 com.example vanilla-mvcfn 0.0.1-SNAPSHOT vanilla-mvcfn vanilla-mvcfn 17 org.springframework spring-webmvc com.fasterxml.jackson.core jackson-databind org.apache.tomcat.embed tomcat-embed-core org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ``` これで`Application`クラスを実行または`./mvnw clean package`で実行可能jarを作って実行し、http://localhost:8080 にアクセスすれば"Hello World!"が返ります。 ``` $ curl localhost:8080 Hello World! ``` MockMvcを使ったテストは次のように書けます。Spring 6.0時点では@ControllerのようにStandalneでMockMvcが使えないのでWebApplicationContextを経由する必要があり、少し冗長です。 https://github.com/spring-projects/spring-framework/issues/30477 が対応されればもっと簡単にテストを書けるでしょう。 ```java 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!")); } } ``` ソースコードは https://github.com/making/vanilla-mvcfn です。 --- ちなみにSpring Bootを使った場合は、以下のようにコードを省略できます。 ```java 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 routes() { return RouterFunctions.route() .GET("/", request -> ServerResponse.ok().body("Hello World!")) .build(); } } ``` ```java 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); } } ``` `pom.xml`とテストコードの変更点は割愛します。 全差分は https://github.com/making/vanilla-mvcfn/compare/boot で確認できます。