1.概述
还记得去年看hystrix源码的时候,被rxjava搞的一头雾水,当时看到响应式这个词语,还是一脸懵的。前段时间学习了Vert.x,再到各大框架把java8的CompletableFuture用到极致,才意识到响应式编程的重要性。毕竟对于这个天生不支持coroutines的语言来讲,响应式还是比较有意义的。但也有利有弊,针对场景,仁者见仁。今天我们就来介绍一下Spring5推出的响应式web框架WebFlux。
2.响应式、异步、同步
- 同步调用:使用传统的阻塞编程在高并发情况下会浪费太多的线程资源,因为每一个请求都要占用一个或多个线程,如果遇到io阻塞的时候,线程会处于阻塞状态,高并发场景会导致创建太多线程,浪费内存资源,过多的线程调度也会浪费cpu资源。
- 异步非阻塞:一般通过回调处理执行结果,但是业务复杂,就会遇到回调地狱,难以回归代码。使用java的Future模式 ,对于get的操作还是阻塞的,算不上异步。
- 响应式:在代码可读性的前提下,支持了任务的异步化,可以提高系统的吞吐量,通常十几个线程就可以应对高并发场景。但是响应式并不会降低请求的处理时间,只会提高cpu利用率,避免不必要的线程切换。缺点就是代码调试复杂。一些threadlocal场景不适用。
背压:因为是基于推得模式,所以当反应式流数据的生产速度太快,消费者处理过慢,就会出现消息堆积。为了避免这个问题,就需要控制消息的生产速度。
借用spring文档的一句话:
If a publisher cannot slow down, it has to decide whether to buffer, drop, or fail.
3.源码追踪
创建服务器
springboot web模块提供了对web服务的支持。WebFlux默认使用netty作为web服务器,所以我们主要看看这方面的实现。
简单回顾一下,springboot通过run方法启动服务。在这个方法中会调用createApplicationContext方法去创建spring的ApplicationContext。spring的通过模版方法模式,可以提供多种context的实现。根据不同的环境选择不同的实现。
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, "
+ "please specify an ApplicationContextClass",
ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
webFlux采用REACTIVE模式,也就是初始化的context为AnnotationConfigReactiveWebServerApplicationContext。创建完成之后,会先调用prepare。再调用refreshContex去刷新context。这个过程就是一系列的初始化、监听的注册等操作。
重点在于refresh的onRefresh方法。
onRefresh方法
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start reactive web server",
ex);
}
}
private void createWebServer() {
WebServer localServer = this.webServer;
if (localServer == null) {
this.webServer = getWebServerFactory().getWebServer(getHttpHandler());
}
initPropertySources();
}
这里回调用createWebServer去创建web服务器。如果使用netty,会调用NettyReactiveWebServerFactory的getWebServer方法。通过builder创建server。
最终会创建TcpBridgeServer,TcpBridgeServer桥接自TcpServer。重写doHander方法。这个方法的作用就是为netty提供ChannelInitializer。因为对于不同的应用层传输协议,处理逻辑肯定不一致。如果换成websocket也是一样的道理。所以基于TCP的应用层协议,我们都可以参考这个方法。
@Override
protected ContextHandler<Channel> doHandler(
BiFunction<? super NettyInbound, ? super NettyOutbound, ? extends Publisher<Void>> handler,
MonoSink<NettyContext> sink) {
BiPredicate<HttpServerRequest, HttpServerResponse> compressPredicate =
compressPredicate(options);
boolean alwaysCompress = compressPredicate == null && options.minCompressionResponseSize() == 0;
return ContextHandler.newServerContext(sink,
options,
loggingHandler,
(ch, c, msg) -> {
HttpServerOperations ops = HttpServerOperations.bindHttp(ch,
handler,
c,
compressPredicate,
msg);
if (alwaysCompress) {
ops.compression(true);
}
return ops;
})
.onPipeline(this)
.autoCreateOperations(false);
}
ok,这里其实就是提供了一个factory,有http请求到达,便会构造一个HttpServerOperations。后续会具体介绍。
ContextHandler
继续跟踪他的initChannel方法。
@Override
protected void initChannel(CHANNEL ch) throws Exception {
accept(ch);
}
accpet:
try {
if (pipelineConfigurator != null) {
pipelineConfigurator.accept(channel.pipeline(),
(ContextHandler<Channel>) this);
}
channel.pipeline()
.addLast(NettyPipeline.ReactiveBridge,
new ChannelOperationsHandler(this));
}
该方法调用对应的accept方法。主要逻辑就是上面这几行,会调用pipelineConfigurator.accept方法。pipelineConfigurator其实就是TcpBridgeServer。在doHandler的onPipeline方法传递进来的。
TcpBridgeServer的accept方法其实就是注册http的编解码器,以及http的请求处理器。
@Override
public void accept(ChannelPipeline p, ContextHandler<Channel> c) {
p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec(
options.httpCodecMaxInitialLineLength(),
options.httpCodecMaxHeaderSize(),
options.httpCodecMaxChunkSize(),
options.httpCodecValidateHeaders(),
options.httpCodecInitialBufferSize()));
if (ACCESS_LOG_ENABLED) {
p.addLast(NettyPipeline.AccessLogHandler, new AccessLogHandler());
}
p.addLast(NettyPipeline.HttpServerHandler, new HttpServerHandler(c));
}
HttpServerHandler
看到这里,我们就知道了。所有的http请求都会走HttpServerHandler的channelRead方法。
if (persistentConnection) {
pendingResponses += 1;
if (HttpServerOperations.log.isDebugEnabled()) {
HttpServerOperations.log.debug(format(ctx.channel(), "Increasing pending responses, now {}"),
pendingResponses);
}
persistentConnection = isKeepAlive(request);
}
else {
if (HttpServerOperations.log.isDebugEnabled()) {
HttpServerOperations.log.debug(format(ctx.channel(), "Dropping pipelined HTTP request, " +
"previous response requested connection close"));
}
ReferenceCountUtil.release(msg);
return;
}
if (pendingResponses > 1) {
if (HttpServerOperations.log.isDebugEnabled()) {
HttpServerOperations.log.debug(format(ctx.channel(), "Buffering pipelined HTTP request, " +
"pending response count: {}, queue: {}"),
pendingResponses,
pipelined != null ? pipelined.size() : 0);
}
overflow = true;
doPipeline(ctx, msg);
return;
}
else {
overflow = false;
parentContext.createOperations(ctx.channel(), msg);
if (!(msg instanceof FullHttpRequest)) {
return;
}
}
这个方法比较长,上面截取了处理httpRequest的逻辑:
1.判断消息是否解析成功,如果失败直接返回错误响应。
2.判断当前连接是否保持。如果连接断开,直接丢弃该消息。否则pendingResponses加1,pendingResponses为等待响应的数量
3.判断 pendingResponses
- 大于1,代表当前连接有一个待响应。将该请求加入队列(doPipeline方法),等待后续处理,设置overflow为true。
- 等于1,处理该请求。所以一个连接不能同时处理两个请求必须等待前一个响应了才会处理。 为什么会有pendingResponses? 这个其实是http1.1的keepalive的限制。因为我印象中tomcat也是这样做的。必须是串行会话。http2.0支持了并行会话
doPipeline方法
void doPipeline(ChannelHandlerContext ctx, Object msg) {
if (pipelined == null) {
pipelined = Queues.unbounded()
.get();
}
if (!pipelined.offer(msg)) {
ctx.fireExceptionCaught(Exceptions.failWithOverflow());
}
}
这个方法就是将请求加入队列,也就是上边说的,对于一条连接,如果上一次请求还没有响应。这里会将后面的请求加入队列。
@Override
public void run() {
Object next;
boolean nextRequest = false;
while ((next = pipelined.peek()) != null) {
if (next instanceof HttpRequest) {
if (nextRequest || !persistentConnection) {
return;
}
nextRequest = true;
parentContext.createOperations(ctx.channel(), next);
if (!(next instanceof FullHttpRequest)) {
pipelined.poll();
continue;
}
}
ctx.fireChannelRead(pipelined.poll());
}
overflow = false;
}
这个run方法其实就是处理上面加入队列的请求。通过nextRequest控制,每次只会处理一个。大家肯定比较好奇这个run方法在哪被执行的,其实我也找了大半天。最后在HttpServerHandler的write方法中找到了它。
if (pipelined != null && !pipelined.isEmpty()) {
if (HttpServerOperations.log.isDebugEnabled()) {
HttpServerOperations.log.debug(format(ctx.channel(), "Draining next pipelined " +
"request, pending response count: {}, queued: {}"),
pendingResponses, pipelined.size());
}
ctx.executor()
.execute(this);
}
else {
ctx.read();
}
为啥要在write中执行。因为write意味着这个连接的上一个请求已经响应。我们可以继续处理这个连接的下一个请求了。
Ok,从上面的分析,我们基本可以知道,netty如何接受处理http请求。并且,如何支持http1.1的keepalive的串行会话的。那么我们下面看看具体的处理http请求的逻辑。
处理请求
从上面的分析我们基本可以知道,接收到请求后,回调用下面这个方法。
parentContext.createOperations(ctx.channel(), msg);
parentContext其实就是在TcpBridgeServer创建的ContextHandler。
ContextHandler的createOperations方法主要逻辑就是这行了channel.eventLoop().execute(op::onHandlerStart)这个方法。
protected void onHandlerStart() {
applyHandler();
}
这里我们注意:他是使用当前的eventLoop去执行的。这个op其实就是HttpServerOperations,最终会调用基类的applyHandler方法。
applyHandler方法
protected final void applyHandler() {
// channel.pipeline()
// .fireUserEventTriggered(NettyPipeline.handlerStartedEvent());
if (log.isDebugEnabled()) {
log.debug(format(channel(), "[{}] Handler is being applied: {}"), formatName(), handler);
}
try {
Mono.fromDirect(handler.apply((INBOUND) this, (OUTBOUND) this))
.subscribe(this);
}
catch (Throwable t) {
log.error(format(channel(), ""), t);
channel.close();
}
}
这里核心逻辑是调用handler.apply方法。这个handler对应的类为ReactorHttpHandlerAdapter。是在创建webServer传入的。
ReactorHttpHandlerAdapter#apply
这个方法构建ReactorServerHttpRequest和ReactorServerHttpResponse。然后调用httpHandler方法的handle方法处理逻辑。这里其实就是对原有的request和response包装了一下。这个handler通过HttpWebHandlerAdapter提供,最终调用的是DispatcherHandler的handler方法。
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
ServerHttpRequest request = exchange.getRequest();
logger.debug("Processing " + request.getMethodValue() + " request for [" + request.getURI() + "]");
}
if (this.handlerMappings == null) {
return Mono.error(HANDLER_NOT_FOUND_EXCEPTION);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(Mono.error(HANDLER_NOT_FOUND_EXCEPTION))
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
1.mapping.getHandler方法会根据请求的信息lookup对应的HandlerMethod。
这里说一下,我们业务写的handler初始化位置: 是在AbstractHandlerMethodMapping的initHandlerMethods方法初始化的。所有信息都保存在MappingRegistry中。
2.invokeHandler会执行业务逻辑。
3.handleResult方法处理返回结果。对于不同逻辑需要不同的处理器处理返回(比如ResponseEntity和ResponseBody的处理逻辑不同)
RequestMappingHandlerAdapter#handler
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized")
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod));
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
Function<Throwable, Mono<HandlerResult>> exceptionHandler =
ex -> handleException(ex, handlerMethod, bindingContext, exchange);
return this.modelInitializer
.initModel(handlerMethod, bindingContext, exchange)
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
.doOnNext(result -> bindingContext.saveModel())
.onErrorResume(exceptionHandler);
}
这个方法就是处理业务逻辑的方法。
initModel主要是初始化一些session数据。存储到bindingContext中。
接着会调用invocableMethod.invoke方法执行业务逻辑。
然后会对结果设置异常处理器。如果处理异常会执行异常逻辑,否则直接返回HandlerResult让上层处理。
InvocableHandlerMethod#invoke
public Mono<HandlerResult> invoke(
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
return resolveArguments(exchange, bindingContext, providedArgs).flatMap(args -> {
try {
Object value = doInvoke(args);
HttpStatus status = getResponseStatus();
if (status != null) {
exchange.getResponse().setStatusCode(status);
}
MethodParameter returnType = getReturnType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType());
boolean asyncVoid = isAsyncVoidReturnType(returnType, adapter);
if ((value == null || asyncVoid) && isResponseHandled(args, exchange)) {
logger.debug("Response fully handled in controller method");
return asyncVoid ? Mono.from(adapter.toPublisher(value)) : Mono.empty();
}
HandlerResult result = new HandlerResult(this, value, returnType, bindingContext);
return Mono.just(result);
}
catch (InvocationTargetException ex) {
return Mono.error(ex.getTargetException());
}
catch (Throwable ex) {
return Mono.error(new IllegalStateException(getInvocationErrorMessage(args)));
}
});
}
1.resolveArguments主要就是根据方法配置,以及request数据反序列化方法参数。
2.doInvoke通过反射调用方法。
3.构建结果返回
返回之后,spring会通过我们方法的配置选择合适的处理器处理返回结果。一般会使用ResponseBody。他的处理逻辑就是将结果序列化成json。最后写回客户端。
具体写操作是在DeferredWriteMono中的subscribe中实现的。封装的层比较多。但是最后会调用到这个方法。
5.总结
其实webflux的实现就是使用响应式将原有的实现方式重写了一遍。代码阅读起来比较易于理解。但是调试起来是真的痛苦。简单的理解就是一顿梭哈Stream,套来套去,最后丢到NioEventLoop去消费。至于如何串起来。其实还是要靠响应式。
使用webflux可以更好的利用cpu。减少不必要的线程阻塞。在日常开发中,如果是计算密集型。完全可以在eventloop上直接跑。但是对于网络io型,就需要构造线程池异步处理。如果对应的服务支持响应式异步,那是更好不过了(否则也没有意义)。