JavaEE 企业级分布式高级架构师(十四)ReactiveStream编程WebFlux(3)

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 属性,用于设置验证失败后的提示信息。

注解适用的数据类型说明
@AssertFalseBoolean, boolean验证注解的元素值是false
@AssertTrueBoolean, 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额外支持:任何子类型的数字和字符序列。验证注解的元素值的整数位数和小数位数上限
@Futurejava.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值
@NotNullAny type验证注解的元素值不是 null
@NullAny type验证注解的元素值是null
@Pastjava.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(包含)指定区间之内,如字符长度、集合大小
@ValidAny non-primitive type(引用类型)验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象
@NotEmptyCharSequence, 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验证注解的元素值在最小值和最大值之间
@NotBlankCharSequence验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Length(min=下限, max=上限)CharSequence验证注解的元素值长度在min和max区间内
@EmailCharSequence验证注解的元素值是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;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值