1、Spring 响应式编程
1.1、早期响应式解决方案
响应式编程是构建响应式系统的主要候选方案。Spring 4.x 引入了 ListenableFuture 类,它扩展了 Java Future,并且可以基于 HTTP 请求实现异步执行操作。但是只有少数 Spring 4.x 组件支持新的 Java 8 CompletableFuture,后者引入了一些用于组合异步执行的简洁方法。
Spring 框架还提供了其他一些基础架构,它们对构建我们的响应式应用程序非常有用。
1.1.1、观察者模式
听着感觉好像观察者模式似乎与响应式编程无关。但是,经过一些小修改,它定义了响应式编程的基础。
观察者模式拥有一个主题(subject),其中包含该模式的依赖者列表,这些依赖者被称为观察者(Observer)。
主题通常通过调用自身的一个方法将状态变化通知观察者。在基于事件处理的系统中此模式至关重要。观察者模式是 MVC(模型−视图−控制器)模式的重要组成部分。

Observe 接口:
package blnp.net.cn.jvm.demos.webflux;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:12
*/
public interface Observer {
void observe(String event);
}
Observe 实现:
package blnp.net.cn.jvm.demos.webflux;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:14
*/
public class ConcreteObserverA implements Observer {
@Override
public void observe(String event) {
System.out.println(getClass().getCanonicalName() + " --- " + event);
}
}
package blnp.net.cn.jvm.demos.webflux;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:14
*/
public class ConcreteObserverB implements Observer {
@Override
public void observe(String event) {
System.out.println(getClass().getCanonicalName() + " --- " + event);
}
}
Subject 接口:
package blnp.net.cn.jvm.demos.webflux;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:10
*/
public interface Subject {
/**
* 用途:注册观察者
* @author liaoyibin
* @since 9:11 2024/10/9
* @params [observer]
* @param observer
* @return void
**/
void registerObserver(Observer observer);
/**
* 用途:解绑观察者
* @author liaoyibin
* @since 9:11 2024/10/9
* @params [observer]
* @param observer
* @return void
**/
void unregisterObserver(Observer observer);
/**
* 用途:通知事件变更
* @author liaoyibin
* @since 9:11 2024/10/9
* @params [event]
* @param event
* @return void
**/
void notifyObservers(String event);
}
Subject 实现:
package blnp.net.cn.jvm.demos.webflux;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:16
*/
public class ConcreteSubject implements Subject {
/**
* 保证Set是线程安全的
**/
private Set<Observer> observers = new CopyOnWriteArraySet<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String event) {
observers.forEach(observer -> observer.observe(event));
}
}
Main 单元测试:
package blnp.net.cn.jvm.demos.webflux;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:17
*/
public class ConcreteMain {
public static void main(String[] args) {
Subject subject = new ConcreteSubject();
Observer observer1 = new ConcreteObserverA();
Observer observer2 = new ConcreteObserverB();
subject.registerObserver(observer1);
subject.registerObserver(observer2);
subject.notifyObservers("hello tom");
System.out.println("==================================");
subject.unregisterObserver(observer1);
subject.notifyObservers("great tom cat");
}
}
为了在多线程场景中确保线程安全,使用 CopyOnWriteArraySet,这是一个线程安全的 Set 实现,它在每次 update 操作发生时都会创建元素的新副本。
更新 CopyOnWriteArraySet中的内容相对代价较高,当容器包含大量元素时尤为如此。但是,订阅者列表通常不会经常更改,因此对于线程安全的 Subject 实现来说,这是一个相当合理的选择。
1.1.2、观察者模式使用
在不需要取消订阅的情况下,我们可以活用 Java 8 特性,用 lambda 替换 Observer 实现类。下面编写相应的测试用例:
@Test
public void subjectLeveragesLambdas() {
Subject subject = new ConcreteSubject();
subject.registerObserver(e -> System.out.println("A: " + e));
subject.registerObserver(e -> System.out.println("B: " + e));
subject.notifyObservers("This message will receive A & B");
// ...
}
在有很多观察者处理明显延迟的事件(由下游处理引入)时,我们可以使用其他线程或线程池(thread pool)并行传播消息。基于这种处理方式可以得出 notifyObservers 方法的下一个实现:
package blnp.net.cn.jvm.demos.webflux;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* <p></p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 9:16
*/
public class ConcreteSubject implements Subject {
/**
* 保证Set是线程安全的
**/
private Set<Observer> observers = new CopyOnWriteArraySet<>();
/**
* 线程池
**/
private final ExecutorService executorService = Executors.newCachedThreadPool();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String event) {
//v1 实现执行
//observers.forEach(observer -> observer.observe(event));
//v2 实现执行
observers.forEach(
observer -> executorService.submit(() -> observer.observe(event))
);
}
}
这些方案通常不是最高效的,并且很可能隐藏着 bug。例如,我们可能忘记限制线程池大小,并最终导致 OutOfMemoryError。因为每个线程在 Java 中消耗大约 1 MB,典型的 JVM 应用程序有可能创建几千个线程来耗尽所有可用内存。
🍕知识拓展:
为了防止资源滥用,我们可以限制线程池大小并将应用程序的活跃度(liveness)属性设置为violate。当所有可用线程试图将某些事件推送到同一个缓慢的 Observer 时,就会出现这种情况。在这里,我们只是初步暴露了可能发生的潜在问题。
public ThreadPoolExecutor(
//线程池的核心线程数量
int corePoolSize,
//线程池的最大线程数
int maximumPoolSize,
//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
long keepAliveTime,
//时间单位
TimeUnit unit,
//任务队列,用来储存等待执行任务的队列
BlockingQueue < Runnable > workQueue,
//线程工厂,用来创建线程,一般默认即可
ThreadFactory threadFactory,
//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
RejectedExecutionHandler handler)
因此,当需要支持多线程的 Observer 模式时,最好使用经过实战验证的库。java.util 包中的观察者模式从 JDK 1.0 发布的。如果查看源代码,会发现一个非常简单的实现,它与前面的实现非常相似。因为这些类是在 Java 泛型(Java generics)之前引入的,所以它们操作Object 类型的事件,是类型不安全的。
此外,这种实现效率不高,尤其是在多线程环境中。这些类在 Java 9 中已被弃用。在开发应用程序时,可能使用观察者模式的手工自定义实现。这能够对事件源和观察者进行解耦。但是,需要考虑许多对现代多线程应用程序至关重要的方面,包括错误处理、异步执行、线程安全、高性能需求等。
1.1.3、基于@EventListener注解的发布和订阅模式
在很长一段时间内,Spring 框架有自己的观察者模式实现,这被广泛用于跟踪应用程序的生命周期事件。从 Spring 4.2 开始,不仅用于处理应用程序事件,还用于处理业务逻辑事件。Spring 的@EventListener 注解实现事件分发,ApplicationEventPublisher 类实现事件发布。
@EventListener 和 ApplicationEventPublisher 实现了发布−订阅模式(Publish-Subscribepattern),它可以被视为观察者模式的变体。在发布−订阅模式中,发布者和订阅者不需要彼此了解,如下图所示:
发布−订阅模式在发布者和订阅者之间提供了额外的间接层。订阅者知道广播通知的事件通道,但通常不关心发布者的身份。此外,每个事件通道中可能同时存在几个发布者。
事件通道(event channel,也被称为消息代理或事件总线)可以额外过滤传入的消息并在订阅者之间分发它们。过滤和路由的执行可以基于消息内容或消息主题,也可以同时基于这两者。因此,基于主题的系统中的订阅者将接收发布到自身感兴趣主题的所有消息。
@EventListener 注解支持基于主题和基于内容的路由。消息类型作为主题的角色;condition 属性基于内容进行事件的路由,事件路由处理基于 Spring 表达式语言(SpEL)。
1.1.4、使用@EventListener注解构建应用程序
实现一个简单的 Web 服务,用于显示房间当前的温度。设置一个温度传感器,它不时地将当前的摄氏温度通过事件发送出来。使用随机数生成器模拟温度传感器。
为了使应用程序遵循响应式设计,不使用旧的拉模型获取数据。使用 WebSocket 和服务器发送事件(Server-Sent Events,SSE)。SSE 能使客户端从服务器接收自动更新,通常用于向浏览器发送消息更新或连续数据流。使用 EventSource 的 JavaScript API,请求特定 URL 并接收事件流。在通信发生问题时,EventSource 默认自动重连。
1、实现业务逻辑
项目完成目录结构:
Maven:
<?xml version="1.0" encoding="UTF-8"?>
<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 https://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.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.blnp.net</groupId>
<artifactId>pubSubDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>pubSubDemo</name>
<description>关于发布订阅模式 @EventListenner 注解应用</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20200518</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
事件对象:
package com.blnp.net.pubSub.dto;
import lombok.Data;
/**
* <p>温度传感器对象</p>
*
* @author lyb 2045165565@qq.com
* @version 1.0
* @since 2024/10/9 11:27
*/
@Data
public class Temperature {
/**
* 实时温度值
**/
private final double value;
}
业务程序:TemperatureSensor类模拟传感器,并使用@Component 注解
package com.blnp.net.pubSub.service;
import com.blnp.net.pubSub.dto.Temperature;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* <p>&