一、目标
基于Spring Webflux搭建一个实现NIO的Web Application代码框架,同时满足如下功能:
- 全局异常处理;
- 入参基本校验;
- 自定义统一响应数据格式;
- 统一日志记录;
- 实现数据基本的CURD操作。
二、实现步骤
基于一个Spring MVC项目进行改造,实现上述目标。
(一)待改造的Spring MVC项目情况介绍
基于Spring MVC并满足5个目标功能的具体代码实现,对外包括student和teacher的基本增删改查操作。
1、代码结构
- config:整体项目配置
- controller:对外入口层,包括student和teacher的增删改查基本接口
- exceptions:自定义异常类及全局异常处理器
- interceptor:全局日志记录拦截器
- model:基本数据对象及自定义统一响应数据格式Result
- repository:基于JPA的持久层操作类
- service:具体业务处理层
- StagingApplication:应用启动类
2、具体代码实现
(1)数据库脚本:
CREATE TABLE `student` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`is_deleted` smallint(0) NOT NULL,
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`age` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`class_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`grade` int(0) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`sex` smallint(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `teacher` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT NULL,
`is_deleted` smallint(0) NOT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
`age` int(0) NOT NULL,
`course` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(2)POM依赖:
<?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.1.1.RELEASE</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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>org.springframework.data</groupId>
<artifactId>spring-data-releasetrain</artifactId>
<version>Lovelace-SR3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.properties:
spring.application.name="staging"
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/abinge?characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
(3)config:
import com.abinge.boot.staging.interceptor.LogInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LogInterceptor logInterceptor;
// 添加统一日志记录拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
/**
* 解决跨域问题
*
* @return
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
}
(4)controller:
- student入口:
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Student;
import com.abinge.boot.staging.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
// student的controller
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("/add")
public Result add(@RequestBody @Valid Student student) {
return Result.success(studentService.add(student));
}
@PostMapping("/update")
public Result update(@Valid Student student) {
return Result.success(studentService.update(student));
}
@PostMapping("/delete")
public Result delete(@Valid Student student) {
return Result.success(studentService.delete(student));
}
@PostMapping("/queryById")
public Result queryById(@NotNull Long id) {
return Result.success(studentService.queryById(id));
}
@GetMapping("/queryAll")
public Result queryAll() {
return Result.success(studentService.queryAll());
}
@PostMapping("/queryAll")
public Result queryAll(@Valid Student student) {
return Result.success(studentService.queryAll(student));
}
}
- teacher入口:
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Teacher;
import com.abinge.boot.staging.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
// teacher的controller
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@PostMapping("/add")
public Result add(@Valid Teacher teacher) {
return Result.success(teacherService.add(teacher));
}
@PostMapping("/update")
public Result update(@Valid Teacher teacher) {
return Result.success(teacherService.update(teacher));
}
@PostMapping("/delete")
public Result delete(@Valid Teacher teacher) {
return Result.success(teacherService.delete(teacher));
}
@PostMapping("/queryById")
public Result queryById(@NotNull Long id) {
return Result.success(teacherService.queryById(id));
}
@GetMapping("/queryAll")
public Result queryAll() {
return Result.success(teacherService.queryAll());
}
@PostMapping("/queryAll")
public Result queryAll(@Valid Teacher teacher) {
return Result.success(teacherService.queryAll(teacher));
}
}
(5)exceptions:
- 自定义异常类
public class BizException extends RuntimeException{
public BizException(){super();}
public BizException(String message){
super(message);
}
public BizException(String message, Throwable cause){
super(message,cause);
}
public BizException(Throwable cause) {
super(cause);
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
- 全局异常处理器
import com.abinge.boot.staging.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
@RestControllerAdvice
@Slf4j
public class ExeptionHandler {
// validation 参数校验异常处理
@ExceptionHandler(WebExchangeBindException.class)
public Result handBindException(WebExchangeBindException e) {
log.error("process web exchange bind error", e);
String errMsg = e.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
.reduce(((s1, s2) -> s1 + " \n" + s2))
.orElse(StringUtils.EMPTY);
return Result.fail(errMsg);
}
// 自定义异常处理
@ExceptionHandler(BizException.class)
public Result handBizException(BizException e) {
log.error("process system error", e);
return Result.fail(e.getMessage());
}
//兜底异常处理
@ExceptionHandler(Exception.class)
public Result handException(Exception e) {
log.error("process system error", e);
return Result.fail();
}
}
(6)interceptor:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 统一日志记录拦截器
@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) {
//统一打印日志
String requestURL = httpServletRequest.getRequestURI();
String method = httpServletRequest.getMethod();
//原始请求地址
String ip = httpServletRequest.getLocalAddr();
//请求参数
String params = httpServletRequest.getQueryString();
log.info("IP:{},method:{},url:{}?{}", ip, method, requestURL, params);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
(7)model:
- 基础类
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
// 基础类
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseModel implements Serializable {
/**
* 主键id,且设置为自增
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@CreatedDate
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
protected Date createTime;
@LastModifiedDate
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
protected Date updateTime;
protected short isDeleted;
}
- student类
import lombok.Data;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
@Entity
@Data
public class Student extends BaseModel {
@NotBlank
private String name;
@NotBlank
private String age;
private int grade;
private short sex;
private String className;
}
- teacher类
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
public class Teacher extends BaseModel {
@NotBlank
private String name;
private int age;
private String course;
}
- 自定义统一响应数据格式
import org.apache.commons.lang3.StringUtils;
public class Result<T> {
private final static String SUCCESS_CODE = "0";
private final static String SUCCESS_MSG = "成功";
private final static String FAIL_CODE = "1";
private final static String FAIL_MSG = "系统异常";
public String code;
public String msg;
public T data;
public Result() { }
public static <T> Result success(){
return success(SUCCESS_MSG,StringUtils.EMPTY);
}
public static <T> Result success(T data){
return success(SUCCESS_MSG,data);
}
public static <T> Result success(String msg,T data){
return new Result(SUCCESS_CODE,msg,data);
}
public static <T> Result fail(){
return fail(FAIL_MSG,StringUtils.EMPTY);
}
public static <T> Result fail(String msg){
return fail(msg,StringUtils.EMPTY);
}
public static <T> Result fail(String msg,T data){
return new Result(FAIL_CODE,msg,data);
}
private Result(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
(8)repository:
- student持久层
import com.abinge.boot.staging.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentRepository extends JpaRepository<Student, Long> {
}
- teacher持久层
import com.abinge.boot.staging.model.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TeacherRepository extends JpaRepository<Teacher, Long> {
}
(9)service:
- student业务处理层
import com.abinge.boot.staging.repository.StudentRepository;
import com.abinge.boot.staging.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public Student add(Student student) {
return studentRepository.save(student);
}
public Student update(Student student) {
return studentRepository.saveAndFlush(student);
}
public Student delete(Student student) {
studentRepository.delete(student);
return student;
}
public Student queryById(Long id) {
return studentRepository.getOne(id);
}
public List<Student> queryAll() {
return studentRepository.findAll();
}
public List<Student> queryAll(Student student) {
Example<Student> example = Example.of(student);
return studentRepository.findAll(example);
}
}
- teacher业务处理层
import com.abinge.boot.staging.repository.TeacherRepository;
import com.abinge.boot.staging.model.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TeacherService {
@Autowired
private TeacherRepository teacherRepository;
public Teacher add(Teacher teacher) {
return teacherRepository.save(teacher);
}
public Teacher update(Teacher teacher) {
return teacherRepository.saveAndFlush(teacher);
}
public Teacher delete(Teacher teacher) {
teacherRepository.delete(teacher);
return teacher;
}
public Teacher queryById(Long id) {
return teacherRepository.getOne(id);
}
public List<Teacher> queryAll() {
return teacherRepository.findAll();
}
public List<Teacher> queryAll(Teacher teacher) {
Example<Teacher> example = Example.of(teacher);
return teacherRepository.findAll(example);
}
}
(10)StagingApplication:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class StagingApplication {
public static void main(String[] args) {
SpringApplication.run(StagingApplication.class, args);
}
}
(二)基于Spring Webflux进行改造
Spring Webflux的实现方式有如下两种:
1、传统Controller模式
代码层级沿用Spring MVC项目模式,保持Controller、Service的模式,但通过Mono和Flux实现NIO
2、RouterFunction模式
代码模式取消对应的Controller模式,而是通过各种Handler实现,完全的响应式流代码模式
三、代码实现-传统Controller模式
主要改造点包括如下几点:
1、为满足响应式NIO的需求,各层数据类型均需改造为Mono或Flux;
2、持久层框架取消jdbc这种阻塞IO的客户端,修改为r2dbc这种非阻塞NIO的客户端;
3、spring webflux中取消了intercepter的定义,故全局日志记录拦截器需要通过WebFliter进行实现。
改造后的代码结构如下:
(一)POM依赖改造
1、修改spring boot的依赖版本为2.4.5
2、取消spring-boot-starter-web的依赖,修改为spring-boot-starter-webflux的依赖
3、取消spring-boot-starter-data-jpa的依赖,修改为spring-boot-starter-data-r2dbc(一个响应式的持久层客户端)的依赖,关于r2dbc的介绍,详见:https://spring.io/projects/spring-data-r2dbc 和 https://r2dbc.io/
4、取消mysql-connector-java的依赖,修改为r2dbc-mysql的依赖
<?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-aop</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.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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>
(二)application.properties配置改造
因为mysql客户端从jdbc修改为了r2dbc了,所以相关配置也需要修改r2dbc的配置即可。
spring.application.name="staging"
server.port=8081
# 此处相关配置修改为r2dbc的配置
spring.r2dbc.url=r2dbc:mysql://localhost:3306/abinge?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
spring.r2dbc.username=root
spring.r2dbc.password=12345678
spring.jackson.serialization.indent_output=true
(三)Repository层改造
1、将原有持久层各接口实现接口从org.springframework.data.jpa.repository.JpaRepository修改为org.springframework.data.repository.reactive.ReactiveCrudRepository,仍然可保留原有JPA的风格。
2、持久层框架修改后,其返回对象将统一为了Mono和Flux
- student持久层
import com.abinge.boot.staging.model.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface StudentRepository extends ReactiveCrudRepository<Student, Long> {
}
- teacher持久层
import com.abinge.boot.staging.model.Teacher;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface TeacherRepository extends ReactiveCrudRepository<Teacher, Long> {
}
(四)Service层改造
因为依赖的持久层框架返回对象变为了Mono和Flux,所以service层的各个接口响应类型也需要做出修改,以满足整体框架的响应式需求。
- student业务处理类
import com.abinge.boot.staging.repository.StudentRepository;
import com.abinge.boot.staging.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public Mono<Student> add(Student student) {
return studentRepository.save(student);
}
public Mono<Student> update(Student student) {
return studentRepository.save(student);
}
public Mono<Student> delete(Student student) {
studentRepository.delete(student);
return Mono.just(student);
}
public Mono<Student> queryById(Long id) {
return studentRepository.findById(id);
}
public Flux<Student> queryAll() {
return studentRepository.findAll();
}
public Flux<Student> queryAll(Student student) {
return studentRepository.findAll();
}
}
- teacher业务处理类
import com.abinge.boot.staging.repository.TeacherRepository;
import com.abinge.boot.staging.model.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class TeacherService {
@Autowired
private TeacherRepository teacherRepository;
public Mono<Teacher> add(Teacher teacher) {
return teacherRepository.save(teacher);
}
public Mono<Teacher> update(Teacher teacher) {
return teacherRepository.save(teacher);
}
public Mono<Teacher> delete(Teacher teacher) {
teacherRepository.delete(teacher);
return Mono.just(teacher);
}
public Mono<Teacher> queryById(Long id) {
return teacherRepository.findById(id);
}
public Flux<Teacher> queryAll() {
return teacherRepository.findAll();
}
public Flux<Teacher> queryAll(Teacher teacher) {
return teacherRepository.findAll();
}
}
(五)Controller层改造
为满足我们统一响应数据结构,又要满足响应式NIO的需求,我们需要将service层返回的对象做二次处理;
1、统一转换为Mono的类型,这里使用flatMap进行转换;
2、对于service层返回的数据为List类型时,这里需要额外做一步collectList()操作,否则响应对象将会是类似于List的格式,而不是Result的格式。
- student入口
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Student;
import com.abinge.boot.staging.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("/add")
public Mono<Result> add(@Valid @RequestBody Student student) {
return studentService.add(student).flatMap(stu -> Mono.just(Result.success(stu)));
}
@PostMapping("/update")
public Mono<Result> update(@RequestBody @Valid Student student) {
return studentService.update(student).flatMap(stu -> Mono.just(Result.success(stu)));
}
@PostMapping("/delete")
public Mono<Result> delete(@RequestBody Student student) {
return studentService.delete(student).flatMap(stu -> Mono.just(Result.success(stu)));
}
@PostMapping("/queryById")
public Mono<Result> queryById(Long id) {
return studentService.queryById(id).flatMap(stu -> Mono.just(Result.success(stu)));
}
@GetMapping(value = "/queryAll")
public Mono<Result> queryAll() {
long start = System.currentTimeMillis();
Flux<Student> result = studentService.queryAll();
System.out.printf("queryAll : " + (System.currentTimeMillis() - start));
// 对于service层返回的数据为List类型时,这里需要额外做一步collectList()操作,
// 否则响应对象将会是类似于List<Result>的格式,而不是Result<List>的格式
return result.collectList().flatMap(students -> Mono.just(Result.success(students)));
}
@PostMapping(value = "/queryAll")
public Mono<Result> queryAll(@RequestBody Student student) {
long start = System.currentTimeMillis();
Flux<Student> result = studentService.queryAll(student);
System.out.printf("queryAll : " + (System.currentTimeMillis() - start));
// 对于service层返回的数据为List类型时,这里需要额外做一步collectList()操作,
// 否则响应对象将会是类似于List<Result>的格式,而不是Result<List>的格式
return result.collectList().flatMap(students -> Mono.just(Result.success(students)));
}
}
- teacher入口
import com.abinge.boot.staging.model.Result;
import com.abinge.boot.staging.model.Teacher;
import com.abinge.boot.staging.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@PostMapping("/add")
public Mono<Result> add(@RequestBody @Valid Teacher teacher) {
return teacherService.add(teacher).flatMap(tea -> Mono.just(Result.success(tea)));
}
@PostMapping("/update")
public Mono<Result> update(@RequestBody @Valid Teacher teacher) {
return teacherService.update(teacher).flatMap(tea -> Mono.just(Result.success(tea)));
}
@PostMapping("/delete")
public Mono<Result> delete(@RequestBody Teacher teacher) {
return teacherService.delete(teacher).flatMap(tea -> Mono.just(Result.success(tea)));
}
@PostMapping("/queryById")
public Mono<Result> queryById(Long id) {
return teacherService.queryById(id).flatMap(tea -> Mono.just(Result.success(tea)));
}
@GetMapping("/queryAll")
public Mono<Result> queryAll() {
return teacherService.queryAll().collectList().flatMap(teaList -> Mono.just(Result.success(teaList)));
}
@PostMapping("/queryAll")
public Mono<Result> queryAll(@RequestBody Teacher teacher) {
return teacherService.queryAll(teacher).collectList().flatMap(teaList -> Mono.just(Result.success(teaList)));
}
}
(六)日志记录改造
原有的统一日志记录是通过intercepter进行的实现,但spring webflux中取消了intercepter的定义,需要通过WebFliter进行实现。
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.util.Map;
@Component
@Slf4j
public class LogFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestURL = request.getURI().getPath();
HttpMethod method = request.getMethod();
Map<String, String> headers = request.getHeaders().toSingleValueMap();
Map<String, String> queryMap = request.getQueryParams().toSingleValueMap();
InetSocketAddress ip = request.getLocalAddress();
log.info("IP:{},method:{}, url:{}?{}, headers:{}, body:{}", ip, method, requestURL, queryMap, headers);
return chain.filter(exchange);
}
}
(七)全局异常改造
原有全局异常返回类型为Result,这里需要改造为Mono的格式。
import com.abinge.boot.staging.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import reactor.core.publisher.Mono;
@RestControllerAdvice
@Slf4j
public class ExeptionHandler {
@ExceptionHandler(BizException.class)
public Mono<Result> handBizException(BizException e) {
log.error("process system biz error", e);
return Mono.just(Result.fail(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public Mono<Result> handException(Exception e) {
log.error("process system error", e);
return Mono.just(Result.fail());
}
@ExceptionHandler(WebExchangeBindException.class)
public Mono<Result> handBindException(WebExchangeBindException e) {
log.error("process web exchange bind error", e);
String errMsg = e.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
.reduce(((s1, s2) -> s1 + " \n" + s2))
.orElse(StringUtils.EMPTY);
return Mono.just(Result.fail(errMsg));
}
}
至此,通过传统Controller模式将spring mvc项目改造为了spring webflux的项目。
基于RouterFunction模式的改造详见:基于Spring Webflux 搭建基本代码框架2