---
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<ServerResponse> 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<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
<?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 が対応されればもっと簡単にテストを書けるでしょう。

```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<ServerResponse> 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 で確認できます。
