其实spring的webflux从spring 5.0以后进引入了,但是因为时间问题一直没怎么看,所以这次打算简单的学习一下。webflux和vert.x非常的类似,当然vert.x更全面一些,其底层也是Netty
实现的,而webflux还是需要spring的其他依赖,比如Data Reactive
,但是在容器选择上选择性更多一些,如果对vert.x感兴趣的也可以去看看,最近找工作看到有家公司就对vert.x有比较高的要求,自己之前也曾经简单的学过ver.x。今天不谈响应式和传统的mvc的区别、优缺点这些,只是简单的学习下怎么进行开发。
其实webflux开发的话和mvc非常的相似,很多注解都是可以共用的,在一定程度上减少了从mvc迁移到webflux的成本。一开始我建议还是可以先看下官方的文档,尤其是关于mvc和webflux的相同点和不同点,感觉还是非常的不错的,webflux官方文档。
下面这张图是官方文档中关于mvc和webflux的一个对比,共同点和不同点还是非常清晰的。
webflux可以使用两种编程模式,一种就是大家都非常熟悉的注解的方式,即使用
@Controller
、@RestController
,另一种是函数式编程,这点和vert.x还是比较类似的。下面会通过两种不同的方式做一个简单的练习。webflux使用的话需要有相关的响应式数据源,因为传统的JDBC是阻塞的,因此必须使用非阻塞(响应式)的数据源,目前spring支持的有Redis、MongoDB、Cassandra等。因为我本地只安装了Redis,所以选择了Reactive Redis
的依赖。新建一个spring boot项目,在选择依赖的时候Web
选择Spring Reactive Web
,NoSQL
选择Spring Data Reactive Redis
,另外也可以选择Lombok
。pom文件依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</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>
在这个项目中因为没有了传统的RDS,所以在持久层我会使用Redis的模板对象对数据进行增删改查,一般我们会使用RedisTemplate
,但是在这个项目中我们使用的是ReactiveRedisTemplate
。另外就是关于Redis数据序列化的问题,我觉得最好还是使用json,所以我们需要进行一个自定义配置,指定Redis的key、value的序列化方式,代码如下:
@Configuration
public class ReactiveRedisConfig {
@Bean
public RedisSerializationContext redisSerializationContext() {
RedisSerializationContext.RedisSerializationContextBuilder builder = RedisSerializationContext.newSerializationContext();
// 指定key value的序列化
builder.key(StringRedisSerializer.UTF_8);
builder.value(RedisSerializer.json());
builder.hashKey(StringRedisSerializer.UTF_8);
builder.hashValue(RedisSerializer.json());
return builder.build();
}
@Bean
public ReactiveRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) {
RedisSerializationContext serializationContext = redisSerializationContext();
ReactiveRedisTemplate reactiveRedisTemplate = new ReactiveRedisTemplate(connectionFactory,serializationContext);
return reactiveRedisTemplate;
}
}
接下来分别使用注解方式和函数式来实现几个简单增删改查的接口。
一、使用注解方式
注解方式一个其实使用起来和使用mvc没什么区别,下面依然和使用mvc一样,定义一个Controller
,并定义几个增删改查的接口,代码如下:
@RestController
@RequestMapping("/user")
public class ReactiveController {
@Autowired
private ReactiveService reactiveService;
// 查询所有
@GetMapping("/find/all")
public Flux<User> findAll() {
return reactiveService.findAll();
}
// 查询单个
@GetMapping("/query/{uuid}")
public Mono<User> queryByName(@PathVariable("uuid") String uuid) {
return reactiveService.queryByUUID(uuid);
}
// 添加用户
@PostMapping("/add")
public Mono<Boolean> add(@RequestBody User user) {
return reactiveService.add(user);
}
// 更新用户
@PutMapping("/update")
public Mono<Boolean> update(@RequestBody User user) {
return reactiveService.update(user);
}
// 删除用户
@DeleteMapping("/delete/{uuid}")
public Mono<Boolean> delete(@PathVariable("uuid") String uuid) {
return reactiveService.delete(uuid);
}
}
和使用mvc没有任何的区别,唯一的区别在于返回的对象是Mono
和Flux
,简单点理解,返回单个数据就是Mono
,多个就使用Flux
。更详细的我觉得可以看下官方文档的介绍。
然后是业务层代码,这里只是调用了ReactiveRedisTemplate
对数据进行操作,代码如下:
@Service
public class ReactiveServiceImpl implements ReactiveService {
@Autowired
private ReactiveRedisTemplate reactiveRedisTemplate;
private static final String USER_KEY = "entity:user";
@Override
public Flux<User> findAll() {
return reactiveRedisTemplate.opsForHash().values(USER_KEY);
}
@Override
public Mono<User> queryByUUID(String name) {
return reactiveRedisTemplate.opsForHash().get(USER_KEY,name);
}
@Override
public Mono<Boolean> add(User user) {
String uuid = generateUUID();
user.setUuid(uuid);
return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
}
private String generateUUID() {
return UUID.randomUUID().toString().replace("-","");
}
@Override
public Mono<Boolean> update(User user) {
return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
}
@Override
public Mono<Boolean> delete(String uuid) {
return reactiveRedisTemplate.opsForHash().delete(uuid);
}
}
启动项目可以看到实际上使用的是Netty
然后通过调用添加数据的接口向Redis添加几条数据,之后调用其他查询接口或者修改接口都是正常的,和使用mvc没什么区别,就不再细述了。
二、使用函数式
函数式变成涉及到几个概念我觉得是比较重要的比如RouterFunction
、HandlerFunction
还有一个DispatcherHandler
其实和mvc都是有对应关系的,不是很难理解。RouterFunction
就是一个路由函数,可以理解为将请求和具体的HandlerFunction
做一个映射。
下面我们按照先定义一个RouterFunction
,但是创建这个bean需要依赖具体的Handler,代码如下:
@Component
public class UserFunctionRouter {
@Autowired
private UserHandler userHandler;
@Bean("userRouter")
public RouterFunction router() {
RouterFunction<ServerResponse> routerFunction = route()
.GET("/user/find/all", accept(MediaType.APPLICATION_JSON_UTF8), userHandler::findAll)
.GET("/user/query/{uuid}", accept(MediaType.APPLICATION_JSON), userHandler::queryByName)
.POST("/user/add", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::add)
.PUT("/user/update", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::update)
.DELETE("/user/delete/{uuid}",accept(MediaType.APPLICATION_JSON_UTF8), userHandler::delete)
.build();
return routerFunction;
}
}
上面的代码是将具体的请求路径和具体的handler做了映射,这样会根据用户具体的请求路径找具体的handler,其实就是具体的方法。和mvc的@RequestMapping
功能上是一样的。但是这个需要注意的是返回的结果是ServerResponse
,那么请求肯定是ServerRequest
,这个也可以和mvc的HttpServletRequest
、HttpServletResponse
对应起来,都是封装用户的请求信息,其实和mvc都还是能对应起来的,只是编程方式不太一样。
然后我们定义自己的HandlerFunction
,代码如下:
@Component
public class UserHandler {
@Autowired
private UserRepository userRepository;
public Mono findAll(ServerRequest serverRequest) {
Flux<User> flux = userRepository.findAll();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(flux, User.class);
}
// 查询单个
public Mono queryByUUID(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userRepository.queryByUUID(uuid),User.class);
}
// 添加用户
public Mono add(ServerRequest serverRequest) {
// 将请求体转成指定Momo对象
Mono<User> mono = serverRequest.bodyToMono(User.class);
User user = mono.block();
Optional<User> optional = mono.blockOptional();
return userRepository.add(user);
}
// 更新用户
public Mono update(ServerRequest serverRequest) {
Mono<User> mono = serverRequest.bodyToMono(User.class);
User user = mono.block();
return userRepository.update(user);
}
// 删除用户
public Mono delete(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return userRepository.delete(uuid);
}
}
在上面的代码中我没有使用具体的范型类型,而是直接使用的Mono
,不管返回的是多个结果还是单个结果其实都不影响具体的输出。比如查询的方法中我时候直接返回ServerResponse
,这时候真正返回的是Mono<ServerResponse>
,也就是说真正的结果内容会被一个Mono
包裹起来,当然也可以指定具体的范型类型。
因为在这个demo里面使用的是Redis存储数据,我选择的数据结构是Hash,这样我就可以将同一类型的数据都放到一个Hash里面,Hash的key使用一个唯一标识,value就是具体的数据。但是新增的时候因为不存在唯一标识,也不能像传统的RDS一样通过自增的方式实现,所以我就想使用一个UUID做为唯一标识符,即代码生成一个UUID,然后赋值给传入的model,在上面使用注解方式的时候我就是这么做的,运行起来没有任何的问题。而且也可以很轻松的将ServerRequest
请求体转变成一个Mono
或者Flux
的具体类型。但是实际中却遇到了一个难点,看下面的代码:
Mono<User> mono = serverRequest.bodyToMono(User.class);
User user = mono.block();
Optional<User> optional = mono.blockOptional();
String uuid = generateUUID();
user.setUuid(uuid);
return userRepository.add(user);
首先将请求体转变成Mono<User>
,然后获取到具体的User
对象,并将一个UUID赋值给这个对象,然后保存到Redis,理论上没有问题,而且根据上面使用注解方式开发时这个方法是切实可行的。但是测试的时候就出现了问题,下面是我调用接口的请求参数,和使用注解开发一样的:
下面的是测试结果:
开始的时候我也很意外,没想到是500。注意错误信息
block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-2
之所以会报这个错误是因为我们在代码中调用了阻塞的方法,即
User user = mono.block();
也就是说这个方法在响应式编程中是不被支持的(不被支持能不能不要啊!!)所以你调用和阻塞相关的就报错,换方法依然不能解决,不管是blockOptional
还是toFuture
等一系列能够得到User
的方法都无效。而且还有一个问题就是使用mvc时可以使用@RequestBody
注解,将请求体映射成为我们需要的类型对象,而且也不影响使用HttpServletRequest
。但是函数式好像并不能同时存在,即用户的请求信息都只能通过ServerRequest
来获取。自己网上搜索了一下但是还是没能解决如何从请求体获取具体请求参数的问题。
查询方法倒是都没有任何问题,所以我的解决方法也只能将请求参数都作为Param
提交了,但是这样的话获取参数会比较麻烦,尤其是复杂对象。但是自己目前还是没能找到合适的方法,如果有对这部分比较熟悉的小伙伴希望指点一下。
更新:后来有朋友指出可使用Mono的doOnNext()方法,这个方法需要一个Consumer函数,可以对Mono包裹的数据进行操作。虽然UUID的问题解决了,而且也可以保存到Redis,但是又困在了如何处理ReactiveRedisTemplate
添加数据后返回结果的问题,返回的是一个Mono<Boolean>,从Mono<User>转成Mono<Boolean>也有点别扭,尝试使用map方法倒是可以,但是感觉不是特别的顺手。所以写了两种方法,一是:保存的时候不返回结果,这种方法需要将ReactiveRedisTemplate
返回的Mono<Boolean>给消耗掉,我的理解是终结操作,不然不能写入到Redis;方法二是将ReactiveRedisTemplate
返回的Mono<Boolean>返回给调用者,这时候是在Mono<User>做了映射,即将Mono<Boolean>映射成一个Object,最终映射成Mono<Object>返回给api的调用者。当然还可能有其他方法,但是目前我尝试这两种方法是可行的。
所以最终的Handler代码如下:
@Slf4j
@Component
public class UserHandler {
@Autowired
private UserRepository userRepository;
public Mono findAll(ServerRequest serverRequest) {
Flux<User> flux = userRepository.findAll();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(flux, User.class);
}
// 查询单个
public Mono queryByUUID(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userRepository.queryByUUID(uuid),User.class);
}
// 添加用户
public Mono add(ServerRequest serverRequest) {
// 将请求体转成指定Momo对象
Mono<User> mono = serverRequest.bodyToMono(User.class);
String uuid = generateUUID();
// 方法2
Mono<Object> safeUser = mono.doOnNext(u -> u.setUuid(uuid)).map(user -> {return userRepository.saveUser(user);});
// User user = createUser(serverRequest);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(safeUser,Object.class);
//方法1
// Mono<User> userMono = mono.doOnNext(u -> u.setUuid(uuid)).doOnSuccess(user -> userRepository.saveNoReturn(user));
// return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userMono,User.class);
}
private User createUser(ServerRequest serverRequest) {
User user = new User();
Optional<String> userId = serverRequest.queryParam("userId");
Optional<String> userName = serverRequest.queryParam("userName");
Optional<String> age = serverRequest.queryParam("age");
Optional<String> sex = serverRequest.queryParam("sex");
Optional<String> uuid = serverRequest.queryParam("uuid");
if (userId.isPresent()) user.setUserId(userId.get());
if (userName.isPresent()) user.setUserName(userName.get());
if (age.isPresent()) user.setAge(Integer.valueOf(age.get()));
if (sex.isPresent()) user.setSex(sex.get());
if (uuid.isPresent()) {
user.setUuid(uuid.get());
} else {
user.setUuid(generateUUID());
}
return user;
}
// 更新用户
public Mono update(ServerRequest serverRequest) {
Mono<User> mono = serverRequest.bodyToMono(User.class);
User user = createUser(serverRequest);
return ServerResponse.ok().body(userRepository.update(user),Boolean.class);
}
// 删除用户
public Mono delete(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return ServerResponse.ok().body(userRepository.delete(uuid),Long.class);
}
private String generateUUID() {
return UUID.randomUUID().toString().replace("-","");
}
}
三、总结
在这次学习中我觉得遇到的最大的问题就是如何从ServerRequest
将请求体映射成具体对象类型的问题,不知道是不是需要自己去实现一个BodyExtractors
,其实使用注解方式和mvc没什么差别。使用函数式感觉是要比自己想象中复杂了一点,之前使用vert.x感觉都还好,只是使用了很多lambda,代码看上去不太美观,另外就是debug比较困难(webflux也存在这个问题)。
最后:自己在微信开了一个个人号:
超超学堂
,都是自己之前写过的一些文章,另外关注还有Java免费自学资料,欢迎大家关注。