Controladores funcionais no Spring MVC
1. Introdução
2. Configuração do Maven
Estaremos usandoSpring Boot para demonstrar as novas APIs.
Essa estrutura suporta a abordagem familiar baseada em anotação da definição de controladores. Mas também adiciona uma nova linguagem específica de domínio que fornece uma maneira funcional de definir controladores.
Do Spring 5.2 em diante, a abordagem funcional também seráavailable in the Spring Web MVC framework. Assim como no móduloWebFlux,RouterFunctionseRouterFunction são as principais abstrações desta API.
Então, vamos começar importando as dependências necessárias:
org.springframework.boot
spring-boot-starter-parent
2.2.0.BUILD-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
Como o Spring 5.2 ainda não está disponível de maneira geral, precisaremosimport these dependencies via the Spring repositories:
spring-snapshots
Spring Snapshots
https://repo.spring.io/snapshot
true
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
spring-snapshots
Spring Snapshots
https://repo.spring.io/snapshot
true
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
3. RouterFunction vs @Controller
In the functional realm, a web service is referred to as a routee o conceito tradicional de@Controllere@RequestMapping é substituído por aRouterFunction.
Para criar nosso primeiro serviço, vamos pegar um serviço baseado em anotações e ver como ele pode ser traduzido em seu equivalente funcional.
Usaremos o exemplo de um serviço que retorna todos os produtos em um catálogo de produtos:
@RestController
public class ProductController {
@RequestMapping("/product")
public List productListing() {
return ps.findAll();
}
}
Agora, vamos dar uma olhada em seu equivalente funcional:
@Bean
public RouterFunction productListing(ProductService ps) {
return route().GET("/product", req -> ok().body(ps.findAll()))
.build();
}
3.1. A definição da rota
Devemos notar que na abordagem funcional, o métodoproductListing() retorna umRouterFunction ao invés do corpo da resposta. It’s the definition of the route, not the execution of a request.
ORouterFunction inclui o caminho, os cabeçalhos de solicitação, uma função de manipulador, que será usada para gerar o corpo de resposta e os cabeçalhos de resposta. Pode conter um único ou um grupo de serviços da web.
Abordaremos grupos de serviços da web em mais detalhes quando examinarmos as rotas aninhadas.
Neste exemplo,we’ve used the static route() method in RouterFunctions to create a RouterFunction. Todas as solicitações e atributos de resposta para uma rota podem ser fornecidos usando este método.
3.2. Solicitar Predicados
Em nosso exemplo, usamos o método GET () em route () para especificar que se trata de uma solicitaçãoGET, com um caminho fornecido comoString.
Também podemos usarRequestPredicate quando quisermos especificar mais detalhes da solicitação.
Por exemplo, o caminho no exemplo anterior também pode ser especificado usandoRequestPredicate como:
RequestPredicates.path("/product")
Aqui,we’ve used the static utility RequestPredicates to create an object of RequestPredicate.
3.3. Resposta
Da mesma forma,ServerResponse contains static utility methods that are used to create the response object.
Em nosso exemplo, usamosok() para adicionar um Status HTTP 200 aos cabeçalhos de resposta e, em seguida, usamosbody() para especificar o corpo da resposta.
Além disso,ServerResponse suporta a construção de resposta de tipos de dados personalizados usandoEntityResponse.. Também podemos usarModelAndView do Spring MVC viaRenderingResponse.
3.4. Registrando a rota
A seguir, vamos registrar esta rota usando a anotação@Bean para adicioná-la ao contexto do aplicativo:
@SpringBootApplication
public class SpringBootMvcFnApplication {
@Bean
RouterFunction productListing(ProductController pc, ProductService ps) {
return pc.productListing(ps);
}
}
Agora, vamos implementar alguns casos de uso comuns que encontramos ao desenvolver serviços da web usando a abordagem funcional.
4. Rotas aninhadas
É bastante comum ter vários serviços da web em um aplicativo e também dividi-los em grupos lógicos com base na função ou entidade. Por exemplo, podemos querer todos os serviços relacionados a um produto, para começar,/product.
Vamos adicionar outro caminho ao caminho existente/product para encontrar um produto pelo seu nome:
public RouterFunction productSearch(ProductService ps) {
return route().nest(RequestPredicates.path("/product"), builder -> {
builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
}).build();
}
Na abordagem tradicional, teríamos conseguido isso passando um caminho para@Controller. No entanto,the functional equivalent for grouping web services is the nest() method on route().
Aqui, começamos fornecendo o caminho sob o qual queremos agrupar a nova rota, que é/product. Em seguida, usamos o objeto construtor para adicionar a rota da mesma forma que nos exemplos anteriores.
O métodonest() se encarrega de mesclar as rotas adicionadas ao objeto construtor com osRouterFunction principais.
5. Manipulação de erros
Outro caso de uso comum é ter um mecanismo personalizado de tratamento de erros. Podemos usar oonError() method on route() to define a custom exception handler.
Isso é equivalente a usar@ExceptionHandler na abordagem baseada em anotação. Mas é muito mais flexível, pois pode ser usado para definir manipuladores de exceção separados para cada grupo de rotas.
Vamos adicionar um manipulador de exceção à rota de pesquisa de produto que criamos anteriormente para lidar com uma exceção personalizada lançada quando um produto não é encontrado:
public RouterFunction productSearch(ProductService ps) {
return route()...
.onError(ProductService.ItemNotFoundException.class,
(e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
.status(HttpStatus.NOT_FOUND)
.build())
.build();
}
O métodoonError() aceita o objeto de classeException e espera umServerResponse da implementação funcional.
UsamosEntityResponse, que é um subtipo de ServerResponse para construir um objeto de resposta aqui a partir do tipo de dados personalizadoError. Em seguida, adicionamos o status e usamosEntityResponse.build(), que retorna um objetoServerResponse.
6. Filtros
Uma maneira comum de implementar autenticação e gerenciar preocupações transversais, como registro e auditoria, é usar filtros. Filters are used to decide whether to continue or abort the processing of the request.
Vejamos um exemplo em que queremos uma nova rota que adiciona um produto ao catálogo:
public RouterFunction adminFunctions(ProductService ps) {
return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
.onError(IllegalArgumentException.class,
(e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
.status(HttpStatus.BAD_REQUEST)
.build())
.build();
}
Como essa é uma função administrativa, também queremos autenticar o usuário que está chamando o serviço.
Podemos fazer isso adicionando um métodofilter() em route ():
public RouterFunction adminFunctions(ProductService ps) {
return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
.filter((req, next) -> authenticate(req) ? next.handle(req) :
status(HttpStatus.UNAUTHORIZED).build())
....;
}
Aqui, como o métodofilter() fornece a solicitação, bem como o próximo manipulador, nós o usamos para fazer uma autenticação simples que permite que o produto seja salvo se for bem-sucedido ou retorna um erroUNAUTHORIZED para o cliente em caso de falha.
7. Preocupações transversais
Sometimes, we may want to perform some actions before, after or around a request. Por exemplo, podemos querer registrar alguns atributos da solicitação de entrada e da resposta de saída.
Vamos registrar uma declaração toda vez que o aplicativo encontrar uma correspondência para a solicitação recebida. We’ll do this using the before() method on route():
@Bean
RouterFunction allApplicationRoutes(ProductController pc, ProductService ps) {
return route()...
.before(req -> {
LOG.info("Found a route which matches " + req.uri()
.getPath());
return req;
})
.build();
}
Da mesma forma, o métodowe can add a simple log statement after the request has been processed using the after() emroute():
@Bean
RouterFunction allApplicationRoutes(ProductController pc, ProductService ps) {
return route()...
.after((req, res) -> {
if (res.statusCode() == HttpStatus.OK) {
LOG.info("Finished processing request " + req.uri()
.getPath());
} else {
LOG.info("There was an error while processing request" + req.uri());
}
return res;
})
.build();
}
8. Conclusão
Neste tutorial, começamos com uma breve introdução à abordagem funcional para definir controladores. Em seguida, comparamos as anotações do Spring MVC com seus equivalentes funcionais.
Em seguida, implementamos um serviço da web simples que retornava uma lista de produtos com um controlador funcional.
Em seguida, implementamos alguns dos casos de uso comuns para controladores de serviços da Web, incluindo rotas de aninhamento, manipulação de erros, adição de filtros para controle de acesso e gerenciamento de preocupações transversais, como o log.
Como sempre, o código de exemplo pode ser encontrado emGitHub.