WebFlux学习入门

其实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的一个对比,共同点和不同点还是非常清晰的。

mvc和webflux.png

webflux可以使用两种编程模式,一种就是大家都非常熟悉的注解的方式,即使用@Controller@RestController,另一种是函数式编程,这点和vert.x还是比较类似的。下面会通过两种不同的方式做一个简单的练习。webflux使用的话需要有相关的响应式数据源,因为传统的JDBC是阻塞的,因此必须使用非阻塞(响应式)的数据源,目前spring支持的有Redis、MongoDB、Cassandra等。因为我本地只安装了Redis,所以选择了Reactive Redis的依赖。新建一个spring boot项目,在选择依赖的时候Web选择Spring Reactive WebNoSQL选择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没有任何的区别,唯一的区别在于返回的对象是MonoFlux,简单点理解,返回单个数据就是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

图-2.png

然后通过调用添加数据的接口向Redis添加几条数据,之后调用其他查询接口或者修改接口都是正常的,和使用mvc没什么区别,就不再细述了。

二、使用函数式

函数式变成涉及到几个概念我觉得是比较重要的比如RouterFunctionHandlerFunction还有一个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的HttpServletRequestHttpServletResponse对应起来,都是封装用户的请求信息,其实和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,理论上没有问题,而且根据上面使用注解方式开发时这个方法是切实可行的。但是测试的时候就出现了问题,下面是我调用接口的请求参数,和使用注解开发一样的:

图-3.png

下面的是测试结果:
图-4.png

开始的时候我也很意外,没想到是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免费自学资料,欢迎大家关注。

二维码.jpg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351