基于Spring Webflux 搭建基本代码框架1

一、目标

基于Spring Webflux搭建一个实现NIO的Web Application代码框架,同时满足如下功能:

  1. 全局异常处理;
  2. 入参基本校验;
  3. 自定义统一响应数据格式;
  4. 统一日志记录;
  5. 实现数据基本的CURD操作。

二、实现步骤

基于一个Spring MVC项目进行改造,实现上述目标。

(一)待改造的Spring MVC项目情况介绍

基于Spring MVC并满足5个目标功能的具体代码实现,对外包括student和teacher的基本增删改查操作。

1、代码结构

image-20211008145312265

  1. config:整体项目配置
  2. controller:对外入口层,包括student和teacher的增删改查基本接口
  3. exceptions:自定义异常类及全局异常处理器
  4. interceptor:全局日志记录拦截器
  5. model:基本数据对象及自定义统一响应数据格式Result
  6. repository:基于JPA的持久层操作类
  7. service:具体业务处理层
  8. 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:
  1. 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));
    }
}
  1. 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:
  1. 自定义异常类
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;
    }
}
  1. 全局异常处理器
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:
  1. 基础类
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;
}
  1. 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;

}
  1. 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;
}
  1. 自定义统一响应数据格式
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:
  1. student持久层
import com.abinge.boot.staging.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Long> {
}
  1. teacher持久层
import com.abinge.boot.staging.model.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TeacherRepository extends JpaRepository<Teacher, Long> {
}
(9)service:
  1. 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);
    }
}
  1. 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进行实现。

改造后的代码结构如下:

image-20211008161953239

(一)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

  1. student持久层
import com.abinge.boot.staging.model.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface StudentRepository extends ReactiveCrudRepository<Student, Long> {
}
  1. 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层的各个接口响应类型也需要做出修改,以满足整体框架的响应式需求。

  1. 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();
    }
}
  1. 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的格式。

  1. 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)));
    }
}

  1. 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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值