O Spring WebFlux usa o modelo de programação funcional para construir serviços assíncronos sem bloqueio

1. Introdução

A descrição acima apresenta a estrutura WebFlux usada especialmente para criar serviços responsivos da Web na estrutura Spring. Ao mesmo tempo, também forneço um dos dois métodos de implementação para criar terminais HTTP estilo RESTful, ou seja, o modelo de programação de anotação.

Este artigo apresenta outro método de implementação - como usar o modelo de programação funcional para criar serviços RESTful responsivos.Esse modelo de programação é bastante diferente do método tradicional de construção de serviços RESTful com base no Spring MVC.

2 Modelo de programação funcional WebFlux

Revise o diagrama de arquitetura do sistema Spring WebFlux:

Na segunda metade da figura, no Spring WebFlux, o Router Functions, o conceito central do modelo de programação funcional, é comparado a anotações padrão como @Controller e @RequestMapping do Spring MVC. E as funções do roteador fornecem um conjunto de APIs de estilo funcional, as mais importantes são as interfaces do roteador e do manipulador. Simplificando:

  • O roteador corresponde ao RequestMapping
  • Controlador corresponde a Manipulador

Quando faço uma chamada remota, a solicitação HTTP recebida é tratada por um HandlerFunction, que é essencialmente uma função que recebe um ServerRequest e retorna um Mono. ServerRequest e ServerResponse são um par de interfaces imutáveis ​​que fornecem acesso amigável às mensagens HTTP subjacentes.

3 ServerRequest

Representa o objeto de solicitação, que pode acessar vários elementos de solicitação HTTP, incluindo método de solicitação, URI e parâmetros, e obter informações de cabeçalho de solicitação HTTP por meio de um ServerRequest.Headers separado.

ServerRequest fornece acesso ao corpo da mensagem de solicitação por meio de uma série de métodos bodyToMono() e bodyToFlux(). Por exemplo, se quisermos extrair o corpo da mensagem de solicitação como um objeto do tipo Mono, podemos usar o seguinte método.

Mono<String> string = request.bodyToMono(String.class);

E se quisermos extrair o corpo da mensagem de solicitação como um objeto do tipo Flux, podemos usar o seguinte método, onde Order é uma classe de entidade que pode ser desserializada do corpo da mensagem de solicitação.

Flux<Order> order = request.bodyToFlux(Order.class);

Os dois métodos bodyToMono() e bodyToFlux() acima são, na verdade, atalhos para o método utilitário comum ServerRequest.body(BodyExtractor), mostrado abaixo.

<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor);

BodyExtractor é um extrator de corpo de mensagem de solicitação que nos permite escrever nossa própria lógica de extração. Observe que o objeto extraído pelo BodyExtractor é uma instância do tipo ServerHttpRequest, e este ServerHttpRequest não é bloqueador, e também há um objeto ServerHttpResponse correspondente a ele.

Operações web reativas são exatamente este conjunto de não-bloqueio:

  • ServerHttpRequest
  • ServerHttpResponse

Em vez da tradição no Spring MVC:

  • HttpServletRequest
  • HttpServletResponseName

Se você não precisar implementar a lógica de extração personalizada, poderá usar a instância BodyExtractors comum fornecida pela estrutura. Por meio de BodyExtractors, o exemplo acima pode ser substituído por .

Mono<String> string = 
	request.body(BodyExtractors.toMono(String.class);
	 
Flux<Person> Order= 
	request.body(BodyExtractors.toFlux(Order.class);

Resposta do Servidor

Correspondendo a ServerRequest, ServerResponse fornece acesso a respostas HTTP. Como é imutável, um construtor pode ser usado para criar um novo ServerResponse.

Os construtores permitem definir o status da resposta, adicionar cabeçalhos de resposta e fornecer conteúdo específico da resposta. O exemplo a seguir demonstra como criar uma resposta representando um código de status 200 por meio do método ok(), onde defino o tipo do corpo da resposta para o formato JSON e o conteúdo específico da resposta é um objeto Mono.

Mono<Order> order =;
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
     .body(order);

Carregar o conteúdo da resposta através do método body() é a forma mais comum de construir um ServerResponse, aqui usamos o objeto Order como valor de retorno. Se quisermos retornar vários tipos de objetos, também podemos usar os métodos de construção fornecidos pela classe de ferramentas BodyInserters, como os métodos comuns fromObject() e fromPublisher(). No exemplo de código a seguir, retornamos diretamente um "Hello World" por meio do método fromObject().

ServerResponse.ok().body(BodyInserters.fromObject("Hello World"));

Por trás do método acima está o uso de um conjunto de métodos body() na interface BodyBuilder para construir um objeto ServerResponse, o método body() típico é o seguinte.

Mono<ServerResponse> body(BodyInserter<?, ? super ServerHttpResponse> inserter);

Aqui também vemos o objeto ServerHttpResponse sem bloqueio. O uso mais comum desse método body() é retornar os resultados das operações de adição e atualização, que você verá mais adiante neste tutorial.

HandlerFunction

A combinação de ServerRequest e ServerResponse cria um HandlerFunction.

public interface HandlerFunction<T extends ServerResponse> {
    
    
 
    Mono<T> handle(ServerRequest request);
}

Um mecanismo personalizado de tratamento de solicitação-resposta pode ser criado implementando HandlerFunction#handle().

Simples função de manipulador "Hello World".

public class HelloWorldHandlerFunction implements 
	HandlerFunction<ServerResponse> {
    
    
 
        @Override
        public Mono<ServerResponse> handle(ServerRequest request) {
    
    
            return ServerResponse.ok().body(
	BodyInserters.fromObject("Hello World"));
        }
};

Aqui, o método body() fornecido pelo ServerResponse apresentado anteriormente é usado para retornar um corpo de mensagem do tipo String.

Normalmente, existem operações comuns, como CRUD para uma determinada entidade de domínio, portanto, é necessário escrever várias funções de processamento semelhantes, o que é complicado. É recomendável agrupar várias funções do manipulador em uma classe Handler dedicada.

RouterFunction

A lógica de processamento da requisição pode ser criada através de HandlerFunction. Em seguida, precisamos associar requisições específicas a esta lógica de processamento. RouterFunction pode nos ajudar a atingir este objetivo. RouterFunction é semelhante à anotação @RequestMapping no SpringMVC tradicional.

A maneira mais comum de criar um RouterFunction é usar o método route, que cria um objeto ServerResponse usando o verbo de solicitação e a função do manipulador.

public static <T extends ServerResponse> RouterFunction<T> route(
            RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
    
    
 
        return new DefaultRouterFunction<>(predicate, handlerFunction);
}

A lógica principal de RouterFunction reside aqui na classe DefaultRouterFunction, cujo método route() é mostrado abaixo.

public Mono<HandlerFunction<T>> route(ServerRequest request) {
    
    
            if (this.predicate.test(request)) {
    
    
                if (logger.isTraceEnabled()) {
    
    
                    String logPrefix = request.exchange().getLogPrefix();
                    logger.trace(logPrefix + String.format("Matched %s", this.predicate));
                }
                return Mono.just(this.handlerFunction);
            }
            else {
    
    
                return Mono.empty();
            }
}

Esse método roteia o ServerRequest de entrada para a função de manipulador específica HandlerFunction. Retorna o resultado da função do manipulador se a solicitação corresponder a uma rota específica, caso contrário, retorna um Mono vazio.

A classe de ferramenta RequestPredicates fornece predicados comumente usados, que podem realizar correspondência automática com base em condições como caminho, método HTTP e tipo de conteúdo.

Exemplo simples de RouterFunction para implementar o roteamento automático do caminho de solicitação "/hello-world":

RouterFunction<ServerResponse> helloWorldRoute =                RouterFunctions.route(RequestPredicates.path("/hello-world"),
            new HelloWorldHandlerFunction());

Da mesma forma, devemos usar RouterFunction e vários HandlerFunctions em combinação de acordo com os requisitos.Uma prática comum é projetar o RouterFunction correspondente com base em objetos de domínio.

A vantagem do mecanismo de roteamento está em sua capacidade de composição. Duas funções de roteamento podem ser combinadas em uma nova função de roteamento e roteadas para qualquer uma delas com um determinado método de avaliação. Se o predicado da primeira rota não corresponder, o segundo predicado será avaliado. Observe que as funções combinadas do roteador são avaliadas em ordem, portanto, é uma prática recomendada colocar algumas funções específicas antes das funções genéricas. Em RouterFunction, o método de combinação correspondente também é fornecido para atingir esse objetivo, consulte o código a seguir.

default RouterFunction<T> and(RouterFunction<T> other) {
    
    
        return new RouterFunctions.SameComposedRouterFunction<>(this, other);
}
 
default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
    
    
        return and(RouterFunctions.route(predicate, handlerFunction));
}

Podemos combinar duas funções de roteamento chamando um dos dois métodos acima, onde o último é equivalente à integração do método RouterFunction.and() com o método RouterFunctions.route(). O código a seguir demonstra a natureza composicional de RouterFunctions.

RouterFunction<ServerResponse> personRoute =
        route(GET("/orders/{id}").and(accept(APPLICATION_JSON)), personHandler::getOrderById)
.andRoute(GET("/orders").and(accept(APPLICATION_JSON)), personHandler::getOrders)
.andRoute(POST("/orders").and(contentType(APPLICATION_JSON)), personHandler::createOrder);

A maioria dos predicados fornecidos pela classe de utilitário RequestPredicates também são compostos. Por exemplo, a implementação do método RequestPredicates.GET(String) é mostrada abaixo.

public static RequestPredicate GET(String pattern) {
    
    
        return method(HttpMethod.GET).and(path(pattern));
}

Como você pode ver, o método é uma combinação de RequestPredicates.method(HttpMethod.GET) e RequestPredicates.path(String). Podemos construir predicados de solicitação complexos chamando o método RequestPredicate.and(RequestPredicate) ou o método RequestPredicate.or(RequestPredicate).

Integração de casos: Web Services em ReactiveSpringCSS

O atendimento ao cliente precisa acessar os serviços da web nos serviços de atendimento de contas e atendimento de pedidos, respectivamente.

O AccountController em serviço de conta foi implementado com base no modelo de programação de anotação. Hoje demonstra o processo de implementação de Web services no pedido-atendimento.

Com base no modelo de programação funcional, em serviço de pedido, escreva OrderHandler para implementar especificamente a função de processamento de obtenção de entidades de domínio de pedido de acordo com OrderNumber

@Configuration
public class OrderHandler {
    
    
 
    @Autowired
    private OrderService orderService;
 
    public Mono<ServerResponse> getOrderByOrderNumber(ServerRequest request) {
    
    
        String orderNumber = request.pathVariable("orderNumber");
        return ServerResponse.ok().body(this.orderService.getOrderByOrderNumber(orderNumber), Order.class);
    }
}

No exemplo de código acima, criamos uma classe OrderHandler, injetamos OrderService e implementamos um manipulador getOrderByOrderNumber().

Agora que temos o OrderHandler, podemos criar o OrderRouter correspondente. O exemplo de código é o seguinte.

@Configuration
public class OrderRouter {
    
    
 
    @Bean
    public RouterFunction<ServerResponse> routeOrder(OrderHandler orderHandler) {
    
    
        return RouterFunctions.route(
                RequestPredicates.GET("/orders/{orderNumber}")
                    .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
                    orderHandler::getOrderByOrderNumber);
    }
}

Neste exemplo, acionaremos automaticamente o método getOrderByOrderNumber() em orderHandler e retornaremos o ServerResponse correspondente acessando o endpoint "/orders/{orderNumber}".

Em seguida, assumindo que obtivemos o objeto Account de destino e o objeto Order por meio de chamadas remotas, respectivamente, o fluxo de execução do método generateCustomerTicket pode ser esclarecido. Com base no método de implementação da programação responsiva, podemos obter o código de amostra conforme mostrado abaixo.

public Mono<CustomerTicket> generateCustomerTicket(String accountId, String orderNumber) {
    
    
 
        // 创建 CustomerTicket 对象
        CustomerTicket customerTicket = new CustomerTicket();
        customerTicket.setId("C_" + UUID.randomUUID().toString());
 
        // 从远程 account-service 获取 Account 对象
        Mono<AccountMapper> accountMapper = getRemoteAccountByAccountId(accountId);
        // 从远程 order-service 中获取 Order 对象
        Mono<OrderMapper> orderMapper = getRemoteOrderByOrderNumber(orderNumber);
        Mono<CustomerTicket> monoCustomerTicket =
                Mono.zip(accountMapper, orderMapper).flatMap(tuple -> {
    
    
            AccountMapper account = tuple.getT1();
            OrderMapper order = tuple.getT2();
            if(account == null || order == null) {
    
    
                return Mono.just(customerTicket);
            }
            // 设置 CustomerTicket 对象属性
            customerTicket.setAccountId(account.getId());
            customerTicket.setOrderNumber(order.getOrderNumber());
            customerTicket.setCreateTime(new Date());
            customerTicket.setDescription("TestCustomerTicket");
            return Mono.just(customerTicket);
        });
        // 保存 CustomerTicket 对象并返回
        return monoCustomerTicket.flatMap(customerTicketRepository::save);
}

Obviamente, os métodos getRemoteAccountById e getRemoteOrderByOrderNumber aqui envolvem chamadas de serviços da Web remotos sem bloqueio e apresentaremos esse processo em detalhes na próxima aula.

Observe que o operador zip na estrutura Reactor é usado aqui para combinar os elementos no fluxo accountMapper com os elementos no fluxo orderMapper de maneira um-para-um, e um objeto Tuple2 é obtido após a fusão. Em seguida, obtemos os objetos AccountMapper e OrderMapper desse objeto Tuple2, respectivamente, e preenchemos suas propriedades no objeto CustomerTicket gerado. Por fim, chamamos o método save de customerTicketRepository por meio do operador flatMap para concluir a persistência dos dados. Este é um cenário de aplicação muito clássico dos dois operadores zip e flatMap, e você precisa ser proficiente.

Resumir

Tudo bem, então isso é tudo para esta palestra. Continuando com a aula anterior, discutimos como usar o Spring WebFlux e fornecemos um método para criar terminais RESTful com base no modelo de programação funcional. Neste modelo de desenvolvimento, é importante compreender:

  • ServerRequest
  • Resposta do Servidor
  • HandlerFunction
  • RouterFunction

Como usar objetos principais.

Perguntas frequentes

Quais objetos de programação principais estão incluídos no modelo de programação funcional do WebFlux?

Agora que construímos serviços da Web reativos com o WebFlux, a próxima etapa é como consumi-los. O Spring também fornece uma classe de ferramenta WebClient sem bloqueio para atingir esse objetivo.Na próxima palestra, discutirei sistematicamente como usar essa classe de ferramenta. Até lá.

おすすめ

転載: blog.csdn.net/qq_33589510/article/details/131640995