栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

springMVC九大组件及一次请求流程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

springMVC九大组件及一次请求流程

一、九大组件
    HandlerMapping(处理器映射器)

HandlerMapping 是⽤来查找Handler的,也就是处理器,具体的表现形式可以是类,也可以是⽅法。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler。Handler负责具 体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器Handler 和 Interceptor.

    HandlerAdapter(处理器适配器)

HandlerAdapter 是⼀个适配器。因为 SpringMVC 中 Handler 可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的⽅法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理⽅法调⽤ Handler 来进⾏处理,便是 HandlerAdapter 的职责。

    HandlerExceptionResolver

HandlerExceptionResolver ⽤于处理 Handler 产⽣的异常情况。它的作⽤是根据异常设置 ModelAndView,之后交给渲染⽅法进⾏渲染,渲染⽅法会将 ModelAndView 渲染成⻚⾯。

    ViewResolver

ViewResolver即视图解析器,⽤于将String类型的视图名和Locale解析为View类型的视图,只有⼀ 个resolveViewName()⽅法。从⽅法的定义可以看出,Controller层返回的String类型视图名viewName 最终会在这⾥被解析成为View。View是⽤来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件。ViewResolver在这个过程主要完成两件事情: ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个InternalResourceViewResolver,是针对 JSP 类型视图的。

    RequestToViewNameTranslator

RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName.因为ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,便要通过这个组件从请求中查找 ViewName。

    LocaleResolver

ViewResolver组件的 resolveViewName ⽅法需要两个参数,⼀个是视图名,⼀个是 Locale。LocaleResolver ⽤于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,⽤来表示⼀个区域。这个组件也是 i18n 的基础。

    ThemeResolver

ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。 Spring MVC 中⼀套主题对应⼀个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等。创建主题⾮常简单,只需准备好资源,然后新建⼀个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使⽤了。SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名, ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。

    MultipartResolver

MultipartResolver ⽤于上传请求,通过将普通的请求包装成MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() ⽅法 直接获得⽂件。如果上传多个⽂件,还可以调⽤ getFileMap()⽅法得到Map这样的结构,MultipartResolver 的作⽤就是封装普通的请求,使其拥有⽂件上传的功能。

    FlashMapManager

FlashMap ⽤于重定向时的参数传递,⽐如在处理⽤户订单时候,为了避免重复提交,可以处理完post请求之后重定向到⼀个get请求,这个get请求可以⽤来显示订单详情之类的信息。这样做虽然 可以规避⽤户重新提交订单的问题,但是在这个⻚⾯上要显示订单的信息,这些数据从哪⾥来获得呢?因为重定向没有传递参数这⼀功能的,如果不想把参数写进URL(不推荐),那么就可以通 过FlashMap来传递。只需要在重定向之前将要传递的数据写⼊请求(可以通过 ServletRequestAttributes.getRequest()⽅法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯上就可以直接从Model中获取数据。FlashMapManager 就是⽤来管理 FalshMap 的。

二、SpringMVC的一次请求

我们先来看一下入口在哪。众所周知,Servlet标准定义了所有请求先由service方法处理,如果是get或post方法,那么再交由doGet或是doPost方法处理。

frameworkServlet覆盖了service方法:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) {
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (HttpMethod.PATCH == httpMethod || httpMethod == null) {
        processRequest(request, response);
    } else {
        super.service(request, response);
    }
}

Spring要覆盖此方法的目的在于拦截PATCH请求,PATCH请求与PUT类似,不同在于PATCH是局部更新,而后者是全部更新。frameworkServlet同样也覆盖了doGet和doPost方法,两者只是调用processRequest方法。

    请求上下文
    Spring MVC会在请求分发之前进行上下文的准备工作,含两部分:

将地区(Locale)和请求属性以ThreadLocal的方法与当前线程进行关联,分别可以通过LocaleContextHolder和RequestContextHolder进行获取。将WebApplicationContext、FlashMap等组件放入到Request属性中。 请求分发

DispatcherServlet.doDispatch简略版源码:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    HandlerExecutionChain mappedHandler = getHandler(processedRequest);
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    applyDefaultViewName(processedRequest, mv);
    mappedHandler.applyPostHandle(processedRequest, response, mv);
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
    处理器查找
    即为请求寻找合适的Controller的过程。DispatcherServlet.getHandler:
protected HandlerExecutionChain getHandler(HttpServletRequest request) {
    for (HandlerMapping hm : this.handlerMappings) {
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
            return handler;
        }
    }
    return null;
}

从这里可以看出,寻找处理器实际上委托给HandlerMapping实现,寻找的过程便是遍历所有的HandlerMapping进行查找,一旦找到,那么不再继续进行遍历。也就是说HandlerMapping之间有优先级的概念,而根据AnnotationDrivenBeanDefinitionParser的注释,RequestMappingHandlerMapping其实有最高的优先级。

AbstractHandlerMapping.getHandler:

@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
    //判断请求头中是否有ORIGIN字段
    if (CorsUtils.isCorsRequest(request)) {
        CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        CorsConfiguration config = (globalConfig != null ? 
            globalConfig.combine(handlerConfig) : handlerConfig);
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}

getHandlerInternal方法便是根据url进行查找的过程,可以参见MVC初始化-HandlerMapping初始化一节。下面重点是执行链的生成。getHandlerExecutionChain方法的原理就是从adaptedInterceptors中获得所有可以适配当前请求URL的MappedInterceptor并将其添加到HandlerExecutionChain的拦截器列表中。拦截器的顺序其实就是我们定义/注册的顺序。从getCorsHandlerExecutionChain的源码中可以看出,对于跨域请求其实是向调用链插入了一个CorsInterceptor。

    适配器查找
    DispatcherServlet.getHandlerAdapter:
protected HandlerAdapter getHandlerAdapter(Object handler) {
    for (HandlerAdapter ha : this.handlerAdapters) {
        if (ha.supports(handler)) {
            return ha;
        }
    }
}

从前面配置解析-注解驱动可以看出,第一个适配器是RequestMappingHandlerAdapter,而其support方法直接返回true,这就导致了使用的适配器总是这一个。

    请求处理

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内部含有一组解析器负责对各类型的参数进行解析。下面我们就常用的自定义参数和Model为例进行说明。

自定义参数

解析由RequestParamMethodArgumentResolver完成。

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

private List 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完成:

默认采用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;
}
    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的前提下)都有一个新的该对象生成。类图:

总结

我们可以通过实现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就会把堆栈打印出来。

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


    

此处理器会返回一个非空的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

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

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

@Override
protected void renderMergedOutputModel(
        Map 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进行转发的过程。

    总结图

其实,在如今的前后端分离的浪潮下,试图解析这一流程也是越来越少,大部分都是返回数据供前端渲染

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/763253.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号