本篇文章将继续按照上篇文章基于Spring Webflux 搭建基本代码框架1,完成RouterFunction模式的改造方式。
一、主要改造点
在传统Controller模式下的代码基础上进一步做如下改造:
1、取消Controller入口层,相关入口路径定义转换为router层;
2、取消service业务逻辑层,相关业务逻辑统一收归到handler层,且入参统一为ServerRequest,出参统一为ServerResponse;
3、参数校验等逻辑需要单独自行代码实现;
4、全局异常处理逻辑调整为通过WebExceptionHandler实现;
改造后的代码结构如下:
二、代码实现
(一)Controller入口层改造为router
基于函数式编程模式,应用所有入口路径定义全部在AppRouter配置类中进行定义:
import com.abinge.boot.staging.handler.StudentHandler;
import com.abinge.boot.staging.handler.TeacherHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.*;
@Configuration
public class AppRouter {
@Bean
RouterFunction<ServerResponse> studentRouter(StudentHandler studentHandler){
// 此处就相当于原来StudentController上面的@RequestMapping("/student")
return nest(path("/student"),
// 下面就相当于是原来StudentController类中的各个方法上的@RequestMapping("/add")
route(POST("/add").and(accept(MediaType.APPLICATION_JSON)), studentHandler::add)
.andRoute(POST("/update").and(accept(MediaType.APPLICATION_JSON)), studentHandler::add)
.andRoute(POST("/delete/{id}"), studentHandler::delete)
.andRoute(GET("/queryById/{id}"), studentHandler::queryById)
.andRoute(GET("/queryAll"), studentHandler::queryAll)
);
}
@Bean
RouterFunction<ServerResponse> teacherRouter(TeacherHandler teacherHandler){
// 此处就相当于原来TeacherController上面的@RequestMapping("/teacher")
return nest(path("/teacher"),
// 下面就相当于是原来TeacherController类中的各个方法上的@RequestMapping("/add")
route(POST("/add").and(accept(MediaType.APPLICATION_JSON)), teacherHandler::add)
.andRoute(POST("/update").and(accept(MediaType.APPLICATION_JSON)), teacherHandler::add)
.andRoute(POST("/delete").and(accept(MediaType.APPLICATION_JSON)), teacherHandler::delete)
.andRoute(POST("/queryById").and(accept(MediaType.APPLICATION_JSON)), teacherHandler::queryById)
.andRoute(GET("/queryAll"), teacherHandler::queryAll)
);
}
}
(二)service业务逻辑层改造为handler
原有service业务逻辑层的代码以及原有controller层的统一响应数据格式转换逻辑全部调整到handler中进行处理;
handler的特点是入参统一为ServerRequest,出参统一为ServerResponse。
- StudentHandler:
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Student;
import com.abinge.boot.staging.repository.StudentRepository;
import com.abinge.boot.staging.util.ParamCheckUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.ServerResponse.*;
@Component
public class StudentHandler {
@Autowired
private StudentRepository studentRepository;
public Mono<ServerResponse> add(ServerRequest serverRequest) {
Mono<Student> student = serverRequest.bodyToMono(Student.class);
return student.flatMap(stu -> {
// 自定义的参数校验
ParamCheckUtil.checkSaveStudent(stu);
Mono<Result> resultMono = studentRepository.saveAll(student)
.collectList().flatMap(stuList -> Mono.just(Result.success(stuList.get(0))));
return ok().contentType(MediaType.APPLICATION_JSON)
.body(resultMono, Result.class);
});
}
public Mono<ServerResponse> delete(ServerRequest serverRequest) {
// 1. 获取路径中的id
String idStr = serverRequest.pathVariable("id");
Long id = Long.valueOf(idStr);
// 2. 首先查询出来在数据库中的数据
return studentRepository.findById(id)
// 2.1 数据存在,则执行后续删除操作
.flatMap(inDb ->
studentRepository.deleteById(id)
// 删除成功,则将删除前的数据转换为统一响应数据结构返回
.then(ok().contentType(MediaType.APPLICATION_JSON).body(Mono.just(Result.success(inDb)),Result.class)))
// 2.2 数据不存在,则直接返回统一响应数据错误格式返回
.switchIfEmpty(ok().contentType(MediaType.APPLICATION_JSON).body(Mono.just(Result.fail("未找到对应的数据")), Result.class));
}
public Mono<ServerResponse> queryById(ServerRequest serverRequest) {
String id = serverRequest.pathVariable("id");
return ok().contentType(MediaType.APPLICATION_JSON)
// 执行成功后,转换为统一响应数据结构返回
.body(studentRepository.findById(Mono.just(Long.valueOf(id)))
.flatMap(stu -> Mono.just(Result.success(stu))), Result.class);
}
public Mono<ServerResponse> queryAll(ServerRequest serverRequest) {
return ok().contentType(MediaType.APPLICATION_JSON)
// 执行成功后,转换为统一响应数据结构返回
.body(studentRepository.findAll()
.collectList().flatMap(stuList -> Mono.just(Result.success(stuList))), Result.class);
}
}
- TeacherHandler:
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Teacher;
import com.abinge.boot.staging.repository.TeacherRepository;
import com.abinge.boot.staging.util.ParamCheckUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
@Component
public class TeacherHandler {
@Autowired
private TeacherRepository TeacherRepository;
public Mono<ServerResponse> add(ServerRequest serverRequest) {
Mono<Teacher> Teacher = serverRequest.bodyToMono(Teacher.class);
return Teacher.flatMap(tea -> {
// 参数校验
ParamCheckUtil.checkSaveTeacher(tea);
Mono<Result> resultMono = TeacherRepository.saveAll(Teacher)
.collectList().flatMap(teaList -> Mono.just(Result.success(teaList.get(0))));
return ok().contentType(MediaType.APPLICATION_JSON)
.body(resultMono, Result.class);
});
}
public Mono<ServerResponse> delete(ServerRequest serverRequest) {
String idStr = serverRequest.pathVariable("id");
Long id = Long.valueOf(idStr);
return TeacherRepository.findById(id)
.flatMap(inDb ->
TeacherRepository.deleteById(id)
.then(ok().contentType(MediaType.APPLICATION_JSON).body(Mono.just(Result.success(inDb)),Result.class)))
.switchIfEmpty(ok().contentType(MediaType.APPLICATION_JSON).body(Mono.just(Result.fail("未找到对应的数据")), Result.class));
}
public Mono<ServerResponse> queryById(ServerRequest serverRequest) {
String id = serverRequest.pathVariable("id");
return ok().contentType(MediaType.APPLICATION_JSON)
.body(TeacherRepository.findById(Mono.just(Long.valueOf(id)))
.flatMap(tea -> Mono.just(Result.success(tea))), Result.class);
}
public Mono<ServerResponse> queryAll(ServerRequest serverRequest) {
return ok().contentType(MediaType.APPLICATION_JSON)
.body(TeacherRepository.findAll()
.collectList().flatMap(teaList -> Mono.just(Result.success(teaList))), Result.class);
}
}
(三)参数校验自行代码实现
spring webflux的RouterFunction模式下暂时无法通过@Valid注解方式在入口处进行校验,故对参数校验需要自行实现,并嵌入到业务代码逻辑中:
import com.abinge.boot.staging.exceptions.BizException;
import com.abinge.boot.staging.model.Student;
import com.abinge.boot.staging.model.Teacher;
import org.apache.commons.lang3.StringUtils;
public class ParamCheckUtil {
public static void checkSaveStudent(Student student){
if (StringUtils.isBlank(student.getName())){
throw new BizException("name can not be blank");
}
if (StringUtils.isBlank(student.getAge())){
throw new BizException("age can not be blank");
}
}
public static void checkSaveTeacher(Teacher teacher) {
if (StringUtils.isBlank(teacher.getName())){
throw new BizException("name can not be blank");
}
}
}
(四)全局异常处理通过WebExceptionHandler实现
原有通过org.springframework.web.bind.annotation.RestControllerAdvice注解来实现的全局异常切面处理,在spring webflux的RouterFunction模式下已无法使用了,故通过实现WebExceptionHandler的自定义异常处理handler来实现全局异常处理,同时通过@Order(-2)来提升我们自定义异常处理器的执行顺序,值越小优先级越高:
import com.abinge.boot.staging.model.Result;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@Order(-2)
public class ExeptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
log.error("process system error", throwable);
ServerHttpResponse response = serverWebExchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer wrap = response.bufferFactory().wrap(JSON.toJSONString(Result.fail(throwable.getMessage())).getBytes());
return response.writeWith(Mono.just(wrap));
}
}
(五)POM依赖取消已经不再使用的aop和validation的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.abinge.boot</groupId>
<artifactId>staging</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>staging</name>
<description>abinge demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
至此,基于通过RouterFunction模式将spring mvc项目改造为了spring webflux的项目已经完成。