1 缘起
新的项目,快速迭代,
技术选型:Spring WebFlux,
非Spring MVC,
之前没有接触过Spring WebFlux,项目中都是使用Spring MVC,
这次学到了新的知识Spring WebFlux,记录下。
2 SpringMVC & Spring WebFlux
Spring产品提供了两个并行的技术路线:
- 基于Spring MVC和Spring Data构造的Servlet API路线
- 基于Spring WebFlux和Spring Data的响应流路线
响应式:https://spring.io/reactive
响应式系统是低延迟、高吞吐工作负载的理想方案
WebFlux一切皆流。
下面看下Spring WebFlux和Spring MVC这两个技术路线的对比:
由上图可知,
Spring WebFlux支持非关系型数据库,
这一点与Spring MVC的差别还是很大的,
如果需要使用关系型数据库,基于Spring MVC构建项目是最有选择,
因此,基于Spring WebFlux构建项目,适用于非结构化存储。
本篇主要讲解应用,响应式编程的细节会在其他文章中分享。
3 项目依赖
版本:
SpringBoot2.4.5
Swagger:3.0.0
这里,使用spring-boot-starter-webflux作为启动容器(默认为Netty)。
<?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 https://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.monkey</groupId><artifactId>spring-boot-template</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-boot-template</name><description>Template project for Spring Boot</description><properties><java.version>1.8</java.version><io.springfox.version>3.0.0</io.springfox.version><io.swagger.version>1.5.22</io.swagger.version><fastjson.version>1.2.60</fastjson.version><logback.version>1.2.3</logback.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-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!--接口管理工具--><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>${io.springfox.version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${io.springfox.version}</version><exclusions><exclusion><groupId>io.swagger</groupId><artifactId>swagger-annotations</artifactId></exclusion><exclusion><groupId>io.swagger</groupId><artifactId>swagger-models</artifactId></exclusion></exclusions></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>${io.springfox.version}</version></dependency><dependency><groupId>io.swagger</groupId><artifactId>swagger-annotations</artifactId><version>${io.swagger.version}</version></dependency><dependency><groupId>io.swagger</groupId><artifactId>swagger-models</artifactId><version>${io.swagger.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version></dependency><!--日志工具--><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-core</artifactId><version>${logback.version}</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>${logback.version}</version></dependency><!--AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb-reactive</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
4 数据库
本文选择MongoDB作为持久层。
MongoDB的部署:https://blog.csdn.net/Xin_101/article/details/130902224
4.1 MongoDB配置
spring:devtools:restart:enabled: truedata:mongodb:uri: mongodb://tutorial:admin123456@172.22.75.234:27017/tutorial?authSource=tutorial
4.2 文档配置
MongoDB使用Document作为存储数据的基本单位,对比而言,MySQL中的表,新建的数据需要明确指定存储的Document,
下面新建一个user文档,存储用户数据。
其中,@Id用户表示该数据为MongDB中的_id
属性,由MongoDB生成。
而,使用MongoDB操作数据没有自动的数据映射,需要手动映射,因此,在每个需要映射的字段下新建了一个static final的属性名称。
如_id
,username
,操作数据时使用。
package com.monkey.springboottemplate.modules.user.entity;import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;/*** 用户信息.** @author xindaqi* @since 2023-05-28 11:38*/
@Document("user")
public class UserEntity {/*** 用户主键id,MongoDB自动生成*/@Idprivate String id;public final static String _id = "_id";/*** 用户id*/private String uid;/*** 用户姓名*/private String username;public static final String _username = "username";/*** 用户性别*/private String sex;public static final String _sex = "sex";/*** 创建时间*/private Long createdTime;/*** 更新时间*/private Long updatedTime;public static final String _updatedTime = "updatedTime";public String getId() {return id;}public void setId(String id) {this.id = id;}public String getUid() {return uid;}public void setUid(String uid) {this.uid = uid;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}public Long getCreatedTime() {return createdTime;}public void setCreatedTime(Long createdTime) {this.createdTime = createdTime;}public Long getUpdatedTime() {return updatedTime;}public void setUpdatedTime(Long updatedTime) {this.updatedTime = updatedTime;}@Overridepublic String toString() {return "UserEntity{" +"id='" + id + '\'' +", uid='" + uid + '\'' +", username='" + username + '\'' +", sex='" + sex + '\'' +", createdTime='" + createdTime + '\'' +", updatedTime='" + updatedTime + '\'' +'}';}
}
5 Service
WebFlux中使用Mono或者Flux作为返回对象,
因此,在Service层直接使用Mono或Flux作为最终的结果类型,
而不是在Controller层中封装最终结果,
WebFlux:一切皆流数据,因此,编码很优雅,可以做到瀑布式编程,数据一层一层地流下去,最终输出结果。
package com.monkey.springboottemplate.modules.user.service.impl;import com.monkey.springboottemplate.common.response.Response;
import com.monkey.springboottemplate.modules.user.convert.UserConvert;
import com.monkey.springboottemplate.modules.user.dao.UserDAO;
import com.monkey.springboottemplate.modules.user.entity.UserEntity;
import com.monkey.springboottemplate.modules.user.service.IUserService;
import com.monkey.springboottemplate.modules.user.vo.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;/*** 用户服务实现类.** @author xindaqi* @date 2021-05-08 16:01*/
@Service
public class UserServiceImpl implements IUserService {private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);@Autowiredprivate ReactiveMongoTemplate mongoTemplate;@Overridepublic Mono<Response<String>> addUser(UserAddInputVO params) {UserEntity userEntity = new UserEntity();userEntity.setUid(params.getUid());userEntity.setUsername(params.getUsername());userEntity.setSex(params.getSex());userEntity.setCreatedTime(System.currentTimeMillis());return mongoTemplate.insert(userEntity).flatMap(user -> Mono.just(Response.success(user.getId())));}@Overridepublic Mono<Response<String>> deleteUser(String id) {Query query = Query.query(Criteria.where(UserEntity._id).is(id));return mongoTemplate.findAndRemove(query, UserEntity.class).flatMap(user ->Mono.just(Response.success(id)));}@Overridepublic Mono<Response<String>> editUser(UserEditInputVO params) {Query query = Query.query(Criteria.where(UserEntity._id).is(params.getId()));Update update = new Update().set(UserEntity._sex, params.getSex()).set(UserEntity._username, params.getUsername()).set(UserEntity._updatedTime, System.currentTimeMillis());return mongoTemplate.updateFirst(query, update, UserEntity.class).flatMap(user -> Mono.just(Response.success(params.getId())));}@Overridepublic Mono<Response<UserInfoVO>> queryUserById(String id) {Query query = Query.query(Criteria.where(UserEntity._id).is(id));return mongoTemplate.findOne(query, UserEntity.class).flatMap(user -> {UserInfoVO userInfo = new UserInfoVO();userInfo.setUid(user.getUid());userInfo.setId(user.getId());userInfo.setSex(user.getSex());userInfo.setUsername(user.getUsername());return Mono.just(Response.success(userInfo));});}@Overridepublic Mono<Response<List<UserInfoVO>>> queryUserByPage(UserPageInputVO params) {Response<List<UserInfoVO>> resp = Response.success();Query query = new Query();return mongoTemplate.count(query, UserEntity.class).flatMap(count -> {if (Objects.isNull(count)) {resp.setTotal(0);}resp.setTotal(count);query.skip((long) (params.getPageStart() - 1) * params.getPageSize()).limit(params.getPageSize());return mongoTemplate.find(query, UserEntity.class).collectList();}).map(userLi -> {List<UserInfoVO> li = userLi.stream().map(UserConvert::convert).collect(Collectors.toList());resp.setData(li);return resp;});}
}
6 Controller
接口层,直接使用Mono封装结果。
package com.monkey.springboottemplate.api;import com.monkey.springboottemplate.common.response.Response;
import com.monkey.springboottemplate.modules.user.service.IUserService;
import com.monkey.springboottemplate.modules.user.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.data.repository.query.Param;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;import javax.annotation.Resource;import java.util.List;import static com.monkey.springboottemplate.common.constant.DigitalConstant.ONE;/*** 用户增删改查接口.** @author xindaqi* @date 2021-05-08 16:09*/
@RestController
@RequestMapping("/api/v1/user")
@Api(tags = "人员配置")
public class UserApi {@ResourceIUserService userService;@PostMapping("/add")@ApiOperation("添加用户")public Mono<Response<String>> addUser(@RequestBody UserAddInputVO params) {return userService.addUser(params);}@GetMapping("/delete")@ApiOperation("删除用户")public Mono<Response<String>> deleteUser(@Param("id") String id) {return userService.deleteUser(id);}@PostMapping("/edit")@ApiOperation("编辑/修改用户")public Mono<Response<String>> editUser(@RequestBody UserEditInputVO params) {return userService.editUser(params);}@GetMapping("/query")@ApiOperation("根据ID查询用户")public Mono<Response<UserInfoVO>> queryUserById(@Param("id") String id) {return userService.queryUserById(id);}@PostMapping("/query/page")@ApiOperation("分页查询用户")public Mono<Response<List<UserInfoVO>>> queryUserByPage(@RequestBody UserPageInputVO params) {return userService.queryUserByPage(params);}
}
7 配置Swagger
Swagger使用3.0.0版本。
package com.monkey.springboottemplate.common.config;import springfox.documentation.service.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import org.springframework.core.env.Profiles;
import org.springframework.core.env.Environment;
import springfox.documentation.oas.annotations.EnableOpenApi;/*** Swagger配置.** @author xindaqi* @since 2023-05-27 22:20*/
@Configuration
@EnableSwagger2
@EnableOpenApi
public class SwaggerConfig {@Beanpublic Docket createRestApi(Environment environment){// 配置Swagger显示,仅dev和test环境显示Profiles profiles = Profiles.of("dev");boolean b = environment.acceptsProfiles(profiles);return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).enable(b).select().apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();}private ApiInfo apiInfo(){return new ApiInfoBuilder().title("接口文档").contact(new Contact("xindaqi", "xxx@qq.com", "xxx@qq.com")).version("1.0").description("个人信息").termsOfServiceUrl("http://localhost:9121/api/v1").build();}
}
访问:http://localhost:9121/swagger-ui/index.htm
8 小结
技术选型依实际情况而定,不是为了引入技术而引入技术,是为了解决问题,引入技术。
(1)Spring WebFlux使用Mono或Flux作为返回类型;
(2)Spring WebFlux一切皆流,通过流将数据一层一层地传递下去,编程方式非常优雅:瀑布式;
(3)Spring WebFlux使用Swagger3.0.0,测试时,使用了Swagger2.9.2无法生成文档;
(4)Spring WebFlux是响应式编程,低延迟、高吞吐系统的首选,但是,数据层的选择受限,如仅支持有限的非关系型数据库:MongoDB、Redis。