响应式堆栈Web框架Spring WebFlux已添加到Spring 5.0中。 它是完全无阻塞的,支持事件驱动,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。 在这个春季的webflux教程中,我们将通过一个hello world
例子来学习反应式编程,webflux api等的基本概念。
1.反应式编程(Reactive Programming)
响应式编程是一种编程范例,可促进异步,非阻塞,事件驱动的数据处理方法。 响应式编程涉及将数据和事件建模为可观察的数据流,并实现数据处理例程以对这些流中的更改做出反应。
在深入研究响应式世界之前,首先要了解阻塞与非阻塞请求处理之间的区别。
1.1. 阻塞与非阻塞(异步)请求处理
1.1.1. 阻止请求处理
在传统的MVC应用程序中,当请求到达服务器时,将创建一个servlet线程。 它将请求委托给工作线程进行I / O操作(例如数据库访问等)。在工作线程忙时,servlet线程(请求线程)保持等待状态,因此被阻塞。 这也称为同步请求处理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3NtWQEf-1571798491919)(https://howtodoinjava.com/wp-content/uploads/2019/02/Blocking-request-processing.png)]
由于服务器对请求线程有数量上的限制,因此限制了服务器在最大服务器负载下处理该数量请求的能力。 它可能会影响性能并限制服务器功能的完全利用。
1.1.2。 非阻塞请求处理
在非阻塞或异步请求处理中,没有线程处于等待状态。 通常只有一个请求线程接收该请求。
所有传入的请求都带有事件处理程序和回调信息。 请求线程将传入的请求委托给线程池(通常为少量线程),该线程池将请求委托给其处理函数,并立即开始处理来自请求线程的其他请求。
处理程序功能完成后,池中的线程之一将收集响应并将其传递给回调函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zbQxTWsP-1571798491926)(https://howtodoinjava.com/wp-content/uploads/2019/02/Non-blocking-request-processing.png)]
线程的非阻塞性质有助于扩展应用程序的性能。 线程数量少意味着内存占用少,上下文切换也较少。
1.2.什么是反应式编程?
术语“反应性”是指围绕响应变化而构建的编程模型。 它是围绕发布者-订阅者模式(观察者模式)构建的。 在反应式编程中,我们请求资源并开始执行其他事情。 当数据可用时,我们会收到通知以及回调函数的数据通知。 在回调函数中,我们根据应用程序/用户需求处理响应。
要记住的重要一件事是背压。 在非阻塞代码中,控制事件的速率非常重要保护,可以保护消费者不被生产者的过度生产压垮。
响应式Web编程非常适合具有流数据的应用程序以及使用该数据并将其流传输给用户的客户端。 这对开发传统的CRUD应用程序不是很友好。 如果您想开发下一个具有海量数据的Facebook或Twitter,则响应式API可能正是您想要的。
2. 反应式流API(Reactive Streams API)
新的Reactive Streams API由Netflix,Pivotal,Lightbend,RedHat,Twitter和Oracle等工程师创建,现已成为Java 9的一部分。它定义了四个接口:
-
Publisher: 生产数据,供订阅者消费,只有一个方法 subscribe(Subscriber)
It has a single method:
public interface Publisher<T> { public void subscribe(Subscriber<? super T> s); }
-
Subscriber: 订阅生产者,接收数据(通过 onNext(T) 方法)、错误信息( onError(Throwable) 方法)、没有更多数据的信号( onComplete() ),在这些动作之前,publisher 会调用 onSubscription(Subscription)。subscribe(Subscriber)
It has four methods to handle various kind of responses received.
public interface Subscriber<T> { public void onSubscribe(Subscription s); public void onNext(T t); public void onError(Throwable t); public void onComplete(); }
-
Subscription: 是发布者和订阅者之间的连接,订阅者会通过它来请求更多的数据( request(long) ),或者中断连接( cancel() )。
It has a single method:
public interface Subscription<T> { public void request(long n); public void cancel(); }
-
Processor: 表示一个处理的阶段,包括订阅者
Subscriber
和发布者Publisher
和两者之间的协议public interface Processor<T, R> extends Subscriber<T>, Publisher<R> { }
Two popular implementations of reactive streams are RxJava (https://github.com/ReactiveX/RxJava) and Project Reactor (https://projectreactor.io/).
3. What is Spring WebFlux ?
Spring WebFlux是[Spring MVC](https://howtodoinjava.com/spring-mvc-tutorial/)的并行版本,并支持完全无阻塞的反应流。 它支持背压概念,并使用“ Netty”作为内置服务器来运行反应性应用程序。 如果您熟悉Spring MVC编程风格,则也可以轻松地使用webflux。
Spring Webflux使用项目反应器作为反应库。 Reactor是Reactive Streams库,因此,它的所有运算符都支持无阻塞背压。 它是与Spring紧密合作开发的。
Spring WebFlux大量使用两个发布者:
-
-
Mono
- 一个实体包装(0,1)
Mono<String> mono = Mono.just(``"Alex"``); Mono<String> mono = Mono.empty();
-
-
Flux
- 一种集合(0,n)
Flux<String> flux = Flux.just("A", "B", "C"); Flux<String> flux = Flux.fromArray(new String[]{"A", "B", "C"}); Flux<String> flux = Flux.fromIterable(Arrays.asList("A", "B", "C")); //To subscribe call method flux.subscribe();
在Spring WebFlux中,我们称为反应式API /函数,它们返回monos和fluxes,而您的控制器将返回monos和助fluxes。 当您调用返回monos或fluxes的API时,它将立即返回。 当它们可用时,函数调用的结果将通过 mono或者 flux传递给您。
要构建真正的非阻塞应用程序,我们必须致力于将其所有组件创建/使用为非阻塞程序,即客户端,控制器,中间服务甚至数据库。 如果其中之一是阻塞的请求,那么我们构建的应用程序将会失败。
4. 程序
这个程序将采用Spring boot 2,为了使它完全非阻塞,将使用mongodb作为背压数据库。
4.1. Maven dependencies
Include spring-boot-starter-webflux
, spring-boot-starter-data-mongodb-reactive
, spring-boot-starter-test
and reactor-test
dependencies.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.howtodoinjava</groupId>
<artifactId>spring-webflux-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-webflux-demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</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>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
4.2. Configurations
Webflux Configuration
WebFluxConfig.java
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer
{
}
MongoDb Configuration
MongoConfig.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
@Configuration
@EnableReactiveMongoRepositories(basePackages = "com.howtodoinjava.demo.dao")
public class MongoConfig extends AbstractReactiveMongoConfiguration
{
@Value("${port}")
private String port;
@Value("${dbname}")
private String dbName;
@Override
public MongoClient reactiveMongoClient() {
return MongoClients.create();
}
@Override
protected String getDatabaseName() {
return dbName;
}
@Bean
public ReactiveMongoTemplate reactiveMongoTemplate() {
return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName());
}
}
Application Configuration
AppConfig.java
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
public class AppConfig
{
@Bean
public static PropertyPlaceholderConfigurer getPropertyPlaceholderConfigurer()
{
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
ppc.setLocation(new ClassPathResource("application.properties"));
ppc.setIgnoreUnresolvablePlaceholders(true);
return ppc;
}
}
Properties file
application.properties
port=27017
dbname=testdb
Logging configuration
logback.xml
<configuration>
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
</pattern>
</encoder>
</appender>
<logger name="org.springframework" level="DEBUG"
additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Spring boot application
WebfluxFunctionalApp.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebfluxFunctionalApp {
public static void main(String[] args) {
SpringApplication.run(WebfluxFunctionalApp.class, args);
}
}
4.3. REST Controller
EmployeeController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.howtodoinjava.demo.model.Employee;
import com.howtodoinjava.demo.service.EmployeeService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@RequestMapping(value = { "/create", "/" }, method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public void create(@RequestBody Employee e) {
employeeService.create(e);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<Mono<Employee>> findById(@PathVariable("id") Integer id) {
Mono<Employee> e = employeeService.findById(id);
HttpStatus status = e != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
return new ResponseEntity<Mono<Employee>>(e, status);
}
@RequestMapping(value = "/name/{name}", method = RequestMethod.GET)
@ResponseBody
public Flux<Employee> findByName(@PathVariable("name") String name) {
return employeeService.findByName(name);
}
@RequestMapping(method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Flux<Employee> findAll() {
Flux<Employee> emps = employeeService.findAll();
return emps;
}
@RequestMapping(value = "/update", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public Mono<Employee> update(@RequestBody Employee e) {
return employeeService.update(e);
}
@RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.OK)
public void delete(@PathVariable("id") Integer id) {
employeeService.delete(id).subscribe();
}
}
4.4. Service classes
IEmployeeService.java
import com.howtodoinjava.demo.model.Employee;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IEmployeeService
{
void create(Employee e);
Mono<Employee> findById(Integer id);
Flux<Employee> findByName(String name);
Flux<Employee> findAll();
Mono<Employee> update(Employee e);
Mono<Void> delete(Integer id);
}
EmployeeService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.howtodoinjava.demo.dao.EmployeeRepository;
import com.howtodoinjava.demo.model.Employee;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class EmployeeService implements IEmployeeService {
@Autowired
EmployeeRepository employeeRepo;
public void create(Employee e) {
employeeRepo.save(e).subscribe();
}
public Mono<Employee> findById(Integer id) {
return employeeRepo.findById(id);
}
public Flux<Employee> findByName(String name) {
return employeeRepo.findByName(name);
}
public Flux<Employee> findAll() {
return employeeRepo.findAll();
}
public Mono<Employee> update(Employee e) {
return employeeRepo.save(e);
}
public Mono<Void> delete(Integer id) {
return employeeRepo.deleteById(id);
}
}
4.5. DAO repository
EmployeeRepository.java
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import com.howtodoinjava.demo.model.Employee;
import reactor.core.publisher.Flux;
public interface EmployeeRepository extends ReactiveMongoRepository<Employee, Integer> {
@Query("{ 'name': ?0 }")
Flux<Employee> findByName(final String name);
}
4.6. Model
Employee.java
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Scope(scopeName = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Document
public class Employee {
@Id
int id;
String name;
long salary;
//Getters and setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}
}
5. Demo
Start the application and check request and responses.
- HTTP POST http://localhost:8080/create
API Request 1
{
"id":1,
"name":"user_1",
"salary":101
}
- HTTP PUT http://localhost:8080/update
API Request 2
{
"id":2,
"name":"user_2",
"salary":102
}
- HTTP GET http://localhost:8080/
API Request
{
"id":2,
"name":"user_2",
"salary":103
}
- HTTP GET http://localhost:8080/
API Response
data:{"id":1,"name":"user_1","salary":101}
data:{"id":2,"name":"user_2","salary":102}
使用postman
进行测试。