Spring MVCのFunctional Endpoints、通称WebMvc.fnをSpring BootなしでEmbedded Tomcat上で使ってみます。
まずは、Functional Endpointsを定義します。定番の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();
}
}
Applicationを起動するためのクラスは次のようになります。DispatcherServlet
の定義とEmbedded Tomcatのセットアップのみです。少しboilerplateが多いですが、至ってシンプルです。
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();
}
}
pom.xml
は次のようになります。dependency managementにSpring Bootを使いますが、アプリにはBootは使用しなくて良いです。
<?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>
これで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 が対応されればもっと簡単にテストを書けるでしょう。
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を使った場合は、以下のようにコードを省略できます。
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);
}
}
pom.xml
とテストコードの変更点は割愛します。
全差分は https://github.com/making/vanilla-mvcfn/compare/boot で確認できます。