Spring Reactive响应式编程-WebFlux编程实战

springboot2 webflux 响应式编程学习路径 : https://zhuanlan.zhihu.com/p/36160025

先学习jdk8的lambda表达式和stream流编程,了解函数式编程的知识点和思想,接着学习jdk9的响应式流flux,理解响应式流概念,理解背压和实现机制。这2者学好之后,很容易理解webflux的基石reactor,再学习webflux就水到渠成了!

Reactive Stream

jdk9的响应式流

就是Reactive Stream,也就是flow。其实和jdk8的stream没有一点关系。说白了就一个发布-订阅模式,一共只有4个接口,3个对象,非常简单清晰
image

什么是背压?

背压是指订阅者能和发布者交互,可以调节发布者发布数据的速率,解决把订阅者压垮的问题

我们重点理解背压在jdk9里面是如何实现的。关键在于发布者Publisher的实现SubmissionPublishersubmit方法是block方法。订阅者会有一个缓冲池,默认为Flow.defaultBufferSize() = 256。当订阅者的缓冲池满了之后,发布者调用submit方法发布数据就会被阻塞,发布者就会停(慢)下来;订阅者消费了数据之后(调用Subscription.request方法),缓冲池有位置了,submit方法就会继续执行下去,就是通过这样的机制,实现了调节发布者发布数据的速率,消费得快,生成就快,消费得慢,发布者就会被阻塞,当然就会慢下来了

package jdk9;


import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class FlowDemo {

    public static void main(String[] args) {
        // 1. 定义发布者,发布的数据类型式Integer
        // 直接使用jdk自带的SubmissionPublisher,它实现了Publisher接口
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        // 2. 定义订阅者
      Flow.Subscriber<Integer> subscriber = new Flow.Subscriber<>() {

            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                // 保存订阅关系,需要用它来给发布者响应
                this.subscription = subscription;

                // 请求一个数据
                this.subscription.request(1);
            }

            @Override
            public void onNext(Integer item) {
                // 接受到一个数据,处理
                System.out.println("接收到数据:" + item);
                // 处理完调用request再请求一个数据
                this.subscription.request(1);

                // 或者已经达到了目标,调用cancel告诉发布者不再接受数据了
                // this.subscription.cancel()
            }

            @Override
            public void onError(Throwable throwable) {
                // 出现了异常(例如处理数据的时候产生了异常)
                throwable.printStackTrace();

                // 我们可以告诉发布者,后面不接受数据了
                this.subscription.cancel();
            }

            @Override
            public void onComplete() {
                // 全部数据处理完了(发布者关闭了)
                System.out.println("处理完了!");
            }
        };

        // 3. 发布者和订阅者 建立订阅关系
        publisher.subscribe(subscriber);

        // 4. 生产数据,并发布
        // 这里忽略数据生产过程
        int data = 111;
        publisher.submit(data);
        publisher.submit(222);
        publisher.submit(333);

        // 5. 结束后,关闭发布者
        // 正式环境应该放入finally或者使用 try-resource 确保关闭
        publisher.close();

        // 主线程延迟停止,否则数据没有消费就退出
        try {
            Thread.currentThread().join(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

自定义 Processer(中间处理器,相当于是发布者的同时又是订阅者)代码示例

import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

/**
 * Processor, 需要继承SubmissionPublisher并实现Processor接口
 *
 * 输入源数据 integer, 过滤掉小于0的, 然后转换成字符串发布出去
 */
class MyProcessor extends SubmissionPublisher<String>
        implements Flow.Processor<Integer, String> {

    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        // 保存订阅关系, 需要用它来给发布者响应
        this.subscription = subscription;

        // 请求一个数据
        this.subscription.request(1);
    }

    @Override
    public void onNext(Integer item) {
        // 接受到一个数据, 处理
        System.out.println("处理器接受到数据: " + item);

        // 过滤掉小于0的, 然后发布出去
        if (item > 0) {
            this.submit("转换后的数据:" + item);
        }

        // 处理完调用request再请求一个数据
        this.subscription.request(1);

        // 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
        // this.subscription.cancel();
    }

    @Override
    public void onError(Throwable throwable) {
        // 出现了异常(例如处理数据的时候产生了异常)
        throwable.printStackTrace();

        // 我们可以告诉发布者, 后面不接受数据了
        this.subscription.cancel();
    }

    @Override
    public void onComplete() {
        // 全部数据处理完了(发布者关闭了)
        System.out.println("处理器处理完了!");
        // 关闭发布者
        this.close();
    }
}

public class FlowDemoWithProcessor {

    public static void main(String[] args) throws Exception {
        // 1. 定义发布者, 发布的数据类型是 Integer
        // 直接使用jdk自带的SubmissionPublisher
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        // 2. 定义处理器, 对数据进行过滤, 并转换为String类型
        MyProcessor processor = new MyProcessor();

        // 3. 发布者 和 处理器 建立订阅关系
        publisher.subscribe(processor);

        // 4. 定义最终订阅者, 消费 String 类型数据
        Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() {

            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                // 保存订阅关系, 需要用它来给发布者响应
                this.subscription = subscription;

                // 请求一个数据
                this.subscription.request(1);
            }

            @Override
            public void onNext(String item) {
                // 接受到一个数据, 处理
                System.out.println("接受到数据: " + item);

                // 处理完调用request再请求一个数据
                this.subscription.request(1);

                // 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
                // this.subscription.cancel();
            }

            @Override
            public void onError(Throwable throwable) {
                // 出现了异常(例如处理数据的时候产生了异常)
                throwable.printStackTrace();

                // 我们可以告诉发布者, 后面不接受数据了
                this.subscription.cancel();
            }

            @Override
            public void onComplete() {
                // 全部数据处理完了(发布者关闭了)
                System.out.println("处理完了!");
            }

        };

        // 5. 处理器 和 最终订阅者 建立订阅关系
        processor.subscribe(subscriber);

        // 6. 生产数据, 并发布
        // 这里忽略数据生产过程
        publisher.submit(-111);
        publisher.submit(111);

        // 7. 结束后 关闭发布者
        // 正式环境 应该放 finally 或者使用 try-resouce 确保关闭
        publisher.close();

        // 主线程延迟停止, 否则数据没有消费就退出
        Thread.currentThread().join(1000);
    }

}

运行结果:
image

发布者生产的数据会存储到默认缓冲池的数组中发送给订阅者,默认缓冲池是256个长度,当缓冲区满了而订阅者还没来的及处理数据时,发布者就会被block(阻塞)而停止生产数据,直到订阅者消费完缓冲区中的数据而产生空位时发布者才会重新生成新的数据

Spring WebFlux

初识Spring WebFlux

Spring WebFlux 是 Spring Framework 5.0中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。

官方地址: https://spring.io/reactive
image

架构 说明
spring-webmvc + Servlet + Tomcat 命令式的、同步阻塞的
spring-webflux + Reactor + Netty 响应式的、异步非阻塞的

所谓异步非阻塞是针对服务端而言的,是说服务端可以充分利用CPU资源去做更多事情,这与客户端无关,客户端该怎么请求还是怎么请求。

架构 说明
Reactive Streams 用于构建高吞吐量、低延迟应用的规范
Reactor 基于Reactive Streams 规范的实现,它是一个完全非阻塞的基础,且支持背压
Spring WebFlux 基于Reactor实现了完全异步非阻塞的一套web框架,是一套响应式堆栈

编写响应式代码之前,我们还需要了解2个重要的概念,就是异步servletSSE(server-sent events)

异步servlet

学习异步servlet我们最重要的了解同步servlet阻塞了什么?为什么需要异步servlet?异步servlet能支持高吞吐量的原理是什么?

  • 同步servlet
    servlet容器(如tomcat)里面,每处理一个请求会占用一个线程,同步servlet里面,业务代码处理多久,servlet容器的线程就会等(阻塞)多久,而servlet容器的线程是由上限的,当请求多了的时候servlet容器线程就会全部用完,就无法再处理请求(这个时候请求可能排队也可能丢弃,得看如何配置),就会限制了应用的吞吐量!

  • 异步servlet
    servlet容器的线程不会傻等业务代码处理完毕,而是直接返回(继续处理其他请求),给业务代码一个回调函数(asyncContext.complete()),业务代码处理完了再通知我!这样就可以使用少量的线程处理更加高的请求,从而实现高吞吐量!

代码示例:

  • 同步servlet
@WebServlet(name = "SyncServlet", urlPatterns="/SyncServlet")
public class SyncServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        long t1 = System.currentTimeMillis();
        // 执行业务代码
        doSomeTing(request, response);

        System.out.println("sync use:" + (System.currentTimeMillis() - t1));
    }

    private void doSomeTing(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // 模拟耗时操作
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        response.getWriter().append("done");
    }
}
  • 异步servlet
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@WebServlet(name = "AsyncServlet", urlPatterns = "/AsyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        long t1 = System.currentTimeMillis();

        // 1.开启异步
        AsyncContext asyncContext = request.startAsync();

        // 2.把我们要执行的代码放到一个独立的线程中,多线程/线程池
        CompletableFuture.runAsync(() ->
                // 执行业务代码
        {
            try {
                doSomeTing(asyncContext, asyncContext.getRequest(), asyncContext.getResponse());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("async use:" + (System.currentTimeMillis() - t1));
    }

    private void doSomeTing(AsyncContext asyncContext, ServletRequest request, ServletResponse response) throws IOException {

        // 模拟耗时操作
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        response.getWriter().append("async done");

        // 3.业务代码处理完毕,通知结束
        asyncContext.complete();
    }
}

运行上面代码,业务代码花了5秒,但servlet容器的线程几乎没有任何耗时。而如果是同步servlet的,线程就会傻等5秒,这5秒内这个线程只处理了这一个请求/。

异步servlet在处理耗时任务时会立马执行完成并且将任务放到另一个线程中去运行,这样我们的这个servlet主线程就不会被阻塞从而能够去执行其他的任务

SSE(Server-Sent Events)

响应式流里面,可以多次返回数据(其实和响应式没有关系),使用的技术就是H5的SSE。我们学习技术,API的使用只是最初级也是最简单的,更加重要的是需要知其然并知其所以然,否则你只能死记硬背不用就忘!我们不满足在spring里面能实现sse效果,更加需要知道spring是如何做到的。其实SSE很简单,我们花一点点时间就可以掌握,我们在纯servlet环境里面实现。我们看代码,这里一个最简单的示例。

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@WebServlet(name = "SSE", urlPatterns = "/SSE")
public class SSE extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");

        for (int i = 0; i < 5; i++) {
            // 指定事件标识
            response.getWriter().write("event:me\n");
            // 格式:data: + 数据 + 2个回车
            response.getWriter().write("data:" + i + "\n\n");
            response.getWriter().flush();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

关键是ContentType 是 "text/event-stream",然后返回的数据有固定的要求格式即可。
如果我们想要在前端接受和使用事件流,可以使用以下方式

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
</head>
<body>
<script type="text/javascript">
	// 初始化,参数为url
	// 依赖H5
	var sse = new EventSource("SSE")

	// 监听消息并打印
	sse.onmessage = function (evt) {
	    console.log("message", evt.data, evt)
	}

	// 如果指定了事件标识需要用这种方式来进行监听事件流
	sse.addEventListener("me", function (evt) {
	    console.log("me event", evt.data)
		// 事件流如果不关闭会自动刷新请求,所以我们需要根据条件手动关闭
		if (evt.data == 3) {
		    sse.close()
		}
	})
</script>
</body>
</html>

使用场景:服务器向客户端推送数据,例如聊天室

WebFlux完整案例

搭建项目

  1. 添加mongodb-reactive依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
  1. 添加mongodb注解
@SpringBootApplication
@ServletComponentScan("com.javaming.study.webflux.servlet")
// 设置开启mongodb响应式存储
@EnableReactiveMongoRepositories
public class SpringWebfluxApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringWebfluxApplication.class, args);
    }

}
  1. 添加User对象
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "user")
@Data
public class User {
    
    @Id
    private String id;
    
    private String name;
    
    private int age;
}
  1. 新建user的数据库操作对象UserRepository
import com.javaming.study.webflux.domain.mongo.User;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends ReactiveMongoRepository<User, String> {

}
  1. 新建Controller
@RestController
@RequestMapping("/user")
public class UserController {

    private final UserRepository userRepository;

    /**
     * 构造函数的方式注入(官方推荐,降低耦合)
     */
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/")
    public Flux<User> getAll() {
        return userRepository.findAll();
    }

    /**
     * 推荐新增另一个相同的方法通过流的方式获取数据
     * @return
     */
    @GetMapping(value = "/stream/all", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<User> streamGetAll() {
        return userRepository.findAll();
    }

}
  1. 安装和启动mongodb

RouterFunction模式

webflux的另一种开发模式,和以前的Controller进行对应

  1. HandlerFunction(输入ServerRequest返回ServerResponse)
@Component
public class UserHandler {

    private final UserRepository userRepository;

    public UserHandler(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    /**
     * 得到所有用户
     * @param request
     * @return
     */
    public Mono<ServerResponse> getAllUser(ServerRequest request) {
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
                .body(this.userRepository.findAll(), User.class);
    }

    /**
     * 创建用户
     * @param request
     * @return
     */
    public Mono<ServerResponse> createUser(ServerRequest request) {
        Mono<User> user = request.bodyToMono(User.class);
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
                .body(this.userRepository.saveAll(user), User.class);
    }


    /**
     * 根据id删除用户
     * @param request
     * @return
     */
    public Mono<ServerResponse> deleteUserById(ServerRequest request) {

        String id = request.pathVariable("id");
        return this.userRepository.findById(id)
                .flatMap(user -> this.userRepository.delete(user).then(ServerResponse.ok().build()))
                .switchIfEmpty(ServerResponse.notFound().build());
    }
}
  1. 编写路由类 RouterFunction(请求URL和HandlerFunction对应起来)
@Configuration
public class AllRouters {

    @Bean
    RouterFunction<ServerResponse> userRouter(UserHandler userHandler) {
        return RouterFunctions.nest(
                // 相当于类上面的@RequestMapping("/user")
                RequestPredicates.path("/user"),
                RouterFunctions
                        // 相当于类里面的@GetMapping("/")
                        // 得到所有用户
                        .route(RequestPredicates.GET("/"),
                                userHandler::getAllUser)
                        // 创建用户
                        .andRoute(RequestPredicates.POST("/").
                                        and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
                                userHandler::createUser)
                        // 删除用户
                        .andRoute(RequestPredicates.DELETE("/{id}"),
                                userHandler::deleteUserById)
        );
    }
}

源码下载地址:https://gitee.com/javaming/springboot-webflux

posted @ 2021-11-03 16:39  狻猊的主人  阅读(2521)  评论(0编辑  收藏  举报