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.