Wraz ze Spring Boot 2 w wersji 2.2.0 pojawiła się nowa funkcjonalność: WebMvc.fn. Jest to implementacja funkcyjnego podejścia do definiowania endpointów, podobnie jak ma to miejsce przy wykorzystaniu Spring WebFlux. O tym, jak to rozwiązanie działa w WebFlux, można było przeczytać w jednym z naszych artykułów: #1 Spring Boot 2 – Router functions. Dziś sprawdzimy, jak to funkcyjne podejście sprawdza się w klasycznym stosie MVC.

WebMvc.fn

Jak pisałem we wstępie, w nowym Spring Boot 2 możemy tworzyć naszą część serwerową na dwa sposoby (można je mieszać w jednej aplikacji):

  • Podejście klasyczne – korzystając z adnotacji @Controller i innych związanych z Webem.
  • Podejście funkcyjne – korzystając z WebMvc.fn.

Zależności

Ponieważ wsparcie dla funkcyjnego podejścia pojawiło się w kontekście stosu MVC, wystarczy, że w naszym projekcie dodamy standardowy moduł Web:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Router Function

Podobnie jak w przypadku Spring WebFlux, tworzymy konfigurację z wykorzystaniem klasy RouterFunction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package pl.codecouple.webmvc.fn.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import java.net.URI;
import java.util.Arrays;
import java.util.List;

import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration
class RouterConfig {

@Bean
public RouterFunction<ServerResponse> routes(DataService dataService) {
return route()
.GET("/code-couple", serverRequest ->
ServerResponse.ok()
.body(
dataService.getAll()))
.GET("/code-couple/{id}", r ->
ServerResponse.ok()
.body(
dataService.getById(
Long.parseLong(r.pathVariable("id")))))
.POST("/code-couple", r -> {
dataService.save(r.body(String.class));
return ServerResponse
.created(URI.create("/location")).build();
})
.build();
}

}

W wyniku naszej lambdy otrzymujemy obiekt typu ServerRequest, z którego możemy odczytać wiele informacji:

  • cookies() - dostęp do ciasteczek.
  • headers() - dostęp do nagłówków.
  • sessions() - dostęp do sesji.
  • pathVariable(name) - dostęp do zmiennej ze ścieżki.
  • servletRequest() - dostęp do obiektu typu HttpServletRequest, z którego można pobrać wszystkie informacje.

Handler

Jeśli chcemy oddzielić konfigurację routingu od logiki, możemy przygotować klasę zawierającą wszystkie handlery. Metody HTTP przyjmują obiekt typu HandlerFunction<ServerResponse>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Bean
public RouterFunction<ServerResponse> routes(DataHandler dataHandler) {
return route()
.GET("/code-couple", dataHandler::handleGetAll)
.GET("/code-couple/{id}", dataHandler::handleGetOne)
.POST("/code-couple", dataHandler::handlePost)
.build();
}

@Component
public class DataHandler {

private final DataService dataService;

public DataHandler(DataService dataService) {
this.dataService = dataService;
}

public ServerResponse handleGetAll(ServerRequest serverRequest) {
return ServerResponse.ok()
.body(dataService.getAll());
}

public ServerResponse handleGetOne(ServerRequest serverRequest) {
return ServerResponse.ok()
.body(dataService.getById(
Long.parseLong(
serverRequest.pathVariable("id"))));
}

public ServerResponse handlePost(ServerRequest serverRequest)
throws ServletException, IOException {
dataService.save(serverRequest.body(String.class));
return ServerResponse.created(URI.create("/location")).build();
}

}

Github

Całość jak zawsze na GitHubie.