在 Spring Web 开发领域,@ResponseBody
是实现 RESTful 接口的核心注解之一,它能够将方法的返回值直接转化为 HTTP 响应体。而 Tomcat 作为 Spring 常用的 Servlet 容器,在处理网络 IO 时采用了 NIO 模型,借助 SelectionKey.OP_WRITE
事件实现非阻塞式的写操作。下面将结合 Spring 5 和 Tomcat 源码,深入探究这两者的协同工作原理。
@ResponseBody
的作用是告知 Spring MVC,需将控制器方法的返回值通过消息转换器转化为 HTTP 响应体,而非解析为视图。其处理流程主要包含以下几个关键环节:
当 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)) { // 检查是否支持 @ResponseBody
return handler;
}
}
return null;
}
若方法或类上标注了 @ResponseBody
,supportsReturnType
方法会返回 true
,最终会选用 RequestResponseBodyMethodProcessor
作为处理器。
RequestResponseBodyMethodProcessor
的 handleReturnValue
方法会调用 writeWithMessageConverters
方法,该方法的主要功能如下:
Accept
字段,从注册的消息转换器(如 MappingJackson2HttpMessageConverter
)中挑选出合适的转换器。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 连接时,通过 Poller
线程管理 Selector
,并借助 SelectionKey
监听 OP_READ
和 OP_WRITE
等事件。当 Spring 将数据写入缓冲区后,Tomcat 会依据缓冲区的状态决定是否注册 OP_WRITE
事件。
在 Tomcat 的 NioChannel
实现类(如 SocketChannelImpl
)的 write
方法中,会先尝试直接向套接字写入数据:
java
int cnt = socket.write(buf); // 尝试直接写入
if (cnt > 0) {
time = System.currentTimeMillis(); // 重置超时时间
continue; // 写入成功,无需注册事件
}
OP_WRITE
事件。0
(表明套接字暂不可写),则需要通过 Poller
注册 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
进行减计数,以唤醒等待线程。若在指定的 writeTimeout
时间内未收到可写通知,Tomcat 会抛出 SocketTimeoutException
,并取消注册的 OP_WRITE
事件:
java
if (timedout) {
poller.cancelKey(reference.key); // 取消事件注册
throw new SocketTimeoutException();
}
通过这种超时机制,能够有效防止因套接字长时间不可写而导致的线程阻塞问题。
下面以一个返回 JSON 数据的接口为例,梳理完整的处理流程:
Spring MVC 处理返回值:
User
对象,@ResponseBody
触发 RequestResponseBodyMethodProcessor
对其进行处理。MappingJackson2HttpMessageConverter
将 User
对象序列化为 JSON 字节流,并写入 ServletOutputStream
,实际上是写入 Tomcat 的 ByteBuffer
缓冲区。Tomcat NIO 处理写操作:
Poller
向 Selector
注册 OP_WRITE
事件,并通过 writeLatch
阻塞当前线程。Selector
轮询到 OP_WRITE
事件后,唤醒阻塞线程,再次尝试写入数据,直至所有数据都写入完毕或者超时。关键类的协作关系:
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 write
long time = System.currentTimeMillis(); //start the timeout timer
try {
while (!timedout && buf.hasRemaining()) {
if (keycount > 0) { //only write if we were registered for a write
int cnt = socket.write(buf); //write the data
if (cnt == -1) {
throw new EOFException();
}
written += cnt;
if (cnt > 0) {
time = System.currentTimeMillis(); //reset our timeout timer
continue; //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 happened
keycount = 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;
}
@Override
public 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);
}
@Override
public 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);
}
@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)) {
return handler;
}
}
return null;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}