文章目录
WebFlux 学习之路
本文借鉴于 Spring WebFlux 官方文档 ,对 Spring WebFlux进行学习、练习它的功能。涉及到 Reactive 的核心Api操作,操作数据库、Redis、Http请求等方面。
本文的代码仓库:
https://gitee.com/fengsoshuai/webflux-demo
以下说明只粘贴了部分代码,全部代码,需要童鞋去仓库下载哦。
1 、WebFlux 简介
Spring WebFlux 是Spring 框架在 5.0版本之后推出的,异步响应式框架。可以不需要 Servlet Api,也就是能够不使用 Spring Mvc就能对外暴露接口。通过 Reactor 项目,实现了相关的流式操作。
一般有两种写法。
一种是使用 Reactor 提供的流式Api,同时使用 Spring Mvc 的相关注解,使用上和 Spring Mvc 差异性很小。主要在于它返回的数据类型变成了 WebFlux 中的 Mono
或 Flux
了。具体见本项目中对应的模块《webflux-hello-web-demo》。
另一种写法是使用配置的方式,使用 @Confuguration ,并在该配置类中配置 Bean,返回RouterFunction对象。该对象可以有多个,存在优先级时可以使用 @Order 注解。该对象的作用是配置了路由,请求参数的格式等。具体见本项目中对应的《webflux-hello-demo》模块。
另外,Spring WebFlux 大量的使用了 流式API,如果对 Java 8 之后的 Stream 操作不熟悉的同学,可以先去了解 Stream 操作。可以参考一下 这篇文章。
2、WebFlux 的数据库操作
本文只探讨对mysql的操作。不同于常规的jdbc的方式,当使用响应式编程对数据库操作时,也有对应的驱动,mysql 的驱动叫 r2dbc。
它的主要特点是,从对数据库的连接,就开始返回了 Publisher 的数据类型。
一般需要引入包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>0.8.2.RELEASE</version>
</dependency>
WebFlux 实践内容
项目结构如下:
1 、入门案例
在Gitee 仓库中,对应的 webflux-hello-demo模块。完整代码请从代码仓库拉取。
主要是使用 RouterFunction 和 处理器,来替代原先的 SpringMVC的映射界面路由的功能。
1.1 RouterConfiguration
package org.feng.config;
import lombok.extern.slf4j.Slf4j;
import org.feng.handler.RouterHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* WebFlux 路由配置
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月07日 21时03分
*/
@Slf4j
@Configuration
public class RouterConfiguration {
@Bean
public RouterFunction<ServerResponse> routerMapper(RouterHandler handler) {
log.info("注册路由...");
// 这里在写完整后,可以考虑使用 import static 来简化代码;虽然牺牲了一部分可读性,但是一些无用代码确实能大面积减少
return RouterFunctions
// 根路径
.nest(RequestPredicates.path("/webflux"),
RouterFunctions
.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::hello)
// 追加其他路由
.andRoute(RequestPredicates.GET("/getEmpById").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::getEmpById)
);
}
}
1.2 RouterHandler
package org.feng.handler;
import lombok.extern.slf4j.Slf4j;
import org.feng.entity.Employee;
import org.feng.entity.dto.EmployeeDTO;
import org.feng.entity.dto.GetEmpRequestDTO;
import org.feng.service.EmployeeService;
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 javax.annotation.Resource;
/**
* 路由处理器
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月07日 21时05分
*/
@Slf4j
@Component
public class RouterHandler {
@Resource
private EmployeeService employeeService;
/**
* GET 请求 http://localhost/webflux/hello 时调用此方法;
*
* @param request 请求体
* @return 响应
*/
public Mono<ServerResponse> hello(ServerRequest request) {
log.info("Hello Webflux 的请求体 {}", request);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue("Hello Webflux");
}
/**
* 通过 HTTP 请求的入参id,获得想要的员工的数据
*
* @param request 请求体
* @return 响应
*/
public Mono<ServerResponse> getEmpById(ServerRequest request) {
log.info("getEmpById 的请求体 {}", request);
// 将接口传入的参数,转换为 Mono 对象
Mono<GetEmpRequestDTO> requestParam = request.bodyToMono(GetEmpRequestDTO.class);
// 消费请求参数,从 业务实现中查找数据,并消费转换获得最终响应
Mono<EmployeeDTO> response = requestParam.map(dto -> {
Mono<Employee> empById = employeeService.getEmpById(dto.getId());
EmployeeDTO responseEmp = new EmployeeDTO();
if (!Mono.empty().equals(empById)) {
// 消费数据
empById.subscribe(resp -> {
responseEmp.setName(resp.getName());
responseEmp.setAge(resp.getAge());
responseEmp.setBirthday(resp.getBirthday());
responseEmp.setJobNo(resp.getJobNo());
responseEmp.setHireDay(resp.getHireDay());
});
}
return responseEmp;
});
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(response, EmployeeDTO.class);
}
}
2、操作Mysql
这一部分主要是实现了 mysql 动态数据源的切换。
并且整合了nacos做配置中心,主要是配置了mysql的连接信息。
另外,练习了 Webclient 的常见使用。
对应的代码模块是 webflux-hello-mysql-demo。
2.1 动态数据源配置
package org.feng.config;
import dev.miku.r2dbc.mysql.MySqlConnectionConfiguration;
import dev.miku.r2dbc.mysql.MySqlConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.r2dbc.connection.lookup.AbstractRoutingConnectionFactory;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Mysql 动态数据源
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月14日 22时32分
*/
@Slf4j
@Configuration
public class MySqlDynamicDatasourceConfig extends AbstractRoutingConnectionFactory {
/**
* 默认租户ID
*/
private static final String DEFAULT_MERCHANT_ID = "master";
/**
* 租户Key,切换数据源时,Context对象设置的key
*/
public static final String MERCHANT_KEY = "merchantKey";
/**
* 数据源连接工厂映射
*/
private static final Map<Object, Object> CONNECTION_FACTORY_CACHE_MAP = new HashMap<>(8);
@Resource
private MysqlDatasourceProperties mysqlDatasourceProperties;
@Resource
private MysqlDatasourcePropertiesV2 mysqlDatasourcePropertiesV2;
@PostConstruct
private void init() {
final List<Property> propertyList = mysqlDatasourcePropertiesV2.getPROPERTY_LIST();
for (Property property : propertyList) {
MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(MySqlConnectionConfiguration.builder()
.host(property.getHost())
.port(property.getPort())
.username(property.getUsername())
.password(property.getPassword())
.database(property.getDatabase())
.connectTimeout(Duration.ofSeconds(property.getConnectTimeout()))
.build());
log.info("注册数据源 租户:{} 数据库名:{}", property.getMerchantKey(), property.getDatabase());
CONNECTION_FACTORY_CACHE_MAP.put(property.getMerchantKey(), connectionFactory);
}
setTargetConnectionFactories(CONNECTION_FACTORY_CACHE_MAP);
setDefaultTargetConnectionFactory(CONNECTION_FACTORY_CACHE_MAP.get(DEFAULT_MERCHANT_ID));
}
@NonNull
@Override
protected Mono<Object> determineCurrentLookupKey() {
return Mono.deferContextual(Mono::just).handle((context, sink) -> {
Object merchantKey = context.getOrDefault(MERCHANT_KEY, DEFAULT_MERCHANT_ID);
log.info("使用数据源 {} HashCode {}", merchantKey, CONNECTION_FACTORY_CACHE_MAP.get(merchantKey).hashCode());
assert merchantKey != null;
sink.next(merchantKey);
});
}
}
2.2 事务配置
package org.feng.config;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
/**
* 事务配置
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月15日 20时44分
*/
@Configuration
public class R2dbcTransactionConfiguration {
@Bean
public ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
return (new R2dbcTransactionManager(connectionFactory));
}
@Bean
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}
}
2.3 DAO 操作
package org.feng.repository;
import org.feng.entity.Student;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 学生表-数据库操作
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月11日 11时01分
*/
@Repository
public interface StudentRepository extends ReactiveCrudRepository<Student, Integer> {
/**
* 按照学生ID查找
*
* @param id 学生ID
* @return 对应ID的学生信息
*/
Mono<Student> getStudentById(Integer id);
/**
* 通过学生姓名,模糊查询
*
* @param name 学生姓名的一部分
* @return 学生列表
*/
@Query("select id, name, age, weight, height from student where name like :name")
Flux<Student> getStudentsByName(String name);
}
2.4 Service操作
这一步使用了两种操作数据库的方式。
package org.feng.service;
import org.feng.config.MySqlDynamicDatasourceConfig;
import org.feng.entity.Student;
import org.feng.repository.StudentRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.List;
/**
* 学生业务实现
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年06月11日 11时11分
*/
@Service
public class StudentServiceImpl implements StudentService {
@Resource
private StudentRepository studentRepository;
@Resource
private R2dbcEntityTemplate r2dbcEntityTemplate;
@Override
public Mono<Student> getById(Integer id) {
return studentRepository.getStudentById(id);
}
@Override
public Flux<Student> getStudentsByName(String merchantKey, String name) {
return studentRepository.getStudentsByName("%" + name + "%")
.contextWrite(context -> context.put(MySqlDynamicDatasourceConfig.MERCHANT_KEY, merchantKey));
}
@Transactional(rollbackFor = RuntimeException.class)
@Override
public Mono<Student> saveStudent(Student student) {
return studentRepository.save(student);
}
@Transactional(rollbackFor = RuntimeException.class)
@Override
public Flux<Student> saveStudent(List<Student> studentList) {
return studentRepository.saveAll(studentList);
}
@Override
@Transactional(rollbackFor = RuntimeException.class)
public Mono<Void> deleteStudent(Integer id) {
return studentRepository.deleteById(id);
}
@Override
public Flux<Student> selectStudent(Integer currentPage, String subName) {
// 设置每页显示2条数据
return r2dbcEntityTemplate.select(
Query.query(Criteria.where("name").like("%" + subName + "%")).with(PageRequest.of(currentPage - 1, 2)),
Student.class);
}
}
3、WebClient的操作
3.1 配置
package org.feng.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.client.reactive.ReactorResourceFactory;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.LoopResources;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.function.Function;
/**
* WebClient 配置,参考官方文档,https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
*
* <br>参考:
* <br> https://www.freesion.com/article/7381889590/
* <br> https://blog.csdn.net/weixin_44266223/article/details/122967933
*
* @version V1.0
* @author: fengjinsong
* @date: 2022年06月16日 15时41分
*/
@Configuration
public class WebClientConfig {
/**
* 默认情况下,HttpClient参与持有的全局 Reactor Netty 资源 reactor.netty.http.HttpResources,包括事件循环线程和连接池<br>
* 但是,这里选择不使用全局资源
*
* @return 资源工厂
*/
private ReactorResourceFactory reactorResourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
// 设置一个loop进行http线程管理
factory.setLoopResources(LoopResources.create("tcp-connect-loop", 30, true));
// 配置固定大小连接池
factory.setConnectionProvider(connectionProvider());
return factory;
}
private ConnectionProvider connectionProvider() {
return ConnectionProvider
.builder("tcp-connect-pool")
// 等待超时时间
.pendingAcquireTimeout(Duration.ofSeconds(6))
// 最大连接数
.maxConnections(30)
// 等待队列大小
.pendingAcquireMaxCount(300)
.maxIdleTime(Duration.ofSeconds(200))
.maxLifeTime(Duration.ofSeconds(200))
.build();
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// 连接超时时间
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
// 连接后的读、写超时
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)))
// 设置响应超时时间
.responseTimeout(Duration.of(6, ChronoUnit.SECONDS))
;
return client;
};
ClientHttpConnector connector = new ReactorClientHttpConnector(reactorResourceFactory(), mapper);
return WebClient.builder()
// 编解码器对在内存中缓冲数据大小修改
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(4 * 1024 * 1024))
.clientConnector(connector)
.build();
}
}
3.2 具体操作
package org.feng.controller;
import org.feng.entity.Student;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
/**
* 学习使用 webClient
*
* @version V1.0
* @author: fengjinsong
* @date: 2022年06月17日 09时03分
*/
@RequestMapping("/webclient")
@RestController
public class StudentWebClientController {
@Resource
private WebClient webClient;
private final String BASE_URL = "http://localhost/student";
@GetMapping("/{id}")
public Mono<Student> getOneByWebClient(@PathVariable("id") Integer id) {
return webClient.get().uri(BASE_URL + "/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve().bodyToMono(Student.class);
}
@GetMapping("/doPostSingle")
public Mono<Student> doPostSingle() {
Student student = new Student();
student.setName("doPostSingle");
return webClient.post().uri(BASE_URL + "/postSingle")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(student)
.retrieve()
.bodyToMono(Student.class);
}
@GetMapping("/doPostForm")
public Mono<Student> doPostForm() {
return webClient.post().uri(BASE_URL + "/postForm?studentName={studentName}&namePrefix={namePrefix}", "小明", LocalDate.now().toString())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.retrieve()
.bodyToMono(Student.class);
}
/**
* 请求体+路径参数
*/
@GetMapping("/doPostRequestBody")
public Mono<Student> doRequestBody() {
Student student = new Student();
student.setName("doPostRequestBody");
int age = ThreadLocalRandom.current().nextInt(20, 30);
return webClient.post().uri(BASE_URL + "/postRequestBody/{age}", age)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(student))
.retrieve()
.bodyToMono(Student.class);
}
/**
* 请求体+URL参数
*/
@GetMapping("/doRequestBodyWithParam")
public Mono<Student> doRequestBodyWithParam() {
Student student = new Student();
student.setName("doRequestBodyWithParam");
int id = ThreadLocalRandom.current().nextInt(20, 30);
return webClient.post().uri(BASE_URL + "/postRequestBodyWithParam?id={id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(student))
.retrieve()
.bodyToMono(Student.class);
}
@GetMapping("/doFormRequest1")
public Mono<Student> doFormRequest() {
Map<String, Object> paramMap = new HashMap<>(4);
paramMap.put("id", ThreadLocalRandom.current().nextInt(20, 30));
paramMap.put("name", "小明");
return webClient.post().uri(BASE_URL + "/formRequest1?name={name}&id={id}", paramMap)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.retrieve()
.bodyToMono(Student.class);
}
@GetMapping("/doFormRequest2")
public Mono<Student> testFormParam() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("name", "value1");
formData.add("id", "23");
return webClient.post()
.uri(BASE_URL + "/formRequest2")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, res -> Mono.error(new RuntimeException(res.statusCode() + "-自定义的")))
.onStatus(HttpStatus::is5xxServerError, res -> Mono.error(new RuntimeException(res.statusCode() + "自定义的")))
.bodyToMono(Student.class);
}
}
注意事项
在练习数据库操作时,需要连接数据库。
我这里使用了Nacos做配置。
项目中的 bootstrap.yml文件如下:
spring:
profiles:
active: ${profiles.active:dev}
application:
# 应用名称
name: webflux-demo
cloud:
nacos:
config:
# 启用 nacos 做配置
enabled: true
# 配置文件后缀
file-extension: yml
# 配置所在组
group: fjs
extension-configs:
# nacos 中组为 fjs,data-id 是 datasource.yml
- data-id: datasource.yml
group: fjs
# 配置热更新
refresh: true
debug: false
# 端口
server:
port: 80
---
spring:
cloud:
nacos:
config:
# nacos 用户名
username: nacos
# nacos 密码
password: nacos
# nacos 所在地址
server-addr: localhost:80
# 命名空间ID
namespace: fjs
config:
activate:
on-profile: dev
Nacos中的配置如下:
dynamic:
mysql:
datasource:
master:
merchantKey: master
host: localhost
port: 3306
username: root
password: 123456
database: dynamic_master1
connectTimeout: 30
slave:
merchantKey: slave
host: localhost
port: 3306
username: root
password: 123456
database: dynamic_slave1
connectTimeout: 30
map:
master:
- merchantKey=master
- host=localhost
- port=3306
- username=root
- password=123456
- database=dynamic_master1
- connectTimeout=30
slave:
- merchantKey=slave
- host=localhost
- port=3306
- username=root
- password=123456
- database=dynamic_slave1
- connectTimeout=30
学生表结构:
/*
Navicat Premium Data Transfer
Source Server : 阿里云
Source Server Type : MySQL
Source Server Version : 80020
Source Schema : test
Target Server Type : MySQL
Target Server Version : 80020
File Encoding : 65001
Date: 11/06/2022 11:21:25
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`age` int(0) NULL DEFAULT NULL,
`weight` double NULL DEFAULT NULL,
`height` double NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '小明', 23, 33.2, 172.23);
INSERT INTO `student` VALUES (2, '小明 79.1669237551011', 68, 79.1669237551011, 206.7008080419343);
INSERT INTO `student` VALUES (3, '小明 74.1299037849883', 63, 74.1299037849883, 185.21078230412152);
SET FOREIGN_KEY_CHECKS = 1;