剖析 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 调度至目标控制器方法后,会调用 RequestMappingHandlerAdapterhandleReturnValue 方法来处理返回值。在此过程中,通过 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;
}

若方法或类上标注了 @ResponseBodysupportsReturnType 方法会返回 true,最终会选用 RequestResponseBodyMethodProcessor 作为处理器。

2. 消息转换与响应写入

RequestResponseBodyMethodProcessorhandleReturnValue 方法会调用 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_READOP_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 的回调,调用 NioSocketWrapperprocessWrite 方法,对 writeLatch 进行减计数,以唤醒等待线程。
3. 超时处理与事件取消

若在指定的 writeTimeout 时间内未收到可写通知,Tomcat 会抛出 SocketTimeoutException,并取消注册的 OP_WRITE 事件:

java

复制代码
if (timedout) {
    poller.cancelKey(reference.key); // 取消事件注册
    throw new SocketTimeoutException();
}

通过这种超时机制,能够有效防止因套接字长时间不可写而导致的线程阻塞问题。

三、@ResponseBody 与 Tomcat NIO 的协作链路

下面以一个返回 JSON 数据的接口为例,梳理完整的处理流程:

  1. Spring MVC 处理返回值

    • 控制器方法返回 User 对象,@ResponseBody 触发 RequestResponseBodyMethodProcessor 对其进行处理。
    • MappingJackson2HttpMessageConverterUser 对象序列化为 JSON 字节流,并写入 ServletOutputStream,实际上是写入 Tomcat 的 ByteBuffer 缓冲区。
  2. Tomcat NIO 处理写操作

    • 当缓冲区已满,无法直接写入套接字时,Tomcat 会通过 PollerSelector 注册 OP_WRITE 事件,并通过 writeLatch 阻塞当前线程。
    • Selector 轮询到 OP_WRITE 事件后,唤醒阻塞线程,再次尝试写入数据,直至所有数据都写入完毕或者超时。
  3. 关键类的协作关系

    • NioSocketWrapper:封装了套接字的状态,如 writeLatchSelectionKey 的附件(attachment)。
    • KeyReference:作为 SelectionKey 的引用池,用于减少对象的创建和销毁开销。
    • Poller:负责协调 Selector 的事件注册和取消操作,确保线程安全。
四、总结

@ResponseBody 的核心原理是利用 Spring MVC 的消息转换机制,将方法返回值转化为字节流并写入响应缓冲区,而实际的网络发送则由 Tomcat 的 NIO 模块通过事件驱动的方式完成。当套接字暂不可写时,Tomcat 会通过注册 SelectionKey.OP_WRITE 事件实现非阻塞等待,这种机制充分发挥了 NIO 的优势,能够高效地处理高并发场景下的写操作。

通过深入理解这一流程,开发者可以更好地优化响应体的转换逻辑(如选择更高效的消息转换器),同时也能针对网络延迟、缓冲区设置等问题进行性能调优。

##源码

cpp 复制代码
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));
	}
相关推荐
Leaf吧3 小时前
java BIO/NIO/AIO
java·开发语言·nio
IT_10243 小时前
springboot从零入门之接口测试!
java·开发语言·spring boot·后端·spring·lua
皮皮林5514 小时前
项目终于用上了 Spring 状态机,太优雅了!
spring
迢迢星万里灬5 小时前
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术点解析
java·spring boot·spring·mybatis·spring mvc·面试指南
Hanson Huang7 小时前
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(2)——Prompt(提示词)
java·人工智能·spring·spring ai
.生产的驴8 小时前
SpringBoot 服务器监控 监控系统开销 获取服务器系统的信息用户信息 运行信息 保持稳定
服务器·spring boot·分布式·后端·spring·spring cloud·信息可视化
没有烦恼的猫猫9 小时前
有关Spring事务的传播机制
spring·事务
magic 2459 小时前
事务传播行为详解
spring
考虑考虑10 小时前
@MockitoBean注解使用
spring boot·后端·spring
Dkodak10 小时前
Could not initialize Logback logging from classpath:logback-spring.xml
xml·spring·logback