Spring Boot 2.0 - WebFlux framework

原文链接:http://www.dubby.cn/detail.html?id=9031

本文翻译自:Spring Boot官方文档——WebFlux framework,Part V. The Web

本文并非转载,本人也看了http://www.jianshu.com/p/f4ff6d74ad4a,发现很多翻译都过于机械,疑似机器直接翻译,而且丢失了不少段落信息,所以我放弃了转载,而是自己重新翻译,限于本人水平,翻译过程如果有不足的地方,还请见谅。“两者都通过在反应堆顶部建立完全反应”这是那篇文章的,你们随意感受下。

1、介绍

1.1 什么是响应式编程(Reactive Programming)?

简单来说,响应式编程是针对异步和事件驱动的非阻塞应用程序,并且需要少量线程来实现系统的垂直扩展(即在 JVM 内扩展)而不是水平扩展(即通过加机器来扩展)。

响应式应用的一个关键概念是“背压(backpressure)”的概念,这就是确保生产者不会压倒消费者。例如,当HTTP连接变慢时,从数据库延伸到HTTP响应的反应组件的流水线、数据存储库也可以减慢或停止,直到网络恢复。

响应式编程也导致从命令式到声明异步组合逻辑的重大转变。与使用Java 8的 CompletableFuture 编写封锁代码相比,可以通过 lambda
表达式编写后续操作。

如果还想了解更多的介绍,可以阅读Notes on Reactive Programming系列博客。

1.2 响应式 API(Reactive API)和 构建块(Building Blocks)

Spring Framework 5 将 Reactive Streams 作为通过异步组件的协议。Reactive Streams 是行业联合创建的一个规范,现已在Java 9中被采用,表现在 java.util.concurrent.Flow

Spring Framework 在内部使用 Reactor 自己的响应实现。Reactor 是一个 Reactive Streams 实现,还进一步扩展基本的 Reactive Streams的PublisherFluxMono 可组合的API类型,以提供对 0..N0..1 的数据序列的声明性操作。

Spring Framework 在许多自己的 Reactive API 中暴露了 Flux 和
Mono。然而,在应用级别,一如既往,Spring 提供了选择,并完全支持使用RxJava。有关的更多信息,请查看 Sebastien Deleuze 发表的 “Understanding Reactive Types”

2、Spring WebFlux 模块

Spring Framework 5 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对REST,HTML浏览器和 WebSocket风格交互的响应式服务器Web应用程序的支持。

2.1、服务端

在服务器端 WebFlux 支持2种不同的编程模式:

  • 基于注解的 @Controller 和其他Spring MVC也支持的注解(这里说成兼容Spring MVC更好)
  • Functional函数式编程 、Java 8 lambda 风格的路由和处理

这两种编程模式底层都需要依赖Spring Boot的反应式API。下面这张图,暂时了Spring Boot的服务端结构,包含了左边的传统的基于Servlet的spring-webmvc模块,和右边的响应式的spring-webflux模块。

WebFlux 可以在支持 Servlet 3.1 非阻塞 IO API 以及其他支持异步运行的 Servlet 容器上运行(如 Netty 和 Undertow )。每个请求都使用 ServerHttpRequestServerHttpResponse,将请求和响应的正文暴露为 Flux<DataBuffer>,而不是InputStreamOutputStreamFlux<Object> 在支持REST风格的 JSON 和 XML 序列化和反序列化方面也有优势,在HTML视图呈现和服务器发送事件也有优势。

基于注解的编程模式
Annotation-based Programming Model

WebFlux中也支持 @Controller 编程模型和 Spring MVC 中使用的其他注解。主要区别在于底层核心框架协议(即 HandlerMapping HandlerAdapter )是非阻塞的,并且在响应型 ServerHttpRequest
ServerHttpResponse 上运行,而不是在 HttpServletRequestHttpServletResponse 上运行。以下是一个响应式 Controller 的例子:

@RestController
public class PersonController {

        private final PersonRepository repository;

        public PersonController(PersonRepository repository) {
                this.repository = repository;
        }

        @PostMapping("/person")
        Mono<Void> create(@RequestBody Publisher<Person> personStream) {
                return this.repository.save(personStream).then();
        }

        @GetMapping("/person")
        Flux<Person> list() {
                return this.repository.findAll();
        }

        @GetMapping("/person/{id}")
        Mono<Person> findById(@PathVariable String id) {
                return this.repository.findOne(id);
        }
}

函数式编程模式
Functional Programming Model

传入的HTTP请求由 HandlerFunction 处理,HandlerFunction 本质上是一个接收 ServerRequest 并返回 Mono<ServerResponse> 的函数。这里的处理方法所做的事相当于 @RequestMapping这个注解帮我做的事。

ServerRequestServerResponse 是不可变接口,而且都给JDK8提供了友好的方式来访问底层的HTTP信息。这两个都是基于Reactor实现的:请求将请求体暴露为 FluxMono; 响应接受任何 响应式的流Reactive Streams Publisher 作为主体。

ServerRequest 提供了访问HTTP各种元素的方法:method,URI,查询参数(query parameters),以及通过单独的ServerRequest.Headers 接口来访问headers。通过 body方法可以访问body。例如,这里把请求体提取为Mono <String>

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

这里是把请求体提取为 Flux,其中 Person 是可以从body内容反序列化的类(即如果body包含JSON,则由Jackson支持,或者如果是XML,则为JAXB)。

Flux<Person> people = request.bodyToFlux(Person.class);

上面的两个方法(bodyToMonobodyToFlux)实际上是使用通用ServerRequest.body(BodyExtractor)函数的快捷方法。
BodyExtractor 使用了策略模式,这允许您编写自己的提取逻辑。但在 BodyExtractors 实用程序类中可以找到常见的 BodyExtractor
实例。所以,上面的例子可以替换为:

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

类似地,ServerResponse 提供对HTTP响应的访问。由于它是不可变的,您可以使用构建器创建一个 ServerResponse 。构建器允许您设置响应状态,添加响应标题并提供正文。例如,这是如何使用200 OK状态创建响应,JSON内容类型和正文:

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

这里创建的一个状态是201,header和body都是空的响应:

URI location = ...
ServerResponse.created(location).build();

将这些组合在一起可以创建一个 HandlerFunction。例如,这里是一个简单的“Hello World”处理程序 lambda 的示例,它返回一个200状态的响应和一个包含String 的主体:

HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body(fromObject("Hello World"));

使用 lambda 写处理函数,就像我们上面所说的那样很方便,但是在处理多个函数时可能缺乏可读性,变得不那么容易维护。因此,建议将相关处理函数分组到一个处理程序或控制器类中。例如,这是一个暴露了一个响应式的 Person 存储库的类(做到功能拆分):

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

public class PersonHandler {

        private final PersonRepository repository;

        public PersonHandler(PersonRepository repository) {
                this.repository = repository;
        }

        // 1
        public Mono<ServerResponse> listPeople(ServerRequest request) { 
                Flux<Person> people = repository.allPeople();
                return ServerResponse.ok().contentType(APPLICATION_JSON).body(people, Person.class);
        }

        // 2
        public Mono<ServerResponse> createPerson(ServerRequest request) { 
                Mono<Person> person = request.bodyToMono(Person.class);
                return ServerResponse.ok().build(repository.savePerson(person));
        }

        // 3
        public Mono<ServerResponse> getPerson(ServerRequest request) { 
                int personId = Integer.valueOf(request.pathVariable("id"));
                Mono<ServerResponse> notFound = ServerResponse.notFound().build();
                Mono<Person> personMono = this.repository.getPerson(personId);
                return personMono
                                .flatMap(person -> ServerResponse.ok().contentType(APPLICATION_JSON).body(fromObject(person)))
                                .switchIfEmpty(notFound);
        }
}
  1. listPeople 是一个处理函数,它把从数据库中查询到所有 Person
    对象返回为JSON。

  2. createPerson 是一个处理函数,用于存储请求正文中包含的新Person。请注意,PersonRepository.savePerson(Person) 返回Mono<Void>:发出完成信号的空 Mono,当人从请求中读取并存储时,发出完成信号。因此,当接收到完成信号时,即当 Person 已被保存时,我们使用 build(Publisher<Void>) 方法来发送响应。

  3. getPerson 是一个处理函数,它通过路径变量id来标识一个人。我们通过数据库检索该Person,并返回JSON响应(如果找到)。如果没有找到,我们使用 switchIfEmpty(Mono<T>) 来返回 404 Not Found 响应。

路由函数
RouterFunctions

传入的请求将被路由函数路由到各个真正处理的方法里,主要就是要依赖 RouterFunction来路由。这个函数接收ServerRequest,并返回一个 Mono<HandlerFunction>。如果请求与特定路由匹配,则返回处理函数; 否则返回一个空的 MonoRouterFunction@Controller 类中的 @RequestMapping 注解类似。

通常,您不要自己编写路由器功能,而是使用
RouterFunctions.route(RequestPredicate, HandlerFunction)。如果路由的条件满足,请求将路由到给定的处理函数; 否则不执行路由,导致 404 Not Found 响应。虽然您可以编写自己的 RequestPredicate但是您没必要自己写RequestPredicates 这个帮助类已经提供了很多常用的判断谓语,例如,基于路径,HTTP方法,内容类型等进行匹配。使用route,我们可以路由到我们的 “Hello World” 处理函数:

RouterFunction<ServerResponse> helloWorldRoute =
        RouterFunctions.route(RequestPredicates.path("/hello-world"),
        request -> Response.ok().body(fromObject("Hello World")));

两个路由功能可以组成一个新的路由功能,新的路由到这两个路由原本可以路由到的处理函数(新的路由功能完全包含了组成的两个路由):如果第一个路由的判断不匹配,则用第二个来判断是否匹配。组合的路由器功能按顺序进行匹配,因此路由的顺序指定是有意义的。您可以通过调用 RouterFunction.and(RouterFunction) 或通过调用
RouterFunction.andRoute(RequestPredicate, HandlerFunction) 来组成两个路由功能,这是 RouterFunction.and()RouterFunctions.route() 的一种方便组合方法,这里给出个例子,一看便知为什么方便了:

return route(POST("/echo"), echoHandler::echo)
                .and(route(GET("/hello"), echoHandler::echo))
                .andRoute(GET("/hi"), echoHandler::echo);

现在我们要把请求路由到上面写的 PersonHandler中去:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> personRoute =
    route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
        .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
        .andRoute(POST("/person").and(contentType(APPLICATION_JSON)), handler::createPerson);

您还可以通过调用
RequestPredicate.and(RequestPredicate)
RequestPredicate.or(RequestPredicate) 来构成请求谓词。这些工作正如预期的那样:and需要两个判断都匹配才是匹配; or是任一判断匹配就算匹配。RequestPredicates 中可以看到大多数谓词都是组合的。例如,RequestPredicates.GET(String)
RequestPredicates.method(HttpMethod)RequestPredicates.path(String) 的组合。

启动服务器
Running a Server

现在只有一个问题了:如何在HTTP服务器中使用路由功能?您可以使用
RouterFunctions.toHttpHandler(RouterFunction) 将路由功能转换为HttpHandlerHttpHandler允许您运行各种响应场景:Reactor Netty,Servlet 3.1和Undertow。以下是在 Reactor Netty 中运行路由功能的方法,例如:

RouterFunction<ServerResponse> route = ...
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(adapter).block();

在Tomcat中 ,是这个样子:

RouterFunction<ServerResponse> route = ...
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
Context rootContext = server.addContext("", System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(rootContext, "servlet", servlet);
rootContext.addServletMapping("/", "servlet");
tomcatServer.start();

TODO:DispatcherHandler
这部分文档中列为todo,还没有相关文档。

过滤函数
HandlerFilterFunction

路由功能映射的路由可以通过调用
RouterFunction.filter(HandlerFilterFunction) 进行过滤,其中
HandlerFilterFunction 本质上是一个接收 ServerRequest
HandlerFunction 的函数,并返回一个 ServerResponse 。参数中的HandlerFilterFunction表示链中的下一个元素:一般来说就是路由到的 HandlerFunction(真正的逻辑处理函数) ,但是如果应用了多个过滤器,也可以是另一个 FilterFunction(另一个过滤器) 。这个和 @ControllerAdviceServletFilter 的功能很类似。现在让我们在我们的路由中添加一个简单的安全过滤器,假设我们有一个 SecurityManager 可以确定是否允许特定的路径:

import static org.springframework.http.HttpStatus.UNAUTHORIZED;

SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = ...

RouterFunction<ServerResponse> filteredRoute =
        route.filter(request, next) -> {
                if (securityManager.allowAccessTo(request.path())) {
                        return next.handle(request);
                }
                else {
                        return ServerResponse.status(UNAUTHORIZED).build();
                }
  });

在这个例子中可以看到,调用 next.handle(ServerRequest)是可选的:只有验证通过的,我们才会让请求走到下一步。

客户端

WebFlux 包括一个 函数式,响应式的客户端WebClient,它为 RestTemplate 提供了一种完全无阻塞和响应式的替代方案。
它将网络输入和输出封装成反应式的 ClientHttpRequestClientHttpResponse ,其中请求和响应的内容是 Flux <DataBuffer>而不是 InputStreamOutputStream。此外,它还支持与服务器端相同的响应式 JSON,XML和SSE 序列化机制,因此您可以使用类型化对象。以下是使用 WebClient的示例,WebClient需要有ClientHttpConnector来实现即插即用的一个特定的HTTP客户端:

WebClient client = WebClient.create("http://example.com");

Mono<Account> account = client.get()
                .url("/accounts/{id}", 1L)
                .accept(APPLICATION_JSON)
                .exchange(request)
                .flatMap(response -> response.bodyToMono(Account.class));

AsyncRestTemplate 还支持非阻塞交互。主要区别在于它不支持非阻塞流,例如 Twitter one ,因为它在本质上仍然依赖于 InputStream
OutputStream

请求体和响应体的转换

spring-core 模块提供了响应式 Encoder (编码器) 和 Decoder (解码器),使得Flux的字节和类型对象之间可以相互转换。spring-web 模块添加了 JSON(Jackson)和 XML(JAXB)实现,用于Web应用程序以及其他用于SSE流和零拷贝文件传输。

如果请求体是以下之一,那么不管是在注解模型还是函数式编程的模型中,这些请求体都会被自动解码:

  • Account account - 在调用控制器之前,account 将无阻塞地被反序列化。
  • Mono<Account> account - controller 可以使用 Mono 来声明在反序列化 account 后执行的逻辑。
  • Single<Account> account - 和 Mono 类似,但是用的是 RxJava
  • Flux<Account> accounts - 输入流场景
  • Observable<Account> accounts - RxJava 的 输入流场景

响应体可以是以下之一:

  • Mono<Account> - 当 Mono 完成时,非堵塞的序列化然后返回这个Account。
  • Single<Account> - 与上类似,但是使用的 RxJava
  • Flux<Account> - 流式场景,可能是SSE,具体取决于所请求的内容类型。
  • Observable<Account> - 与上类似, 但是使用的 RxJava Observable 类型
  • Flowable<Account> - 与上类似, 但是使用的 RxJava 2 Flowable 类型。
  • Flux<ServerSentEvent> - SSE 流。
  • Mono<Void> - 当 Mono 完成时,请求处理完成。
  • Account - 异步非堵塞的序列化然后返回。
  • void - 异步非堵塞的序列化然后返回。

当使用像 FluxObservable 这样的流类型时,请求/响应或映射/路由级别中指定的媒体类型用于确定数据应如何序列化和刷新。例如,返回 Flux<User> 的REST端点将默认序列化如下:

  • application/json : Flux<User> 作为异步集合处理,并在完成事件发布时将其序列化为JSON数组。
  • application/stream+json : 一个 Flux<User>会被处理成一个User元素集合的一个流,元素之间用新的一行分隔开。WebClient 支持JSON流解码,因此这对于server to server的用例来说是一个很好的用例(也就是服务端相互调用的时候,最好用这种)。
  • text/event-stream : 一个 Flux<User>Flux<ServerSentEvent<User >> 会被处理成一个User或者ServerSentEvent元素集合的一个流,使用JSON编码。这非常适合将流暴露给浏览器客户端。WebClient 也支持读取SSE流。

2.4 响应式 Websocket 支持

WebFlux 包括响应式 WebSocket 客户端和服务端。而且,Java WebSocket API(JSR-356),Jetty,Undertow和Reactor Netty都支持这个客户端和服务端。

在服务器端,声明一个 WebSocketHandlerAdapter,然后用WebSocketHandler映射短点:

@Bean
public HandlerMapping webSocketMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/foo", new FooWebSocketHandler());
        map.put("/bar", new BarWebSocketHandler());

        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(10);
        mapping.setUrlMap(map);
        return mapping;
}

@Bean
public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
}

在客户端,为上面列出的支持的库之一创建一个 WebSocketClient

WebSocketClient client = new ReactorNettyWebSocketClient();
client.execute("ws://localhost:8080/echo"), session -> {... }).blockMillis(5000);

2.5 测试

spring-test 模块包括一个 WebTestClient,可用于测试WebFlux服务,不管他在不在运行。

没有运行服务器的测试和Spring MVC的 MockMvc 差不多,其使用模拟请求和响应,而不是使用套接字通过网络连接。当然,WebTestClient 也可以针对正在运行的服务器执行测试。

想了解框架的更多测试例子,可以参考sample tests

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值