返回介绍

2.3 请求处理

发布于 2025-10-03 18:08:54 字数 12745 浏览 0 评论 0 收藏

RequestMappingHandlerAdapter.handleInternal:

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod){
    ModelAndView mav;
    // Execute invokeHandlerMethod in synchronized block if required.
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
            // No HttpSession available -> no mutex necessary
            mav = invokeHandlerMethod(request, response, handlerMethod);
        }
    } else {
        // No synchronization on session demanded at all...
        mav = invokeHandlerMethod(request, response, handlerMethod);
    }
    if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
        if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
        }
        else {
            prepareResponse(response);
        }
    }
    return mav;
}

Session 同步

可以看出,如果开启了 synchronizeOnSession,那么 同一个 session 的请求将会串行执行 ,这一选项默认是关闭的,当然我们可以通过注入的方式进行改变。

参数解析

策略模式

正如前面 HandlerAdapter 初始化-参数解析器一节提到的,HandlerAdapter 内部含有一组解析器负责对各类型的参数进行解析。下面我们就常用的自定义参数和 Model 为例进行说明。

自定义参数

解析由 RequestParamMethodArgumentResolver 完成。

supportsParameter 方法决定了一个解析器可以解析的参数类型,该解析器支持 @RequestParam 标准的参数或是 简单类型 的参数,具体参见其注释。为什么此解析器可以同时解析 @RequestParam 注解和普通参数呢?玄机在于 RequestMappingHandlerAdapter 方法在初始化参数解析器时其实初始化了 两个 RequestMappingHandlerAdapter 对象 ,getDefaultArgumentResolvers 方法相关源码:

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    // Catch-all
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
}

useDefaultResolution 参数用于启动对常规类型参数的解析,这里的常规类型指的又是什么呢?

实际上由 BeanUtils.isSimpleProperty 方法决定:

public static boolean isSimpleProperty(Class<?> clazz) {
    Assert.notNull(clazz, "Class must not be null");
    return isSimpleValueType(clazz) || (clazz.isArray() && isSimpleValueType(clazz.getComponentType()));
}

public static boolean isSimpleValueType(Class<?> clazz) {
    return (ClassUtils.isPrimitiveOrWrapper(clazz) || clazz.isEnum() ||
            CharSequence.class.isAssignableFrom(clazz) ||
            Number.class.isAssignableFrom(clazz) ||
            Date.class.isAssignableFrom(clazz) ||
            URI.class == clazz || URL.class == clazz ||
            Locale.class == clazz || Class.class == clazz);
}

忽略复杂的调用关系,最核心的实现位于 resolveName 方法,部分源码:

@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) {
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}

name 就是方法的参数名,可以看出,参数解析 就是根据参数名去 request 查找对应属性的过程 ,在这里参数类型并没有起什么作用。

参数名是从哪里来的

方法名获取的入口位于 RequestParamMethodArgumentResolver 的 resolveArgument 方法:

@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
}

getNamedValueInfo 方法最终完成对 MethodParameter 的 getParameterName 方法的调用:

public String getParameterName() {
    ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer;
    if (discoverer != null) {
        String[] parameterNames = (this.method != null ?
                discoverer.getParameterNames(this.method) : discoverer.getParameterNames(this.constructor));
        if (parameterNames != null) {
            this.parameterName = parameterNames[this.parameterIndex];
        }
        this.parameterNameDiscoverer = null;
    }
    return this.parameterName;
}

显然,参数名的获取由接口 ParameterNameDiscoverer 完成:

ParameterNameDiscoverer

默认采用 DefaultParameterNameDiscoverer,但此类其实相当于 StandardReflectionParameterNameDiscoverer 和 LocalVariableTableParameterNameDiscoverer 的组合,且前者先于后者进行解析。

StandardReflectionParameterNameDiscoverer.getParameterNames:

@Override
public String[] getParameterNames(Method method) {
    Parameter[] parameters = method.getParameters();
    String[] parameterNames = new String[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        Parameter param = parameters[i];
        if (!param.isNamePresent()) {
            return null;
        }
        parameterNames[i] = param.getName();
    }
    return parameterNames;
}

此类被注解 UsesJava8 标注,其原理就是利用的 jdk8 的-parameters 编译参数,只有在加上此选项的情况下才能用反射的方法获得真实的参数名,所以一般情况下 StandardReflectionParameterNameDiscoverer 是无法成功获取参数名的。

LocalVariableTableParameterNameDiscoverer 利用了 ASM 直接访问 class 文件中的本地变量表来得到变量名,下面是使用 javap -verbose 命令得到的本地变量表示例:

本地变量表

但是默认情况下 javac compiler 是不生成本地变量表这种调试信息的,需要加 -g 参数才可以,那为什么在我们的测试 Controller 中却可以获得呢,玄机就在于 idea 的下列设置:

idea 编译设置

取消这项设置的勾选再次运行程序便出问题了:

调试信息错误

Model

解析由 ModelMethodProcessor 完成。

supportsParameter 方法很简单:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return Model.class.isAssignableFrom(parameter.getParameterType());
}

很直白了。

resolveArgument:

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    return mavContainer.getModel();
}

忽略各种调用关系, Model 其实是一个 BindingAwareModelMap 对象,且每次请求(需要注入 Model 的前提下) 都有一个新的该对象生成 。类图:

BindingAwareModelMap 类图

总结

  • 我们可以通过实现 HandlerMethodArgumentResolver 接口并将其注册容器的方式实现自定义参数类型的解析。
  • 为了防止出现参数名获取不到的问题,应优先使用 @RequestParam 注解直接声明需要的参数名称。

返回值解析

套路和上面是一样的,通常情况,我们返回的其实是 view 名,负责处理的是 ViewNameMethodReturnValueHandler,

supportsReturnType 方法:

@Override
public boolean supportsReturnType(MethodParameter returnType) {
    Class<?> paramType = returnType.getParameterType();
    return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}

handleReturnValue:

@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
    if (returnValue instanceof CharSequence) {
        String viewName = returnValue.toString();
        mavContainer.setViewName(viewName);
         // 判断的依据: 是否以 redirect:开头
        if (isRedirectViewName(viewName)) {
            mavContainer.setRedirectModelScenario(true);
        }
    }
}

可见这里并没有进行实际的处理,只是解析得到了最终的视图名称。

视图渲染

由 DispatcherServlet 的 processDispatchResult 方法完成,源码:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) {
    boolean errorView = false;
    if (exception != null) {
         //一般不会到这个分支
        if (exception instanceof ModelAndViewDefiningException) {
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        } else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }
    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
}

可以看出,处理 根据是否抛出异常分为了两种情况

如果抛出了异常,那么 processHandlerException 方法将会遍历所有的 HandlerExceptionResolver 实例,默认有哪些参考 MVC 初始化-HandlerExceptionResolver 检查一节。默认的处理器用于改变响应状态码、调用标注了 @ExceptionHandler 的 bean 进行处理,如果没有 @ExceptionHandler 的 bean 或是不能处理此类异常,那么就会导致 ModelAndView 始终为 null,最终 Spring MVC 将异常向上抛给 Tomcat,然后 Tomcat 就会把堆栈打印出来。

如果我们想将其定向到指定的错误页面,可以这样配置:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="defaultErrorView" value="error"></property>
</bean>

此处理器会返回一个非空的 ModelAndView。

ModelAndView

回过头来看一下这到底是个什么东西。类图:

ModelAndView 类图

很直白。

怎么生成的。RequestMappingHandlerAdapter.getModelAndView 相关源码:

ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());

渲染

DispatcherServlet.render 简略版源码:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) {
    Locale locale = this.localeResolver.resolveLocale(request);
    response.setLocale(locale);
    View view;
    //判断依据: 是否是 String 类型
    if (mv.isReference()) {
        // We need to resolve the view name.
        view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
    } else {
        // No need to lookup: the ModelAndView object contains the actual View object.
        view = mv.getView();
    }
    if (mv.getStatus() != null) {
        response.setStatus(mv.getStatus().value());
    }
    view.render(mv.getModelInternal(), request, response);
}

resolveViewName 方法将会遍历所有的 ViewResolver bean,只要有一个解析的结果(View) 不为空,即停止遍历。根据 MVC 初始化-ViewResolver 检查一节和我们的配置文件可知,容器中有两个 ViewResolver ,分别是: InternalResourceViewResolver 和 UrlBasedViewResolver。

ViewResolver

类图(忽略实现类):

ViewResolver 类图

resolveViewName 方法的源码不再贴出,其实只做了一件事: 用反射创建并初始化我们指定的 View,根据我们的配置,就是 JstlView。

View

类图:

JstlView 类图

渲染的核心逻辑位于 InternalResourceView.renderMergedOutputModel,简略版源码:

@Override
protected void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
    // 将 Model 中的属性设置的 request 中
    exposeModelAsRequestAttributes(model, request);
    // 获取资源(jsp) 路径
    String dispatcherPath = prepareForRendering(request, response);
    // Obtain a RequestDispatcher for the target resource (typically a JSP).
    RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
    // If already included or response already committed, perform include, else forward.
    if (useInclude(request, response)) {
        response.setContentType(getContentType());
        rd.include(request, response);
    } else {
        // Note: The forwarded resource is supposed to determine the content type itself.
        rd.forward(request, response);
    }
}

可以看出,对 jsp 来说,所谓的渲染其实就是 将 Model 中的属性设置到 Request,再利用原生 Servlet RequestDispatcher API 进行转发的过程

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。