剖析 Spring 中 @ResponseBody 原理与 Tomcat NIO 写事件(SelectionKey.OP_WRITE)的协作机制

发布于:2025-05-25 ⋅ 阅读:(18) ⋅ 点赞:(0)

在 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)) { // 检查是否支持 @ResponseBody
            return handler;
        }
    }
    return null;
}

若方法或类上标注了 @ResponseBodysupportsReturnType 方法会返回 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 数据的接口为例,梳理完整的处理流程:

  1. Spring MVC 处理返回值

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

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

    • 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));
	}


网站公告

今日签到

点亮在社区的每一天
去签到