Spring MVC with Functional Web Framework Example

Contains an Example of Functional Controllers in Spring MVC. Learn to create a Controller less REST Service using Spring Boot and Spring Router Functions and Handlers.

Overview

Spring Functional Web Framework supports Java functional style request mapping and handling. The framework was first introduced in Spring 5 for Spring reactive framework and it is also available in Spring MVC.

In this tutorial we will create a Spring Boost REST service. Most importantly, instead of writing controllers to handle requests, we will use Spring Functional WebFramework style Router Functions and Handler Functions.

To learn more about Spring 5 Functional Web Framework, refer to Spring Functional Web Framework Guide.

Spring MVC with Router Functions

As stated earlier Spring Functional Web Framework was initially created in Spring WebFlux. Spring WebFlux is a reactive sibling of Spring MVC. As per Spring both of these web frameworks will live and grow side by side. Keeping that in mind the Functional Web Framework was also made available to Spring MVC applications.

Spring Controllers

Since the beginning, Spring MVC has been supporting the concept of Controllers. Controllers can contain multiple handler methods where each handler method has a @RequestMapping annotation. The @RequestMapping annotation can map a specific request to the associated request handler.

For example, have a look PersonController that has a single method, which can handle GET /persons/{id} request.

PersonController.java

@RestController
public class PersonController {
    private PersonService personService;
    
    @GetMapping("/persons/{id}")
    public Mono<Person> getPerson(@PathVariable Long id) {
        return personService.getPerson(id);
    }
}Code language: Java (java)

Router Functions

With the introduction of Functional Web Framework, we can replace Spring Controllers with Routing Functions. The routing functions are equivalent to the @RequestMapping annotations and more than that.

With the help of Router Function, we can rewrite the GET /persons/{id} endpoint like this:

@Bean
RouterFunction<ServerResponse> getPersonRoute() {
    return RouterFunctions.route(GET("/persons/{id}"),
      request -> {
          Mono<Person> person =
            personService.getPerson(parseLong(request.pathVariable("id")));
          return ServerResponse.ok()
            .body(BodyInserters.fromPublisher(person, Person.class));
      }
    );
}Code language: Java (java)

The Router Function defines a RequestPredicate and a Handler. The request predicate choses a request the respective handler can handle. Inside our handler function the processing is being delegated to a service class.

In the next sections, we will create a Spring MVC Application with Functional Controllers from Scratch.

Setup

For the tutorial, we will create a RESTful service to manage student information. Let’s create an empty Spring Boot project and follow the steps in the tutorial.

REST Endpoints

We’ll implement next three endpoints in the Spring MVC Student Service.

  • GET /students
  • GET /students/{id}
  • POST /students

Dependency

The most essential dependency for our application is the Spring Web module. Let’s add spring-boot-starter-web dependency.

pom.xml (Maven)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>Code language: HTML, XML (xml)

build.gradle (Gradle)

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web'Code language: Gradle (gradle)

Service Layer

In a real world application, the data will be accessed from a Database, or any other services. However, we want to keep our focus limited to the Routing Functions. Thus, we will create a mock service to support three endpoint operations.

@Service
public class StudentService {
    public Student getStudent(Long id) {
        return 
          new Student(id, "fName", "lName", 2030);
    }

    public List<Student> getStudents() {
        return 
          List.of(
            new Student(111L, "fName1", "lName1", 2030),
            new Student(112L, "fName2", "lName2", 2031)
          );
    }

    public Student addStudent(Student student) {
        return student;
    }
}Code language: Java (java)

Functional Routes

Unlike Controllers, functional routes do not require a dedicated class. All we need to do is, to create a @Bean factory method and return an instance of RouterFunction.

Thus, we will create a @Configuration class to hold all of the RouterFunctions.

@Configuration
public class StudentFunctionalConfig {
    private final StudentService service;

    public StudentFunctionalConfig(StudentService service) {
        this.service = service;
    }
    ...
}Code language: Java (java)

GET Collections of Resources

First, we will implement the GET endpoint that returns all of the students.

@Bean
public RouterFunction<ServerResponse> getStudentsRouter() {
    return route(GET("/students"),
      request -> ok().body(service.getStudents()));
}Code language: Java (java)

The Predicate associated with the router clearly defines the endpoint it can handle. The Handler function, simply invokes the service class method, and then creates a ServerResponse based on the data returned by the service.

GET Single Resource

Secondly, we’ll implement find a student by Id endpoint. To do so, the handler needs to read the provided Id from the request path. The example also shows how to read request path variables in Functional Routers.

@Bean
public RouterFunction<ServerResponse> getStudentRouter() {
    return route(GET("/students/{id}"),
      request -> {
        Long id = Long.parseLong(request.pathVariable("id"));
        return ok().body(service.getStudent(id));
      }
   );
}Code language: Java (java)

By default the path variables are read in String form. Thus, handler is first parsing the path variable into a long. The parsed value is then passed to the student service method.

POST a Resource

Lastly, we will create a POST endpoint on the Students service. To do so, the handler needs to read the student from the request body.

@Bean
public RouterFunction<ServerResponse> createStudent() {
    return route(POST("/students/"),
      request -> {
        Student student = request.body(Student.class);
        return ok().body(service.addStudent(student));
      }
    );
}Code language: Java (java)

The hander reads the Student from the request body and passes it to the service method.

Testing Routes

In order to test the newly created routes, we will write @SpringBootTest test.

Let’s first test the endpoint that returns all students.

@Test
public void testGetStudents() {
    Student student = new Student(1L, "f1", "l1", 1999);
    when(service.getStudents()).thenReturn(List.of(student));

    Student[] result = new RestTemplate()
        .getForEntity(URL + "/students", Student[].class)
        .getBody();

    assertEquals(student, result[0]);
}Code language: Java (java)

Here, we are using RestTemplate to test the routing function. The @SpringBootTest launches the application in the test context. However, we have mocked the service instance. Thus, the tests will execute the actual routes but the service response will be mocked.

Next, we will test the endpoint that returns a single student.

@Test
public void testGetStudent() {
    Student student = new Student(1L, "f1", "l1", 1999);
    when(service.getStudent(1L)).thenReturn(student);

    Student result = new RestTemplate()
        .getForEntity(URL + "/students", Student.class)
        .getBody();

    assertEquals(student, result);
}Code language: Java (java)

At first, we are mocking the service method so that it returns a particular student instance when invoked. In the end we compare if the same student is returned by the routing function.

Lastly, we will test the create student endpoint.

@Test
public void testCreateStudent() {
    Student student = new Student(1L, "f1", "l1", 1999);
    when(service.addStudent(student)).thenReturn(student);

    Student result = new RestTemplate()
        .postForObject(URL + "/students", entity(student), Student.class);

    assertEquals(student, result);
}Code language: Java (java)

We are POSTing the student entity using RestTemplate. Also we have mocked the service method to return the same object as in input. Lastly, we verify if the returned student is the same as the one sent.

Summary

In this tutorial, we created a Functional Controller in a Spring MVC application. Functional Controllers are a @Bean factories that return Router Functions. The Router Functions are a flexible way of routing and handling requests.

We, then from the scratch built a Spring Boot application and by using functional routing we created three REST Endpoints. Lastly, we tested each of the endpoints by using @SpringBootTest.