WebFlux
WebFlux服务端开发
- 需求:通过 WebFlux 实现对 MongoDB 的 CRUD 操作。
使用传统处理器开发
- 使用传统处理器开发,指的是使用 @Controller 注解的类作为处理器类,使用 @RequestMapping 进行请求与处理器方法映射,来开发 WebFlux 服务端的开发方式。
演示工程 05-webflux-ordinary
- 创建一个 Spring Boot 工程,命名为 05-webflux-ordinary,添加依赖:
<!-- mongodb-reactive依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
- 定义启动类:
@EnableReactiveMongoRepositories // 开启MongoDB的Spring-data-jpa
@SpringBootApplication
public class WebFluxOrdinaryApplication {
public static void main(String[] args) {
SpringApplication.run(WebFluxOrdinaryApplication.class, args);
}
}
- 定义 dao 层的实体类:MongoDB 中表的 id 一般为 String 类型。@Document 与@Id 均为 spring data jpa 中的注解,可以完成自动建表并指定表的主键。
@Data
// 指定在MongoDB中生成的表
@Document(collection = "t_student")
public class Student {
@Id
private String id; // MongoDB表中的id一般为String类型
@NotBlank(message = "姓名不能为空")
private String name;
@Range(min = 10, max = 80, message = "年龄必须在{min}-{max}范围内")
private int age;
}
- 定义 dao 层的Repository接口:第一个泛型为该 JPA 操作的实体类,第二个泛型为实体类的主键类型。
@Repository
public interface StudentRepository extends ReactiveMongoRepository<Student, String> {
}
- 定义处理器:
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentRepository repository;
// 一次性返回数据
@GetMapping("/all")
public Flux<Student> getAll() {
return repository.findAll();
}
// 以SSE形式实时性返回数据
@GetMapping(value = "/sse/all", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Student> getAllSse() {
return repository.findAll();
}
}
- 数据库连接配置:
spring:
data:
mongodb:
uri: mongodb://192.168.254.128:27017/webflux
CRUD实现
- 添加数据
@PostMapping("/save")
public Mono<Student> saveStudent(@RequestBody Student student) {
return repository.save(student);
}
- 无状态数据删除:所谓无状态删除,即指定的要删除的对象无论是否存在,其响应码均为 200,我们无法知道是否真正删除了数据。
@DeleteMapping("/delcomm/{id}")
public Mono<Void> deleteStudent(@PathVariable("id") String id){
return repository.deleteById(id);
}
- 有状态数据删除:所谓有状态删除,即指若删除的对象存在,且删除成功,则返回响应码 200,否则返回响应码 404。通过响应码就可以判断删除操作是否成功。
/**
* 需求:若删除的对象存在,且删除成功,则返回响应码200,否则返回响应码404
*
* Mono<ResponseEntity<Void>>表示方法返回值为Mono序列
* 其包含的元素为ResponseEntity对象,该对象中仅为包含响应状态码
*
* map()与flatMap()均可做映射,但这两个方法与Stream编程中的两个同名方法没有任何关系
* map():同步方法
* flatMap():异步方法
* 一般选择的标准是:若映射的内容中包含有耗时方法,则选择flatMap(),否则选择map()
*/
@DeleteMapping("/delstat/{id}")
public Mono<ResponseEntity<Void>> deleteStatStudent(@PathVariable("id") String id) {
return repository.findById(id)
.flatMap(stu -> repository.delete(stu)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK))))
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
- 修改数据:对于执行修改操作的处理器方法,我们可以这样定义其返回值:若修改成功,则返回修改后的对象数据;若指定的 id 对象不存在,则返回 404。
/**
* 需求:若修改成功,则返回修改后的对象数据及200;若指定的id对象不存在,则返回404
*/
@PutMapping("/update/${id}")
public Mono<ResponseEntity<Student>> updateStudent(@PathVariable("id") String id,
@RequestBody Student student) {
return repository.findById(id)
.flatMap(stu -> {
stu.setName(student.getName());
stu.setAge(student.getAge());
return repository.save(stu);
})
.map(stu -> new ResponseEntity<Student>(stu, HttpStatus.OK))
.defaultIfEmpty(new ResponseEntity<Student>(HttpStatus.NOT_FOUND));
}
- 根据id查询:对于执行根据 id 进行查询操作的处理器方法,我们可以这样定义其返回值,若有查询结果,则返回查询到的对象数据;若没有查询结果,则返回 404。
@GetMapping("/find/{id}")
public Mono<ResponseEntity<Student>> findStudentById(@PathVariable("id") String id) {
return repository.findById(id)
.map(stu -> new ResponseEntity<Student>(stu, HttpStatus.OK))
.defaultIfEmpty(new ResponseEntity<Student>(HttpStatus.NOT_FOUND));
}
- 根据年龄上下限查询:在 StudentRepository接口中添加一个抽象方法
/**
* 根据年龄查询
* @param below 年龄下限(不包含)
* @param top 年龄上限(不包含)
* @return
*/
Flux<Student> findByAgeBetween(int below, int top);
/**
*根据年龄查询(普通返回)
*/
@GetMapping("/age/{below}/{up}")
public Flux<Student> findByAgeHandle(@PathVariable("below") int below,
@PathVariable("up") int up) {
return repository.findByAgeBetween(below, up);
}
@GetMapping(value = "/sse/age/{below}/{up}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Student> findByAgeSSEHandle(@PathVariable("below") int below,
@PathVariable("up") int up) {
return repository.findByAgeBetween(below, up);
}
- 使用 MongoDB 的原始查询语句:在 StudentRepository 接口中添加一个抽象方法
/**
* 使用Mongo原生查询
*/
@Query("{'age':{'$gt':?0, '$lte':?1}}")
Flux<Student> findByAge(int below, int top);
/**
* 根据年龄查询(普通返回)
*/
@GetMapping("/find/age/{below}/{up}")
public Flux<Student> findByAgeHandle2(@PathVariable("below") int below, @PathVariable("up") int up) {
return repository.findByAge(below, up);
}
@GetMapping(value = "/sse/find/age/{below}/{up}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Student> findByAgeSSEHandle2(@PathVariable("below") int below, @PathVariable("up") int up) {
return repository.findByAge(below, up);
}
参数校验
- 为了保证数据在进入到业务运算时的正确性,我们会对参数首先进行校验。一般情况下,我们可以直接使用 Hibernate Validator 中已经定义好的校验注解,若不能满足需求,也可以自定义校验逻辑。
使用Hibernate注解校验
- Hibernate Validator 中已经定义好了很多通用的校验注解,我们可以直接使用。
注解方式
- 在要验证的属性上添加相应注解,即验证规则。
- 处理器:
AOP
@ControllerAdvice // 处理器通知切面(连接点为处理器方法)
public class ParamValidAdvice {
@ExceptionHandler
public ResponseEntity<String> xxx(WebExchangeBindException ex) {
return new ResponseEntity<>(getExceptionMsg(ex), HttpStatus.BAD_REQUEST);
}
private String getExceptionMsg(WebExchangeBindException ex) {
return ex.getFieldErrors()
.stream()
.map(e -> e.getField() + " : " + e.getDefaultMessage())
.reduce("", (s1, s2) -> s1 + "\n" + s2);
}
}
常用Hibernate校验注解
每一个 Hibernate 校验注解均有一个 message 属性,用于设置验证失败后的提示信息。
注解 | 适用的数据类型 | 说明 |
---|---|---|
@AssertFalse | Boolean, boolean | 验证注解的元素值是false |
@AssertTrue | Boolean, boolean | 验证注解的元素值是true |
@DecimalMax(value = x) | BigDecimal, BigInteger, String, byte,short, int, long 以及基本类型的各自包装类型。由hv额外支持:任何子类型的数字和字符序列。 | 验证注解的元素值小于等于@ DecimalMax指定的 value 值 |
@DecimalMin(value = x) | BigDecimal, BigInteger, String, byte,short, int, long 以及基本类型的各自包装类型。由hv额外支持:任何子类型的数字和字符序列。 | 验证注解的元素值小于等于@ DecimalMin指定的 value 值 |
@Digits(integer=整数位数, fraction=小数位数) | BigDecimal, BigInteger, String, byte,short, int, long 以及基本类型的各自包装类型。由hv额外支持:任何子类型的数字和字符序列。 | 验证注解的元素值的整数位数和小数位数上限 |
@Future | java.util.Date, java.util.Calendar; Additionally supported by HV, if theJoda Time date/time API is on the class path: any implementations ofReadablePartial andReadableInstant. | 验证注解的元素值(日期类型)比当前时间晚 |
@Max(value=x) | BigDecimal, BigInteger, byte, short,int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type ofCharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number. | 验证注解的元素值小于等于@Max指定的value值 |
@Min(value=x) | BigDecimal, BigInteger, byte, short,int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type ofCharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number. | 验证注解的元素值大于等于@Min指定的value值 |
@NotNull | Any type | 验证注解的元素值不是 null |
@Null | Any type | 验证注解的元素值是null |
@Past | java.util.Date, java.util.Calendar; Additionally supported by HV, if theJoda Time date/time API is on the class path: any implementations ofReadablePartial andReadableInstant. | 验证注解的元素值(日期类型)比当前时间早 |
@Pattern(regex=正则表达式, flag=) | String. Additionally supported by HV: any sub-type of CharSequence. | 验证注解的元素值与指定的正则表达式匹配 |
@Size(min=最小值, max=最大值) | String, Collection, Map and arrays. Additionally supported by HV: any sub-type of CharSequence. | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Valid | Any non-primitive type(引用类型) | 验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象 |
@NotEmpty | CharSequence, Collection, Map and Arrays | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | CharSequence, Collection, Map and Arrays,BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types | 验证注解的元素值在最小值和最大值之间 |
@NotBlank | CharSequence | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@Length(min=下限, max=上限) | CharSequence | 验证注解的元素值长度在min和max区间内 |
CharSequence | 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
自定义校验逻辑
- 对于有些情况,我们使用注解无法满足校验需求,例如:我们需要使 Student 的 name 值不能是 admin 或 administrator,此时,可自己定义校验逻辑。
- 定义异常类:
@Data
public class StudentException extends RuntimeException {
private String field;
private String value;
public StudentException() {
super();
}
public StudentException(String field, String value, String message) {
super(message);
this.field = field;
this.value = value;
}
}
- 定义校验工具类:
public class ValidateUtil {
// 指定无效姓名列表
private static final String[] INVALID_NAME = {"admin", "administrator"};
// 对姓名进行校验
public static void validName(String name) {
Stream.of(INVALID_NAME)
// 对比的值为true,则通过过滤,该值将继续保留在流中
.filter(name::equalsIgnoreCase)
.findAny() // 返回Optional
.ifPresent(inName -> {
throw new StudentException("name", name, "使用了非法姓名");
});
}
}
- 修改检验切面:在校验切面类中添加如下异常处理方法
@ControllerAdvice // 处理器通知切面(连接点为处理器方法)
public class ParamValidAdvice {
@ExceptionHandler
public ResponseEntity<String> validateHandle(StudentException ex) {
// 获取异常对象中的数据
String message = ex.getMessage();
String fn = ex.getField();
String fv = ex.getValue();
String msg = fn + " : " + fv + " : " + message;
return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}
// ...
}
- 修改处理器:修改具有 Student 类型参数的处理器方法,在其中添加校验代码。
使用Router Functions开发
- 使用 Router Functions 开发,指的是使用由 @Component 注解的普通类作为处理器类,使用 Router 进行请求与处理器方法映射,来开发 WebFlux 服务端的开发方式。
演示工程 06-webflux-router
- 复制 05-webflux-ordinary 工程,并重命名为 06-webflux-router。删除其中的 StudentController 类及 ParamValidateAdvice 类,其它代码保留。
- 定义路由器:
@Configuration
public class StudentRouter {
@Bean
RouterFunction<ServerResponse> customRouter(StudentHandler handler) {
return RouterFunctions.nest(
RequestPredicates.path("/student"),
RouterFunctions.route(RequestPredicates.GET("/all"), handler::findAllHandler)
);
}
}
- 定义处理器:这里的处理器并不是之前使用 @Controller 注解的处理器类,而是一个使用 @Component 注解的普通类。通过对前面路由器中 route() 方法的第二个参数分析可知,该处理器就是一个 HandlerFunction 类。
@Component
public class StudentHandler {
@Autowired
private StudentRepository repository;
// 查询所有
public Mono<ServerResponse> findAllHandler(ServerRequest request) {
return ServerResponse
// 指定响应码200
.ok()
// 指定请求体中的内容类型为UTF8编码的JSON数据
.contentType(MediaType.APPLICATION_JSON_UTF8)
// 构建响应体
.body(repository.findAll(), Student.class);
}
}
CRUD实现
添加数据
- 添加路由规则:
- 修改处理器:在处理器中添加如下处理器方法
// 添加数据
public Mono<ServerResponse> saveHandler(ServerRequest request) {
Mono<Student> studentMono = request.bodyToMono(Student.class);
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(repository.saveAll(studentMono), Student.class);
}
有状态删除
- 添加路由规则:
- 修改处理器:
// 有状态删除
public Mono<ServerResponse> deleteHandler(ServerRequest request) {
String id = request.pathVariable("id");
return repository.findById(id)
.flatMap(stu -> repository.delete(stu).then(ServerResponse.ok().build()))
.switchIfEmpty(ServerResponse.notFound().build());
}
修改数据
- 添加路由规则:
- 修改处理器:这里实现的逻辑是,若指定的 id 对象不存在,则指定 id 作为新的 id 完成插入;否则完成修改。
// 修改数据
public Mono<ServerResponse> updateHandler(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Student> studentMono = request.bodyToMono(Student.class);
return studentMono.flatMap(stu -> {
stu.setId(id);
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(repository.save(stu), Student.class);
});
}
参数校验
- 由于这里的处理器方法只有 ServerRequest 一个参数,所以无法使用注解方式的参数校验,即无法使用 Hibernate Validator。但可以使用自定义的参数校验。
- 修改实体类:去掉实体类中的 Hibernate Validator 注解。
@Data
// 指定在MongoDB中生成的表
@Document(collection = "t_student")
public class Student {
@Id
private String id;
private String name;
private int age;
}
- 修改处理器:对添加数据和修改数据的验证
// 添加数据(带校验)
public Mono<ServerResponse> saveValideHandler(ServerRequest request) {
Mono<Student> studentMono = request.bodyToMono(Student.class);
return studentMono.flatMap(stu -> {
// 验证姓名
ValidateUtil.validName(stu.getName());
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(repository.saveAll(studentMono), Student.class);
});
}
// 修改数据(带校验)
public Mono<ServerResponse> updateValidHandler(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Student> studentMono = request.bodyToMono(Student.class);
return studentMono.flatMap(stu -> {
// 验证姓名
ValidateUtil.validName(stu.getName());
stu.setId(id);
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(repository.save(stu), Student.class);
});
}
- 添加路由规则:
- 定义异常处理器:异常处理器指的是当发生异常时,其会捕获到异常,并对异常信息进行处理。
/**
* 自定义异常处理器:当异常发生时返回400,并返回异常信息。
*/
@Component
@Order(-99)
public class CustomExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD_REQUEST);
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
String message = this.formatExceptionMassage(ex);
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(buffer));
}
private String formatExceptionMassage(Throwable ex) {
String msg = "发生异常:" + ex.getMessage();
if (ex instanceof StudentException) {
StudentException e = (StudentException) ex;
msg = msg + "【" + e.getField() + ":" + e.getValue() + "】";
}
return msg;
}
}
WebFlux客户端开发
- 前面我们通过两种方式开发了 WebFlux 服务器端,下面我们来开发 WebFlux 客户端,消费 WebFlux 服务端提供的服务。WebFlux 官方推荐我们使用 WebClient 客户端,其是随 WebFlux一起推出的。
- 创建一个 spring boot 工程,并命名为 07-webflux-client。客户端端口号为8081,调用前面的服务端工程的接口。
- 定义处理器类:
@RestController
public class StudentController {
/**
* 创建WebClient客户端,其参数baseUrl用于与下面处理器方法中的uri进行拼接
* 向服务端提交请求
*/
private WebClient client = WebClient.create("http://localhost:8080/student");
@PostMapping("/save")
public String saveStudentHandle(@RequestBody Student student) {
Mono<Student> studentMono = client.post()
.uri("/save")
.body(Mono.just(student), Student.class)
.retrieve() // 提交请求
.bodyToMono(Student.class); // 接收服务器的响应
// 输出每个Mono中的元素
studentMono.subscribe(System.out::println);
return "插入完毕";
}
@DeleteMapping("/del/{id}")
public String deleteStudentHandle(@PathVariable("id") String id) {
Mono<Void> voidMono = client.delete()
.uri("/delstat/{id}", id)
.retrieve()
.bodyToMono(Void.class);
// voidMono.subscribe();
return "删除完毕";
}
@PutMapping("/update/{id}")
public String updateStudentHandle(@PathVariable("id") String id, @RequestBody Student student) {
Mono<ResponseEntity> mono = client.put()
.uri("/update/{id}", id)
.body(Mono.just(student), Student.class)
.retrieve()
.bodyToMono(ResponseEntity.class);
// mono.subscribe();
return "修改完毕";
}
@GetMapping("/list")
public Flux<Student> listAllHandle() {
Flux<Student> studentFlux = client.get()
.uri("/all")
.retrieve()
.bodyToFlux(Student.class);
studentFlux.subscribe(System.out::println);
return studentFlux;
}
@GetMapping("/get/{id}")
public Mono<Student> getStudentHandle(@PathVariable("id") String id) {
Mono<Student> studentMono = client.get()
.uri("/find/{id}", id)
.retrieve()
.bodyToMono(Student.class);
studentMono.subscribe(System.out::println);
return studentMono;
}
@PutMapping("/modify/{id}")
public Flux<Student> modifyHandler(@PathVariable("id") String id, @RequestBody Student student) {
Flux<Student> studentFlux = client.put()
.uri("/update/{id}", id)
.body(Mono.just(student), Student.class)
.retrieve()
.bodyToFlux(Student.class);
studentFlux.subscribe(System.out::println);
return studentFlux;
}
}