Spring Reactor Tutorial
总览
在本文中,我们将介绍Spring Reactor项目及其重要性。 这个想法是利用Reactive Streams规范在JVM上构建非阻塞的反应式应用程序。
利用这些知识,我们将构建一个简单的反应式应用程序,并将其与传统的阻止应用程序进行比较。
响应式应用程序是"热门新事物",许多应用程序都切换到该模型。 您可以在The Reactive Manifesto中阅读有关此内容的更多信息。
动机
常规API正在阻止
现代应用程序处理大量并发用户和数据。 摩尔定律不再像以前那样成立。 硬件功能虽然在增加,但是却无法跟上性能非常重要的现代应用程序的步伐。
Java开发人员默认情况下编写阻止代码。 这就是API的设置方式。 另一个示例是传统的servlet(Tomcat)方法。 每个请求都保证有一个新线程,该线程等待整个后台进程完成以将响应发送回去。
这意味着我们的数据层逻辑默认情况下会阻止应用程序,因为线程闲置地等待响应。 在我们等待响应返回之前,不将这些线程用于其他目的是浪费的。
信用:http://projectreactor.io/learn small>
注意:如果我们的资源有限或某个过程需要太多时间来执行,则可能会出现问题。
异步静止块
在Java中,您可以使用Callbacks and Futures异步编写代码。 然后,您可以在以后的某个时间点获取并加入线程并处理结果。 Java 8为我们引入了一个新类CompletableFuture,它使协调这些事情变得更加容易。
它以一种简单的方式工作-当一个进程结束时,另一个进程开始。 在第二个步骤结束之后,将结果合并到第三个过程中。
这使协调您的应用程序变得容易得多,但是在创建线程并等待调用
信用:http://projectreactor.io/learn small>
反应式编程
我们想要的是异步和非阻塞。 来自Netflix,Pivotal,RedHat等公司的一组开发人员聚集在一起,并商定了一个名为"反应流规范"的项目。
Project Reactor是Spring的The Reactive Specification的实现,它特别受Spring Webflux模块的青睐,尽管您可以将其与RxJava等其他模块一起使用。
这个想法是使用发布者和订阅者与Backpressure异步操作。
在这里,我们将介绍几个新概念! 让我们一一解释:
发布者-发布者是数量可能不受限制的元素的提供者。
订阅服务器-订阅服务器侦听该发布服务器,询问新数据。 有时,它也被称为消费者。
背压-订阅服务器让发布服务器当时可以处理多少个请求的能力。 因此,负责数据流的是订阅服务器,而不是发布服务器,因为发布服务器仅提供数据。
Reactor项目提供两种类型的发布者。 这些被认为是Spring Webflux的主要构建基块:
通量-是产生
Mono-是产生
开发反应性应用程序
考虑到以上所有内容,让我们跳入创建一个简单的Web应用程序,并利用这一新的响应式范例的优势!
与往常一样,从框架式Spring Boot项目开始的最简单方法是使用Spring Initializr。 选择您首选的Spring Boot版本,并添加" Reactive Web"依赖项。 之后,将其生成为Maven项目,一切就绪!
让我们定义一个简单的POJO-
1 2 3 4 |
定义发布者
伴随它,让我们定义一个具有适当映射的简单REST控制器:
1 2 3 4 5 6 7 8 | @RestController public class GreetReactiveController { @GetMapping("/greetings") public Publisher<Greeting> greetingPublisher() { Flux<Greeting> greetingFlux = Flux.<Greeting>generate(sink -> sink.next(new Greeting("Hello"))).take(50); return greetingFlux; } } |
调用Flux < T > .generate() t>将创建
顾名思义,take()方法将仅从流中获取前50个值。
重要的是要注意,该方法的返回类型是异步类型
要测试此终结点,请将浏览器导航到http:// localhost:8080 / greetings或在命令行上使用curl客户端-
系统将提示您类似以下内容的响应:
这看起来没什么大不了的,我们可以简单地返回
但是要再次注意,我们返回的是
假设我们有一个发布者返回了超过一千条甚至更多的记录。 考虑一下框架必须做什么。 它为对象提供了
如果我们在Spring MVC中使用传统方法,则这些对象将继续在您的RAM中累积,并且一旦它收集了所有东西,便会将其返回给客户端。 这可能超出了我们的RAM容量,同时也阻止了其他任何操作的处理。
当我们使用Spring Webflux时,整个内部动力学都会改变。 框架开始从发布者那里订阅这些记录,并序列化每个项目并将其分块发送回客户端。
我们异步进行操作,而无需创建太多线程并重用正在等待的线程。 最好的部分是您不必为此做任何额外的事情。 在传统的Spring MVC中,我们可以通过返回
服务器发送的事件
自服务器到达事件以来一直使用的另一个发布者。
这些事件使网页可以实时从服务器获取更新。
让我们定义一个简单的反应式服务器:
1 2 3 4 5 6 7 | @GetMapping(value ="/greetings/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Publisher<Greeting> sseGreetings() { Flux<Greeting> delayElements = Flux .<Greeting>generate(sink -> sink.next(new Greeting("Hello @" + Instant.now().toString()))) .delayElements(Duration.ofSeconds(1)); return delayElements; } |
另外,我们可以定义以下内容:
1 2 3 4 5 6 | @GetMapping(value ="/greetings/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) Flux<Greeting> events() { Flux<Greeting> greetingFlux = Flux.fromStream(Stream.generate(() -> new Greeting("Hello @" + Instant.now().toString()))); Flux<Long> durationFlux = Flux.interval(Duration.ofSeconds(1)); return Flux.zip(greetingFlux, durationFlux).map(Tuple2::getT1); } |
这些方法产生
请注意,在第一个示例中,我们使用
"那我应该使用哪种返回类型?"
建议在
这两个示例重点介绍了创建延迟的服务器发送事件的两种方法:
导航到http:// localhost:8080 / greetings / sse或在命令行上使用curl客户端,您将看到类似以下内容的响应:
定义消费者
现在让我们看看它的消费者方面。 值得注意的是,您不需要具有反应式发布者即可在消费方使用反应式编程:
1 2 3 4 5 | public class Person { private int id; private String name; // Constructor with getters and setters } |
然后我们有一个带有单个映射的传统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @RestController public class PersonController { private static List<Person> personList = new ArrayList<>(); static { personList.add(new Person(1,"John")); personList.add(new Person(2,"Jane")); personList.add(new Person(3,"Max")); personList.add(new Person(4,"Alex")); personList.add(new Person(5,"Aloy")); personList.add(new Person(6,"Sarah")); } @GetMapping("/person/{id}") public Person getPerson(@PathVariable int id, @RequestParam(defaultValue ="2") int delay) throws InterruptedException { Thread.sleep(delay * 1000); return personList.stream().filter((person) -> person.getId() == id).findFirst().get(); } } |
我们初始化了类型为
尽管
如果您有兴趣阅读有关Java Streams的更多信息,请阅读本文!
让我们继续前进,创造我们的消费者。 就像发布者一样,我们可以使用Spring Initializr轻松做到这一点:
我们的生产者应用程序正在端口
首先让我们使用传统的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class CallPersonUsingRestTemplate { private static final Logger logger = LoggerFactory.getLogger(CallPersonUsingRestTemplate.class); private static RestTemplate restTemplate = new RestTemplate(); static { String baseUrl ="http://localhost:8080"; restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl)); } public static void main(String[] args) { Instant start = Instant.now(); for (int i = 1; i <= 5; i++) { restTemplate.getForObject("/person/{id}", Person.class, i); } logTime(start); } private static void logTime(Instant start) { logger.debug("Elapsed time:" + Duration.between(start, Instant.now()).toMillis() +"ms"); } } |
让我们运行它:
正如预期的那样,花了10秒钟多一点,这就是Spring MVC默认工作的方式。
在这个时代,等待一页结果超过10秒是不可接受的。 这是保留客户/客户与由于等待太久而丢失客户/客户之间的区别。
Spring Reactor引入了一个新的Web客户端来发出Web请求,称为WebClient。 与RestTemplate相比,此客户端具有更多的功能感,并且具有完全的反应性。 它包含在
这次,我们使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class CallPersonUsingWebClient_Step1 { private static final Logger logger = LoggerFactory.getLogger(CallPersonUsingWebClient_Step1.class); private static String baseUrl ="http://localhost:8080"; private static WebClient client = WebClient.create(baseUrl); public static void main(String[] args) { Instant start = Instant.now(); for (int i = 1; i <= 5; i++) { client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class); } logTime(start); } private static void logTime(Instant start) { logger.debug("Elapsed time:" + Duration.between(start, Instant.now()).toMillis() +"ms"); } } |
在这里,我们通过传递
最终,我们要求Spring将响应映射到
并没有发生任何意外。
这是因为我们没有订阅。 整个事情都推迟了。 它是异步的,但是直到我们调用
让我们更改主要方法并添加订阅:
1 2 3 | for (int i = 1; i <= 5; i++) { client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class).subscribe(); } |
添加方法会提示我们所需的结果:
发送了请求,但
我们可以通过在方法调用的末尾链接
1 2 3 | for (int i = 1; i <= 5; i++) { client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class).block(); } |
结果:
这次我们确实收到了每个人的回复,尽管花费了10秒钟以上。 这违反了应用程序具有反应性的目的。
解决所有这些问题的方法很简单:我们列出一个类型为
1 2 3 4 5 | List<Mono<Person>> list = Stream.of(1, 2, 3, 4, 5) .map(i -> client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class)) .collect(Collectors.toList()); Mono.when(list).block(); |
结果:
这就是我们的目标。 这次,即使有巨大的网络延迟,也只花了两秒钟多的时间。 这极大地提高了我们应用程序的效率,确实改变了游戏规则。
如果仔细观察线程,Reactor会重用它们而不是创建新线程。 如果您的应用程序在短时间内处理了许多请求,这真的很重要。
结论
在本文中,我们讨论了对反应式编程的需求及其Spring的实现-Spring Reactor。
之后,我们讨论了内部使用Reactor的Spring Webflux模块以及诸如Publisher和Subscriber之类的涵盖概念。 基于此,我们构建了一个应用程序,该应用程序将数据发布为响应流,并在另一个应用程序中使用它。
该教程的源代码可以在Github上找到。