SpringWebFlux虽然并非基于JavaEE的Servlet API,但它在设计上仍保留了原来SpringMVC的使用方式,我们可以使用@Controller
或@RestController
定义控制器组件,框架会自动通过反射找到这些控制器并用其处理HTTP请求。对于和SpringMVC相同的部分我们这里不会再重复介绍,这篇笔记我们主要了解SpringWebFlux中使用Controller模式编写代码处理HTTP请求时和SpringMVC之间的不同点。
SpringWebFlux中,我们的代码必须整个都是Reactive的,这也意味着,它和SpringMVC最大的不同点就是Controller返回值的类型也一定是Flux或Mono。
package com.gacfox.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
public Mono<String> hello() {
return Mono.just("Hello, world!");
}
}
代码中,我们使用@RestController
声明这是一个控制器服务端点,它返回Rest响应,@RequestMapping
声明该控制器处理/demo
路径,@GetMapping
声明了一个控制器方法,它处理/hello
路径,因此启动服务后我们访问/demo/hello
即可查看效果,到此为止其使用方法和SpringMVC完全一致。不一样的地方是方法体内,我们使用just()
方法创建了一个简单的Mono类型,它包含Hello, world!
字符串信息。
对于POST请求,如果我们需要客户端传入一些内容,我们也需要使用Flux或Mono类型接收,下面是一个例子。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ApiResult;
import com.gacfox.demo.model.StudentDto;
import com.gacfox.demo.service.StudentService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private StudentService studentService;
@PostMapping("/saveStudent")
public Mono<ApiResult<?>> saveStudent(@RequestBody Mono<StudentDto> studentDtoMono) {
return studentDtoMono
.flatMap(studentDto -> studentService.saveStudent(studentDto))
.then(Mono.just(ApiResult.success()));
}
}
代码中,我们的控制器方法接收Mono<StudentDto>
类型的参数,这意味着框架对数据进行反序列化时也会异步的进行。SpringWebFlux控制器方法也支持直接传入非Mono或Flux的DTO类,但注意这样反序列化阶段就会同步阻塞式的进行,因此适用于较小的单个的请求体数据。
此外,由于SpringWebFlux中所有代码都是Reactive的,我们这里的StudentService
中saveStudent()
方法也是Reactive的,它接收StudentDto
类型参数,操作完成后返回Mono<Void>
,这也是比较常见的Reactive写法。
public Mono<Void> saveStudent(StudentDto studentDto) { }
Controller的响应式流程中,我们使用了flatmap
和then
操作符,先调用saveStudent()
方法保存数据,然后返回我们自定义的通用响应体ApiResult.success()
,向客户端输出成功信息。
对于Controller,额外我们还需要了解,如果在SpringWebFlux中不使用Mono和Flux会发生什么?这样做其实SpringWebFlux并不会直接报错,框架支持这种写法,但这并不是推荐的方式!如果Controller方法接收非Mono或Flux请求,返回非Mono或Flux响应,本质上SpringWebFlux就会以同步的方式处理这些数据,但这些同步代码会阻塞线程,这与使用SpringWebFlux的初衷是相反的,在高并发环境下这种代码可能造成服务端性能的急剧下降!此外,这类代码也与整个SpringWebFlux的Reactive生态格格不入,给后续维护造成极大困难。
SpringWebFlux支持同步写法的目的仅在于需要兼容部分旧有传统代码的场景,千万不要滥用它们。
在传统的SpringMVC中我们可以通过访问HttpServletRequest
读取请求上下文,在SpringWebFlux中由于框架并非基于JavaEE Servlet API,因此没有这个对象,在这里我们需要使用的是ServerHttpRequest
。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
public Mono<ApiResult<?>> hello(ServerWebExchange exchange) {
// 获取Header
HttpHeaders headers = exchange.getRequest().getHeaders();
headers.keySet().forEach(key -> log.info("header: {}, value: {}", key, headers.getFirst(key)));
// 获取Cookie
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
cookies.keySet().forEach(key -> log.info("cookie: {}, value: {}", key, cookies.getFirst(key)));
return Mono.just(ApiResult.success());
}
}
代码中我们首先注入了ServerWebExchange
对象,它是SpringWebFlux框架中的核心接口,用于在应用中表示服务器端HTTP请求和响应的上下文。exchange.getRequest()
方法能够获取到SpringWebFlux的ServerHttpRequest
对象,其中包含了大量的请求上下文信息,我们可以从其中取出Header或Cookie键值对。
此外SpringWebFlux其实也可以直接注入ServerHttpRequest
对象,如果我们只操作请求上下文而不用处理响应信息,像下面这样写也是可以的。
@GetMapping("/hello")
public Mono<ApiResult<?>> hello(ServerHttpRequest serverHttpRequest) { }
和获取请求上下文类似,处理响应信息时我们需要通过ServerHttpResponse
对象实现,它包含了设置响应状态码、响应头、响应Cookie等方法。下面例子代码中我们使用ServerHttpResponse
设置了响应头信息。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
public Mono<ApiResult<?>> hello(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return Mono.just(ApiResult.failure("You are not allowed to access!"));
}
}
Controller控制器中,设置响应信息另一种更好的方式是使用ResponseEntity
。统一使用ResponseEntity
封装状态码等信息和响应体,语义更清晰,代码可读性更好。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
public Mono<ResponseEntity<ApiResult<?>>> hello() {
return Mono.just(ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(ApiResult.failure("You are not allowed to access!"))
);
}
}
前面我们提到过,SpringWebFlux的主要用途就是开发那些高并发、长连接、实时性强的服务,输出SSE流式响应就是一个非常常见的使用场景。下面例子中,我们输出了一组Flux类型的SSE响应信息到客户端。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ChatEventResponseData;
import com.gacfox.demo.service.ChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import javax.annotation.Resource;
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private ChatService chatService;
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<ChatEventResponseData>> chat() {
return chatService.chat()
.map(data -> ServerSentEvent.<ChatEventResponseData>builder()
.event("message")
.data(data)
.build());
}
}
代码中,我们的ChatService
能够返回Flux类型数据流,Controller内我们指定HTTP响应类型是MediaType.TEXT_EVENT_STREAM_VALUE
,SpringWebFlux框架会自动处理Flux,每当数据流中有数据元素生成时,框架都会将其推送给客户端,这样客户端就能实时接收服务端返回的消息,而不必等待整个响应全部生成后再处理。SSE虽然可以使用任何文本类型,但一般还是遵循W3C SSE规范,SpringWeb提供了ServerSentEvent
封装SSE响应,因此我们没必要手动拼接SSE响应格式字符串,控制器方法返回Flux<ServerSentEvent<>>
类型即可。如果你对HTML5中的SSE不熟悉,可以参考Web前端/HTML5/SSE服务端推送
章节。
总体上来看,这显然是一个长连接服务,但和SpringMVC相比,我们是采用异步非阻塞的方式开发的,在这里SpringWebFlux能够以更少的服务器资源支持更大的并发连接数。
SpringMVC中我们经常使用ControllerAdvice定义控制器的切面逻辑,比如十分常用的通用异常处理机制,SpringWebFlux同样支持ControllerAdvice,我们使用@ControllerAdvice
或@RestControllerAdvice
定义,但它的返回值通常应当是Mono或Flux。
package com.gacfox.demo.controller;
import com.gacfox.demo.model.ApiResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import reactor.core.publisher.Mono;
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(Exception.class)
public Mono<ApiResult<?>> handleException(Exception e) {
return Mono.just(ApiResult.failure(e.getMessage()));
}
}
代码中,如果我们的其它控制器调用流程中抛出异常,ControllerAdvice对应的处理方法将执行,并返回一个通用的异常响应。