剖析 Spring 中 @ResponseBody 原理与 Tomcat NIO 写事件(SelectionKey.OP_WRITE)的协作机制
在 Spring Web 开发领域,@ResponseBody
是实现 RESTful 接口的核心注解之一,它能够将方法的返回值直接转化为 HTTP 响应体。而 Tomcat 作为 Spring 常用的 Servlet 容器,在处理网络 IO 时采用了 NIO 模型,借助 SelectionKey.OP_WRITE
事件实现非阻塞式的写操作。下面将结合 Spring 5 和 Tomcat 源码,深入探究这两者的协同工作原理。
一、@ResponseBody 的核心处理逻辑(基于 Spring MVC 机制)
@ResponseBody
的作用是告知 Spring MVC,需将控制器方法的返回值通过消息转换器转化为 HTTP 响应体,而非解析为视图。其处理流程主要包含以下几个关键环节:
1. 处理器的识别与选取
当 Spring MVC 调度至目标控制器方法后,会调用 RequestMappingHandlerAdapter
的 handleReturnValue
方法来处理返回值。在此过程中,通过 selectHandler
方法从 returnValueHandlers
列表中筛选合适的处理器:
java
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {boolean isAsyncValue = isAsyncReturnValue(value, returnType);for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {continue;}if (handler.supportsReturnType(returnType)) { // 检查是否支持 @ResponseBodyreturn handler;}}return null;
}
若方法或类上标注了 @ResponseBody
,supportsReturnType
方法会返回 true
,最终会选用 RequestResponseBodyMethodProcessor
作为处理器。
2. 消息转换与响应写入
RequestResponseBodyMethodProcessor
的 handleReturnValue
方法会调用 writeWithMessageConverters
方法,该方法的主要功能如下:
- 依据请求头中的
Accept
字段,从注册的消息转换器(如MappingJackson2HttpMessageConverter
)中挑选出合适的转换器。 - 将返回值(例如 Java 对象)转换为字节流,并写入
ServletServerHttpResponse
对应的OutputStream
中。
java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {mavContainer.setRequestHandled(true); // 标记为直接处理响应体ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);writeWithMessageConverters(returnValue, returnType, null, outputMessage); // 执行转换与写入
}
值得注意的是,此时的写入操作仅仅是将数据写入到 Tomcat 的缓冲区中,并未真正通过网络发送数据,实际的网络发送由 Tomcat 的 NIO 模块负责。
二、Tomcat NIO 中的写事件(SelectionKey.OP_WRITE)处理流程
Tomcat 在处理 NIO 连接时,通过 Poller
线程管理 Selector
,并借助 SelectionKey
监听 OP_READ
和 OP_WRITE
等事件。当 Spring 将数据写入缓冲区后,Tomcat 会依据缓冲区的状态决定是否注册 OP_WRITE
事件。
1. 写操作的初始尝试
在 Tomcat 的 NioChannel
实现类(如 SocketChannelImpl
)的 write
方法中,会先尝试直接向套接字写入数据:
java
int cnt = socket.write(buf); // 尝试直接写入
if (cnt > 0) {time = System.currentTimeMillis(); // 重置超时时间continue; // 写入成功,无需注册事件
}
- 若缓冲区有足够的空间,数据会直接写入套接字,此时无需注册
OP_WRITE
事件。 - 若写入字节数为
0
(表明套接字暂不可写),则需要通过Poller
注册OP_WRITE
事件,等待可写状态的通知。
2. 注册 OP_WRITE 事件与等待机制
当套接字暂不可写时,Tomcat 会执行以下操作:
java
poller.add(att, SelectionKey.OP_WRITE, reference); // 向 Poller 注册写事件
att.awaitWriteLatch(writeTimeout, TimeUnit.MILLISECONDS); // 等待可写通知或超时
Poller
的作用:Poller
是 Tomcat NIO 中的事件轮询线程,负责将SelectionKey
的注册 / 取消操作封装成任务,提交到Selector
所在的线程执行,从而避免多线程竞争问题。writeLatch
的作用:通过CountDownLatch
实现线程间的通信。当Selector
检测到套接字可写时,会触发SelectionKey
的回调,调用NioSocketWrapper
的processWrite
方法,对writeLatch
进行减计数,以唤醒等待线程。
3. 超时处理与事件取消
若在指定的 writeTimeout
时间内未收到可写通知,Tomcat 会抛出 SocketTimeoutException
,并取消注册的 OP_WRITE
事件:
java
if (timedout) {poller.cancelKey(reference.key); // 取消事件注册throw new SocketTimeoutException();
}
通过这种超时机制,能够有效防止因套接字长时间不可写而导致的线程阻塞问题。
三、@ResponseBody 与 Tomcat NIO 的协作链路
下面以一个返回 JSON 数据的接口为例,梳理完整的处理流程:
-
Spring MVC 处理返回值:
- 控制器方法返回
User
对象,@ResponseBody
触发RequestResponseBodyMethodProcessor
对其进行处理。 MappingJackson2HttpMessageConverter
将User
对象序列化为 JSON 字节流,并写入ServletOutputStream
,实际上是写入 Tomcat 的ByteBuffer
缓冲区。
- 控制器方法返回
-
Tomcat NIO 处理写操作:
- 当缓冲区已满,无法直接写入套接字时,Tomcat 会通过
Poller
向Selector
注册OP_WRITE
事件,并通过writeLatch
阻塞当前线程。 Selector
轮询到OP_WRITE
事件后,唤醒阻塞线程,再次尝试写入数据,直至所有数据都写入完毕或者超时。
- 当缓冲区已满,无法直接写入套接字时,Tomcat 会通过
-
关键类的协作关系:
NioSocketWrapper
:封装了套接字的状态,如writeLatch
和SelectionKey
的附件(attachment
)。KeyReference
:作为SelectionKey
的引用池,用于减少对象的创建和销毁开销。Poller
:负责协调Selector
的事件注册和取消操作,确保线程安全。
四、总结
@ResponseBody
的核心原理是利用 Spring MVC 的消息转换机制,将方法返回值转化为字节流并写入响应缓冲区,而实际的网络发送则由 Tomcat 的 NIO 模块通过事件驱动的方式完成。当套接字暂不可写时,Tomcat 会通过注册 SelectionKey.OP_WRITE
事件实现非阻塞等待,这种机制充分发挥了 NIO 的优势,能够高效地处理高并发场景下的写操作。
通过深入理解这一流程,开发者可以更好地优化响应体的转换逻辑(如选择更高效的消息转换器),同时也能针对网络延迟、缓冲区设置等问题进行性能调优。
##源码
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout)throws IOException {SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector());if (key == null) {throw new IOException(sm.getString("nioBlockingSelector.keyNotRegistered"));}KeyReference reference = keyReferenceStack.pop();if (reference == null) {reference = new KeyReference();}NioSocketWrapper att = (NioSocketWrapper) key.attachment();int written = 0;boolean timedout = false;int keycount = 1; //assume we can writelong time = System.currentTimeMillis(); //start the timeout timertry {while (!timedout && buf.hasRemaining()) {if (keycount > 0) { //only write if we were registered for a writeint cnt = socket.write(buf); //write the dataif (cnt == -1) {throw new EOFException();}written += cnt;if (cnt > 0) {time = System.currentTimeMillis(); //reset our timeout timercontinue; //we successfully wrote, try again without a selector}}try {if (att.getWriteLatch() == null || att.getWriteLatch().getCount() == 0) {att.startWriteLatch(1);}poller.add(att, SelectionKey.OP_WRITE, reference);att.awaitWriteLatch(AbstractEndpoint.toTimeout(writeTimeout), TimeUnit.MILLISECONDS);} catch (InterruptedException ignore) {// Ignore}if (att.getWriteLatch() != null && att.getWriteLatch().getCount() > 0) {//we got interrupted, but we haven't received notification from the poller.keycount = 0;} else {//latch countdown has happenedkeycount = 1;att.resetWriteLatch();}if (writeTimeout > 0 && (keycount == 0)) {timedout = (System.currentTimeMillis() - time) >= writeTimeout;}}if (timedout) {throw new SocketTimeoutException();}} finally {poller.remove(att, SelectionKey.OP_WRITE);if (timedout && reference.key != null) {poller.cancelKey(reference.key);}reference.key = null;keyReferenceStack.push(reference);}return written;}@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {mavContainer.setRequestHandled(true);ServletServerHttpRequest inputMessage = createInputMessage(webRequest);ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);// Try even with null return value. ResponseBodyAdvice could get involved.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);}@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);if (handler == null) {throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());}handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);}@Nullableprivate HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {boolean isAsyncValue = isAsyncReturnValue(value, returnType);for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {continue;}if (handler.supportsReturnType(returnType)) {return handler;}}return null;}@Overridepublic boolean supportsReturnType(MethodParameter returnType) {return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||returnType.hasMethodAnnotation(ResponseBody.class));}