初次接触
第一次了解webflux是在调研网关技术方案的时候,当时看了Netflix开源的zuul以及spring cloud gateway。zuul与spring cloud gateway最大的差异就在于zuul是基于BIO实现的,基于的Servlet 2.5的阻塞架构实现的,通俗理解就是一个请求对应线程池中的一个线程,技术非常成熟,也非常容易理解。但是spring cloud gateway确实基于响应式编程实现的,底层基于Netty和webflux。这个是我第一次接触webflux和响应式编程。
响应式编程及webflux
这里不想引入响应式编程的定义,只想通俗的来介绍一下本人对响应式编程的理解。
想象一下你在一家餐厅吃饭。在传统的编程模型中,服务员会给你一份菜单,你点菜之后,服务员会站在你旁边等你吃完,然后再去处理下一位顾客。这就像是传统的同步编程方式,每一个任务都必须等待前一个任务完成后才能开始。响应式编程就像是一个更聪明的餐厅服务方式。你点完菜后,服务员会去照顾其他客人,一旦你的菜准备好了,他们就会把它送到你的桌子上。这里的关键是服务员不需要一直等待,而是可以同时处理多个任务。如果你想要的菜暂时没了,服务员会告诉你需要等待,这就是所谓的"背压",它允许系统优雅地处理"客人"(也就是数据或任务)的高流量,而不是让整个"餐厅"(也就是系统)因为无法处理太多的订单而崩溃。
可以看到响应式编程其实非常适合网关的应用场景,可以非常优雅的解决网关场景下一些比较慢的请求拖垮整个系统的问题。能够极大的提升系统的吞吐量。并且随着springboot的升级,响应式编程再政改革java社区也变得越来越流行,而WebFlux是Spring Framework 5.0引入的一套新的响应式编程框架,也是spring社区在大力推广的新的编程模式。
应用场景
在开发网关的时候引入了spring cloud gateway,同步引入了webflux。在网关中需要对请求进行鉴权,鉴权的过程中涉及到请求body的读取,这个问题在网关开发中十分常见。在spring mvc的场景下这个问题非常容易解决,最常见的方法就是使用byte数组缓存body数据,这样就不会出现流读取异常的问题。简化的代码如下:
java
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
//使用一个ByteArray缓存请求数据,可以多次读取
private ByteArrayOutputStream cachedBytes;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
cachedBytes = new ByteArrayOutputStream();
ServletInputStream inputStream = null;
try {
inputStream = super.getInputStream();
IOUtils.copy(inputStream, cachedBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedServletInputStream(cachedBytes);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public void setByteArrayOutputStream(InputStream inputStream){
try {
cachedBytes.reset();
IOUtils.copy(inputStream,cachedBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
请求数据缓存之后就可以在拦截器中多次读取请求数据了,并且对请求数据进行处理。
在引入webflux之后,其基本的思路和spring mvc是一样的,就是找一个类来缓存请求数据。webflux用的是DataBuffer
。主要用于webflux的底层数据的缓冲操作。典型的操作如下所示:
java
public class CachingRequestBodyFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
// 将DataBuffer转换为字节数组
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
// 使用请求体的副本
String body = new String(bytes, StandardCharsets.UTF_8);
log.info("the request body is:{}", body);
// 在这里处理body内容...
// 将字节重新包装成DataBuffer,并重新设定请求体
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return Mono.just(buffer);
});
// 替换请求体,以便后续可以重新读取
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
// 继续过滤链
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
和spring mvc的处理思路非常一致。到这里一切都非常顺利,但是接下来坑就来了。以上过程有一个前提假设就是不出异常,但是在实际开发过程中怎么可能会不出异常。正常的思路异常抛出来了就catch异常、处理异常。代码如下:
java
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
// 使用请求体的副本
String body = new String(bytes, StandardCharsets.UTF_8);
log.info("the request body is:{}", body);
try{
checkAuth(body)
}catch(Exception ex){
//处理异常
}
// 将字节重新包装成DataBuffer,并重新设定请求体
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
以上代码在实际调测过程中你会发现,程序可能先执行下面Flux<DataBuffer> cachedFlux
语句,后执行try catch
。这个相当的反直觉,让习惯了代码串行执行的我们非常不适应。那具体原因是什么呢?
根因分析
其实有以上疑惑的原因还是没有深刻理解响应式编程的思想。响应式编程的根本思想是异步 。在filter方法中try catch没有执行是因为异常不是在声明它们的地方抛出的,而是在数据流的某个阶段由调度器在另一个线程中抛出的 。因此异常当然不会被拦截。那么我们应该如何处理webflux中的异常呢。webflux异常处理应该使用提供的错误处理操作符,例如onErrorReturn
, onErrorResume
, 或doOnError
。
- onErrorReturn:在出现错误时提供一个备选返回值。
- onErrorResume:在出现错误的时候提供一个备选Publisher继续进行流的处理。
- doOnError:不改变流的行为,只执行一些操作,比如打印日志。
修复
以上的代码进行如下调整:
less
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
//
})
.doOnError(e-> log.error("auth failed"))
.onErrorResume(throwable -> handleErrorResponse(exchange, throwable));
通过这样的方式才能处理流中的异常。
小结
响应式编程可以认为是一种新的编程思想,也可以认为是一种新的思维方式,与传统的java多线程的编程完全不一样。其本质是一种异步编程。它有很多优点,如提高了资源使用效率,尤其是在IO密集的场景下,能够大幅减少线程等待时间,提升吞吐量。但也有一些缺点,如学习曲线比较陡峭,代码不易调试等问题。尽管如此响应式编程仍然是未来java web开发主流方向之一。值得我们程序员花时间来学习和掌握,并将其用到实际的项目中。