揭秘 Spring MVC:手写一个迷你 DispatcherServlet 处理 @RequestParam 与 @ResponseBody

当一个网络请求到达我们的 Spring Boot 应用时,一场精心编排的“交响乐”就开始演奏了。@RestController 就像舞台,@GetMapping 负责指路,@RequestParam 负责递上道具,而 @ResponseBody 则决定了最终的谢幕方式。这场演出的总指挥,就是 DispatcherServlet

Chapter 1: The DispatcherServlet Lifecycle - Spring MVC 的心脏

在 Spring MVC 的世界里,所有请求都会先经过 DispatcherServlet。它的核心职责可以用一个流程来概括:

  1. 请求分发 (Dispatch): DispatcherServlet 接收到请求后,会向 HandlerMapping(处理器映射器) 询问:“这个 URL 应该由谁来处理?” HandlerMapping 会返回一个处理器(通常是我们 Controller 里的一个具体方法)。
  2. 适配与调用 (Adapt & Invoke): DispatcherServlet 不会直接调用这个方法,而是将它交给 HandlerAdapter(处理器适配器)。HandlerAdapter 是一个非常关键的角色,它负责真正地调用目标方法。在调用之前,它会“聪明”地解析方法的参数,看到 @RequestParam 就去请求参数里取值,看到 @RequestBody 就去读请求体。
  3. 返回值处理 (Return Value Handling): 方法执行完毕后,HandlerAdapter 会将返回值告诉 DispatcherServlet。如果方法或类上带有 @ResponseBody 注解,DispatcherServlet 会通过 HttpMessageConverter (消息转换器) 将返回的 Java 对象序列化成 JSON(或其他格式),然后写入 HTTP 响应。

我们的任务,就是模拟这个流程,特别是 HandlerMappingHandlerAdapter 的核心功能。

Chapter 2: The Building Blocks - 新的注解与组件

首先,定义好我们的注解和模拟的 Web 环境。

2.1 Web 注解

// @MiniRestController: 标记这是一个控制器,且所有方法默认返回 JSON
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MiniRestController {}

// @MiniGetMapping: 标记一个方法处理 GET 请求
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MiniGetMapping {
    String value(); // 用于定义 URL 路径
}

// @MiniRequestParam: 从请求参数中获取值并绑定到方法参数上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface MiniRequestParam {
    String value(); // 请求参数的名字
}

为了简化,我们约定 @MiniRestController 就隐含了所有方法都按 @ResponseBody 方式处理,这与 Spring 的 @RestController 行为一致。

2.2 模拟 Web 环境

import java.util.Map;
// 模拟 HTTP 请求
public class MiniHttpServletRequest {
    private String path;
    private Map<String, String> parameters;
    public MiniHttpServletRequest(String path, Map<String, String> parameters) { this.path = path; this.parameters = parameters; }
    public String getPath() { return path; }
    public String getParameter(String name) { return parameters.get(name); }
}

// 模拟 HTTP 响应
public class MiniHttpServletResponse {
    private String body;
    public void setBody(String body) { this.body = body; }
    public String getBody() { return body; }
}

2.3 处理器元数据

HandlerMethod: 用于封装一个准备被调用的 Controller 方法的所有信息。

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class HandlerMethod {
    private final Object bean; // 方法所在的 Bean 实例
    private final Method method; // 方法本身
    private final Parameter[] parameters; // 方法的参数列表

    public HandlerMethod(Object bean, Method method) {
        this.bean = bean;
        this.method = method;
        this.parameters = method.getParameters();
    }
    // Getters...
}

Chapter 3: The Engine Room - 构建 MiniDispatcherServlet

这个类是本次实战的核心,它将使用我们之前构建的 MiniApplicationContext

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

public class MiniDispatcherServlet {

    // 模拟 HandlerMapping,存储 URL 到处理方法的映射
    private final Map<String, HandlerMethod> handlerMapping = new HashMap<>();
    private final MiniApplicationContext context;

    public MiniDispatcherServlet(MiniApplicationContext context) {
        this.context = context;
        initHandlerMappings();
    }

    /**
     * 初始化 HandlerMapping,在启动时完成
     */
    private void initHandlerMappings() {
        System.out.println("--- 初始化 Handler Mappings ---");
        // 从 IoC 容器中获取所有的 Bean
        for (Object bean : context.getAllBeans().values()) {
            Class<?> clazz = bean.getClass();
            if (clazz.isAnnotationPresent(MiniRestController.class)) {
                // 遍历 Controller 的所有方法
                for (Method method : clazz.getDeclaredMethods()) {
                    if (method.isAnnotationPresent(MiniGetMapping.class)) {
                        MiniGetMapping mapping = method.getAnnotation(MiniGetMapping.class);
                        String path = mapping.value();
                        // 创建 HandlerMethod 并注册
                        handlerMapping.put(path, new HandlerMethod(bean, method));
                        System.out.printf("  [Mapping] 发现处理器方法: %s -> %s.%s\n", path, clazz.getSimpleName(), method.getName());
                    }
                }
            }
        }
    }
    
    /**
     * 处理请求的核心方法
     */
    public void doDispatch(MiniHttpServletRequest request, MiniHttpServletResponse response) throws Exception {
        System.out.printf("\n--- 接收到请求: %s ---\n", request.getPath());
        // 1. 根据请求路径查找处理器方法
        HandlerMethod handler = handlerMapping.get(request.getPath());

        if (handler == null) {
            System.out.println("  [Error] 404 Not Found. 没有找到处理器。");
            return;
        }

        // 2. 调用处理器方法(模拟 HandlerAdapter 的工作)
        Object returnValue = invokeHandler(request, handler);

        // 3. 处理返回值(模拟 HttpMessageConverter 的工作)
        handleReturnValue(returnValue, response);
    }
}

Chapter 4: The Real Magic - 参数解析与返回值处理

doDispatch 方法将最复杂的工作委托给了 invokeHandlerhandleReturnValue。这正是模拟 HandlerAdapter 的部分。

MiniDispatcherServlet.java (添加 invokehandle 方法)

// ... 接上文
public class MiniDispatcherServlet {
    // ...
    
    /**
     * 模拟 HandlerAdapter 的功能:解析参数并调用方法
     */
    private Object invokeHandler(MiniHttpServletRequest request, HandlerMethod handler) throws Exception {
        System.out.println("  [Adapter] 开始解析参数并调用方法...");
        Parameter[] parameters = handler.getParameters();
        Object[] args = new Object[parameters.length];

        // 遍历方法的每一个参数,看有什么注解,然后从请求中取值
        for (int i = 0; i < parameters.length; i++) {
            Parameter param = parameters[i];
            if (param.isAnnotationPresent(MiniRequestParam.class)) {
                MiniRequestParam requestParam = param.getAnnotation(MiniRequestParam.class);
                String paramName = requestParam.value();
                String paramValue = request.getParameter(paramName);

                // 简单的类型转换(真实 Spring 中更复杂)
                if (param.getType() == Integer.class) {
                    args[i] = Integer.parseInt(paramValue);
                } else {
                    args[i] = paramValue;
                }
                 System.out.printf("    - 解析到 @RequestParam: '%s' -> '%s'\n", paramName, paramValue);
            }
        }
        
        System.out.println("  [Adapter] 参数解析完成,准备调用...");
        // 通过反射调用 Controller 的方法
        return handler.getMethod().invoke(handler.getBean(), args);
    }
    
    /**
     * 模拟 HttpMessageConverter,将返回值序列化为 JSON 字符串
     */
    private void handleReturnValue(Object returnValue, MiniHttpServletResponse response) {
        System.out.println("  [Converter] 开始处理返回值...");
        // 简单模拟 JSON 序列化
        // 真实 Spring 使用 Jackson 等库
        if (returnValue instanceof Map) {
            StringBuilder json = new StringBuilder("{");
            for (Object entry : ((Map<?, ?>) returnValue).entrySet()) {
                Map.Entry<?, ?> mapEntry = (Map.Entry<?, ?>) entry;
                json.append("\"").append(mapEntry.getKey()).append("\":\"").append(mapEntry.getValue()).append("\",");
            }
            if (json.length() > 1) json.deleteCharAt(json.length() - 1);
            json.append("}");
            response.setBody(json.toString());
        } else {
             response.setBody(returnValue.toString());
        }
         System.out.printf("  [Converter] 返回值处理完成,响应体为: %s\n", response.getBody());
    }
}

Chapter 5: Putting It All Together - 组装与验证

我们来创建一个 Controller,并用 Main 方法模拟一次完整的请求-响应流程。

UserController.java

import java.util.Map;

@MiniRestController // 标记为控制器
public class UserController {

    @MiniGetMapping("/user/info") // 映射到 /user/info
    public Map<String, String> getUserInfo(
            @MiniRequestParam("userId") Integer userId, // 绑定名为 userId 的请求参数
            @MiniRequestParam("name") String name
    ) {
        System.out.printf("    [Controller] 进入 getUserInfo 方法,参数: userId=%d, name=%s\n", userId, name);
        return Map.of(
                "userId", userId.toString(),
                "name", name,
                "from", "Mini-Spring-MVC"
        );
    }
}

MainApplication.java

import java.util.Map;

public class MainApplication {
    public static void main(String[] args) throws Exception {
        // 1. 启动 IoC 容器,注册我们的 Controller
        MiniApplicationContext context = new MiniApplicationContext(UserController.class);

        // 2. 启动我们的 DispatcherServlet,它会自动初始化 HandlerMapping
        MiniDispatcherServlet dispatcherServlet = new MiniDispatcherServlet(context);

        // 3. 模拟一个 HTTP GET 请求,访问 /user/info,并带有两个参数
        MiniHttpServletRequest request = new MiniHttpServletRequest(
                "/user/info",
                Map.of("userId", "123", "name", "Geek-Player")
        );
        MiniHttpServletResponse response = new MiniHttpServletResponse();

        // 4. 将请求交给 DispatcherServlet 处理
        dispatcherServlet.doDispatch(request, response);

        // 5. 打印最终的响应结果
        System.out.println("\n--- 请求处理完毕,最终响应体 ---");
        System.out.println(response.getBody());
    }
}

运行 MainApplication,你将看到一条清晰的请求处理链:

... (IoC 容器实例化日志) ...
--- 初始化 Handler Mappings ---
  [Mapping] 发现处理器方法: /user/info -> UserController.getUserInfo

--- 接收到请求: /user/info ---
  [Adapter] 开始解析参数并调用方法...
    - 解析到 @RequestParam: 'userId' -> '123'
    - 解析到 @RequestParam: 'name' -> 'Geek-Player'
  [Adapter] 参数解析完成,准备调用...
    [Controller] 进入 getUserInfo 方法,参数: userId=123, name=Geek-Player
  [Converter] 开始处理返回值...
  [Converter] 返回值处理完成,响应体为: {"name":"Geek-Player","userId":"123","from":"Mini-Spring-MVC"}

--- 请求处理完毕,最终响应体 ---
{"name":"Geek-Player","userId":"123","from":"Mini-Spring-MVC"}

Conclusion: The MVC Symphony - MVC 各组件的协同演奏

这次的旅程,我们从核心容器走向了 Web 世界。与 DI 和 AOP 在启动时“一次性”的处理不同,Web 层注解的处理是在每次请求时动态发生的

核心思想总结:

  1. 统一入口,分层处理: DispatcherServlet 作为唯一入口,将复杂的请求处理流程,委托给了 HandlerMappingHandlerAdapter 等多个策略组件,完美体现了单一职责原则和策略模式。
  2. 映射驱动: HandlerMapping 的核心是在启动时建立一个请求(URL)到处理器(方法)的映射 Map,这是实现快速路由的关键。
  3. 适配器模式的妙用: HandlerAdapter 隔离了 DispatcherServlet 与具体 Controller 方法的调用细节。它内部再使用更细粒度的 ArgumentResolver (参数解析器) 和 ReturnValueHandler (返回值处理器) 来处理具体注解,使得整个体系极易扩展。
  4. 运行时解析: 所有 Web 注解的解析,都发生在请求到达时,是高度动态的。