Spring Boot 从“会用”到“精通”:参数解析原理

参数解析原理

一、是什么 ------ 参数解析到底在做什么?

1. 一句话定义

参数解析就是 Spring MVC 在调用 Controller 方法之前,根据方法参数的类型、注解等信息,从 HTTP 请求中提取出对应的值,然后通过反射传给方法的过程。

2. 用大白话理解

你的 Controller 方法可以声明五花八门的参数:

java 复制代码
// 源码位置:springboot2-master/boot-05-web-01/.../controller/ParameterTestController.java
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,          // 来自路径
                                 @PathVariable("username") String name,   // 来自路径
                                 @RequestParam("age") Integer age,        // 来自查询参数
                                 @RequestHeader("User-Agent") String ua,  // 来自请求头
                                 @CookieValue("_ga") Cookie cookie) {     // 来自 Cookie
    // ...
}

Spring 是怎么知道:

  • id 要从 URL 路径 /car/2/owner/zhangsan 中提取 2
  • age 要从 ?age=18 中提取 18
  • ua 要从请求头 User-Agent 中提取

答案就是:参数解析器(HandlerMethodArgumentResolver)体系。

3. 核心接口

java 复制代码
public interface HandlerMethodArgumentResolver {
    // 判断:这个解析器能处理这个参数吗?
    boolean supportsParameter(MethodParameter parameter);
    
    // 解析:如果能处理,就解析出参数的值
    Object resolveArgument(MethodParameter parameter, 
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest, 
                           WebDataBinderFactory binderFactory) throws Exception;
}

二、为什么 ------ 为什么要用这套机制?

1. 不这么做会怎样?

如果没有参数解析器,你需要自己在每个 Controller 方法开头写:

java 复制代码
// 手动解析,噩梦!
String idStr = request.getParameter("id");
Integer id = Integer.parseInt(idStr);
String name = request.getParameter("name");
// ... 每个参数都要写,还要处理 null、类型转换、默认值

几百个 Controller 方法,几千行重复代码。

2. 设计哲学:策略模式 + 组合模式

Spring 把"解析参数"这个行为抽象成策略接口(HandlerMethodArgumentResolver),然后注册了几十个具体策略(@RequestParam的、@PathVariable的、@RequestBody的......)。

需要解析时,遍历所有策略,找到第一个支持的,就交给它执行


三、怎么做 ------ 参数解析的完整源码流程

1. HandlerAdapter 的选取(getHandlerAdapter)

doDispatch() 中,拿到 Handler 之后,需要找到能执行它的适配器:

java 复制代码
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

这也是遍历匹配的:

java 复制代码
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        for (HandlerAdapter adapter : this.handlerAdapters) {
            if (adapter.supports(handler)) {  // 询问每个适配器
                return adapter;               // 找到就返回
            }
        }
    }
    throw new ServletException("No adapter for handler...");
}

对于 @RequestMapping 标注的方法,handler 是 HandlerMethod 类型,命中的是 RequestMappingHandlerAdapter

java 复制代码
// RequestMappingHandlerAdapter 的 supports 判断
public final boolean supports(Object handler) {
    return handler instanceof HandlerMethod 
        && this.supportsInternal((HandlerMethod) handler);
}

2. RequestMappingHandlerAdapter 的准备工作

适配器内部已经准备好了两套"兵工厂"

java 复制代码
// RequestMappingHandlerAdapter.invokeHandlerMethod() 中
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);

// ★ 关键:注入参数解析器(27个)
if (this.argumentResolvers != null) {
    invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}

// ★ 关键:注入返回值处理器(15个)
if (this.returnValueHandlers != null) {
    invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}

默认的 argumentResolvers27 个,包含:

解析器 处理什么
RequestParamMethodArgumentResolver @RequestParam 注解
PathVariableMethodArgumentResolver @PathVariable 注解
RequestResponseBodyMethodProcessor @RequestBody 注解
ServletRequestMethodArgumentResolver HttpServletRequest 类型
ServletModelAttributeMethodProcessor 自定义 POJO 对象
MapMethodProcessor Map 类型参数
ModelMethodProcessor Model 类型参数
...... ......

3. 执行入口:invokeAndHandle()

java 复制代码
// ServletInvocableHandlerMethod
public void invokeAndHandle(ServletWebRequest webRequest, 
                             ModelAndViewContainer mavContainer, 
                             Object... providedArgs) throws Exception {
    // ★ 第一步:解析参数 + 反射调用 Controller 方法
    Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
    // 第二步:处理返回值
    this.returnValueHandlers.handleReturnValue(...);
}

4. 核心!getMethodArgumentValues() ------ 逐行拆解

java 复制代码
protected Object[] getMethodArgumentValues(NativeWebRequest request, 
                                            ModelAndViewContainer mavContainer, 
                                            Object... providedArgs) throws Exception {
    // 步骤 1:获取方法的所有参数元数据
    MethodParameter[] parameters = this.getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;  // 步骤 2:无参方法直接返回空数组
    }
    
    // 步骤 3:初始化参数值数组
    Object[] args = new Object[parameters.length];
    
    // 步骤 4:逐个遍历参数
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        
        // 步骤 5:确保参数名可用(通过 ASM 或 JDK8+ -parameters 编译标志)
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        
        // 步骤 6:检查外部预提供的参数(如测试环境预置的 Model)
        args[i] = findProvidedArgument(parameter, providedArgs);
        
        if (args[i] == null) {
            // 步骤 7:判断是否有解析器支持当前参数
            if (!this.resolvers.supportsParameter(parameter)) {
                throw new IllegalStateException("No suitable resolver for: " + parameter);
            }
            
            // 步骤 8:调用解析器解析参数值
            try {
                args[i] = this.resolvers.resolveArgument(parameter, mavContainer, 
                                                         request, this.dataBinderFactory);
            } catch (Exception ex) {
                throw ex;
            }
        }
    }
    // 步骤 9:返回参数数组 → 用于反射调用
    return args;
}

5. 解析器匹配的内部机制 ------ supportsParameter()

this.resolvers 实际上是 HandlerMethodArgumentResolverComposite(组合模式),它在内部遍历所有解析器

java 复制代码
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    // ★ 缓存优先:同一个方法的同一个参数,不需要每次遍历
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                // ★ 找到后放入缓存,下次 O(1) 直接命中
                this.argumentResolverCache.put(parameter, resolver);
                break;
            }
        }
    }
    return result;
}

性能优化的点睛之笔argumentResolverCache 是一个 ConcurrentHashMap。第一次请求时遍历 N 个解析器找到匹配的,后续相同方法的相同参数直接从缓存取,时间复杂度 O(1)。

6. 解析参数的通用模板 ------ resolveArgument()

对于 @RequestParam@PathVariable@RequestHeader命名参数 ,都继承自 AbstractNamedValueMethodArgumentResolver,使用了模板方法模式

java 复制代码
public final Object resolveArgument(MethodParameter parameter, ...) throws Exception {
    // ① 获取命名值元数据(注解中的 name、required、defaultValue 等)
    NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
    
    // ② 解析名称中的占位符/SpEL 表达式
    Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
    
    // ③ ★ 核心取值(抽象方法,子类实现)
    //    @RequestParam → request.getParameter()
    //    @PathVariable → 从 URI 模板变量中取
    //    @RequestHeader → request.getHeader()
    Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
    
    // ④ 处理缺失值和默认值
    if (arg == null) {
        if (namedValueInfo.defaultValue != null) {
            arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
        } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
            handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
        }
        arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
    }
    
    // ⑤ ★ 类型转换(使用 WebDataBinder)
    if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
        arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
    }
    
    return arg;
}

7. 案例分析:HttpServletRequest 参数如何解析

java 复制代码
// 源码位置:springboot2-master/boot-05-web-01/.../controller/RequestController.java
@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
    request.setAttribute("msg", "成功了...");
    return "forward:/success";
}

匹配的解析器ServletRequestMethodArgumentResolver

java 复制代码
public boolean supportsParameter(MethodParameter parameter) {
    Class<?> paramType = parameter.getParameterType();
    return WebRequest.class.isAssignableFrom(paramType)
        || ServletRequest.class.isAssignableFrom(paramType)     // ← HttpServletRequest
        || HttpSession.class.isAssignableFrom(paramType)
        || InputStream.class.isAssignableFrom(paramType)
        || Reader.class.isAssignableFrom(paramType)
        // ... 其他 Servlet API 类型
        ;
}

解析逻辑:直接从 WebRequest 中取出原生 Request 返回:

java 复制代码
private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
    T nativeRequest = webRequest.getNativeRequest(requiredType);
    if (nativeRequest == null) {
        throw new IllegalStateException("Current request is not of type...");
    }
    return nativeRequest;
}

非常简单直接------HttpServletRequest 本身就在请求上下文中,不需要从参数中提取,直接返回引用即可


四、总结 ------ 参数解析全景图

复制代码
Controller 方法有参数需要解析
    ↓
getMethodArgumentValues() ------ 获取方法参数元数据数组
    ↓
for 循环遍历每个参数
    ↓
this.resolvers.supportsParameter(parameter)
    ├── 先查缓存(argumentResolverCache)
    └── 缓存未命中 → 遍历 27 个解析器
            ├── supportsParameter() 询问每个解析器
            └── 找到第一个支持的 → 放入缓存 + 执行 resolveArgument()
                    ├── 从 Request 中提取原始值(String)
                    ├── 处理默认值 / 缺失值
                    ├── WebDataBinder.convertIfNecessary() 类型转换
                    └── 返回转换后的值
    ↓
args[] 数组组装完成
    ↓
doInvoke(args) → 反射调用 Controller 方法

一句话总结

参数解析的本质就是策略模式 + 模板方法模式 + 缓存优化的三合一:遍历解析器列表找合适的策略,用模板方法统一处理"取值→默认值→类型转换"流程,用缓存保证 O(1) 的二次命中。

相关推荐
Wenzar_1 小时前
VITS+Whisper微调:低延迟TTS实战
java·人工智能·whisper
创可贴治愈心灵1 小时前
AI浪潮下C#就业前景剖析:深耕C#为主,按需选修Java与Python
java·人工智能·c#
huohaiyu1 小时前
深入解析Java垃圾回收机制
java·开发语言·算法·gc
JustHappy1 小时前
古法编程秘籍(五):什么是进程和线程?从软件到 CPU 的一次完整旅程
前端·后端·代码规范
SunnyDays10111 小时前
如何在 Java 中实现 OFD 与 PDF 格式互转
java·开发语言
BLSxiaopanlaile1 小时前
关于常见 map的一些比较探究
后端
花大师2 小时前
基于深度学习的鼠标轨迹真实性检测系统
后端
小江的记录本2 小时前
【Spring全家桶】Spring Cloud 2023.0.x:微服务核心理论、CAP/BASE定理(附《思维导图》+《面试高频考点清单》)
java·spring boot·后端·spring·spring cloud·微服务·面试
Solis程序员2 小时前
缓存三剑客预防策略
java·spring·缓存