springMVC九大组件及一次请求流程
一、九大组件
- HandlerMapping(处理器映射器)
HandlerMapping 是⽤来查找Handler的,也就是处理器,具体的表现形式可以是类,也可以是⽅法
。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler
。Handler负责具 体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器Handler 和 Interceptor.
- HandlerAdapter(处理器适配器)
HandlerAdapter 是⼀个适配器。因为 SpringMVC 中 Handler 可以是任意形式的,只要能处理请求即可
。但是把请求交给 Servlet 的时候,由于 Servlet 的⽅法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理⽅法调⽤ Handler 来进⾏处理,便是 HandlerAdapter 的职责。
注意:因为RequestMappingHandlerAdapter实现了 InitializingBean接口
,所以在其初始化之后,会调用afterPropertiesSet()。在该方法中对spring默认的方法参数解析器,方法返回值解析器,初始化绑定器进行了设置的
- 参数解析器
- 返回值处理器
- 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<FileName,File>这样的结构,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方法。
- 请求上下文
- processRequest方法
它首先备份了请求的LocaleContext和RequestAttributes
,然后copy了一份新的,并绑定到当前线程
:
//将地区(Locale)和请求属性以ThreadLocal的方法与当前线程进行关联,分别可以通过LocaleContextHolder和RequestContextHolder进行获取。
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());initContextHolders(request, localeContext, requestAttributes);
initContextHolders就是将localeContext和requestAttributes两个对象存入Holder。并且在方法最后,将备份的数据恢复过来,并触发请求处理完毕事件:
finally {resetContextHolders(request, previousLocaleContext, previousAttributes);if (requestAttributes != null) {requestAttributes.requestCompleted();}logResult(request, response, failureCause, asyncManager);publishRequestHandledEvent(request, response, startTime, failureCause);
}
备份和绑定操作完成后,调用它的核心方法doService,这是一个抽象方法,在DispatcherServlet实现,首先也是备份属性,并且最后也进行了恢复
:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {logRequest(request);// 如果该请求是include的请求(请求包含) 那么就把request域中的数据保存一份快照版本// 等doDispatch结束之后,会把这个快照版本的数据覆盖到新的request里面去Map<String, Object> attributesSnapshot = null;if (WebUtils.isIncludeRequest(request)) {attributesSnapshot = new HashMap<>();Enumeration<?> attrNames = request.getAttributeNames();while (attrNames.hasMoreElements()) {String attrName = (String) attrNames.nextElement();if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {attributesSnapshot.put(attrName, request.getAttribute(attrName));}}}// 把一些常用对象放进请求域,方便Handler里面可以随意获取// doService将webApplication、localeResolver、themeResolver、ThemeSource、outputFlashMap和flashMapManage设置到request的属性中,// 请求分发。request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());// 如果是重定向,放置得更多一些,比如flashMapManagerif (this.flashMapManager != null) {FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);if (inputFlashMap != null) {request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));}request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);}try {//然后将请求传入到doDispatch中,DispatcherServlet最重要的方法,交给他去分发请求、找到handler处理等等doDispatch(request, response);}finally {if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Restore the original attribute snapshot, in case of an include.if (attributesSnapshot != null) {restoreAttributesAfterInclude(request, attributesSnapshot);}}}}
- DispatcherServlet.doDispatch源码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 此处用processedRequest 需要注意的是:若是处理上传,processedRequest 将和request不再指向同一对象HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;//主要用来管理异步请求的处理。什么时候要用到异步处理呢?就是业务逻辑复杂(或者其他原因),为了避免请求线程阻塞,需要委托给另一个线程的时候。WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {//检查是否为上传文件请求//checkMultipart 这个方法很重要,判断是否是上传需求。且看下面的具体分析://如果请求是POST请求,并且请求头中的Context-Type是以multipart/开头的就认为是文件上传的请求processedRequest = checkMultipart(request);//标记一下:是否是文件上传的requestmultipartRequestParsed = (processedRequest != request);// 步骤1,获取执行链,重要(也就是找到某个controller中的某个方法)// 找到一个处理器,如果没有找到对应的处理类的话,这里通常会返回404,如果throwExceptionIfNoHandlerFound属性值为true的情况下会抛出异常mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// 步骤2,获取适配器,一般都是返回RequestMappingHandlerAdapter(用于处理@RequestMapping注解的方法)// 根据实际的handler去找到一个合适的HandlerAdapter,方法详细逻辑同getHandler,因此不再解释HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//如果是GET请求,如果内容没有变化的话,则直接返回,用于响应304状态码String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {//在客户端地一次输入URL时,服务器端会返回内容和状态码200, 表示请求成功,同时会添加一个“Last-Modified”属性,表示该请求资源的最后修改时间//客户端第二次请求此URL时,客户端会向服务器发送请求头 “IF-Modified-Since”,//如果服务端内容没有变化,则自动返回HTTP304状态码(只返回相应头信息,不返回资源文件内容,这样就可以节省网络带宽,提供响应速度和用户体验)long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}// 步骤3,拦截器pre方法,重要// 这段代码很有意思:执行处理器连里的拦截器们,具体参阅下面详细:if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}//步骤4,执行目标方法,真正处理逻辑,重要// 真正执行我们自己书写的controller方法的逻辑。返回一个ModelAndView// 这也是一个很复杂的过程(序列化、数据绑定等等),需要后面专题讲解// 这里一般最后都是由RequestMappingHandlerAdapter的invocableMethod.invokeHandle执行// 参数解析和返回值处理都在这个方法执行// 如果是@ResponseBody的方法,那么mv=nullmv = ha.handle(processedRequest, response, mappedHandler.getHandler());// 如果异步启动了,这里就先直接返回了,也就不会再执行拦截器PostHandle之类的if (asyncManager.isConcurrentHandlingStarted()) {return;}//意思是:如果我们没有设置viewName,就采用默认的。否则采用我们自己的applyDefaultViewName(processedRequest, mv);//步骤5,拦截器post方法,重要// 执行所有的拦截器的postHandle方法,并且把mv给他// 这里有一个小细节:这个时候拦截器是【倒序】执行的mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {dispatchException = new NestedServletException("Handler dispatch failed", err);}//步骤6,处理视图,重要//这个方法很重要,顾名思义,他是来处理结果的,渲染视图、处理异常等等的 下面详细分解//如果是@ResponseBody,那么这个方法里面的逻辑除了拦截器的逻辑,其他都不会执行processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {//步骤7,拦截器收尾方法,重要triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}
- checkMultipart
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {// 只有配置了multipartResolver,并且是文件上传的请求,才会继续往下走,没有的话就可以不必继续了。if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {// 如果是文件上传请求,则继续判断这个请求是不是已经被转换为MultipartHttpServletRequest类型了// 如果该请求已经是MultipartHttpServletRequest 那就输出一个日志走人//在Spring-Web这个jar中有一个过滤器org.springframework.web.multipart.support.MultipartFilter//如果在web.xml中配置这个过滤器的话,则会在过滤器中提前判断是不是文件上传的请求,//并将请求转换为MultipartHttpServletRequest类型。if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");}}else if (hasMultipartException(request)) {logger.debug("Multipart resolution previously failed for current request - " +"skipping re-resolution for undisturbed error rendering");}else {try {// 这里特别注意,不管是哪种multipartResolver的实现,内部都是new了一个新的MultipartHttpServletRequest的实现类,// 所以不再指向原来的request了,所以一定要注意// 将请求转换为MultipartHttpServletRequest类型return this.multipartResolver.resolveMultipart(request);}catch (MultipartException ex) {if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {logger.debug("Multipart resolution failed for error dispatch", ex);// Keep processing error dispatch with regular request handle below}else {throw ex;}}}}// If not returned before: return original request.// 如果前面没有返回,就原样返回,相当于啥都不做return request;}
更多关于文件上传处理的debug参考这篇文章
- 处理器查找:
为此请求返回HandlerExecutionChain
即为请求寻找合适的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进行查找的过程。下面重点是执行链的生成。getHandlerExecutionChain方法的原理就是从adaptedInterceptors中获得所有可以适配当前请求URL的MappedInterceptor并将其添加到HandlerExecutionChain的拦截器列表中
。拦截器的顺序其实就是我们定义/注册的顺序。从getCorsHandlerExecutionChain的源码中可以看出,对于跨域请求其实是向调用链插入了一个CorsInterceptor。
- 适配器查找:
一般都是返回RequestMappingHandlerAdapter(用于处理@RequestMapping注解的方法)
,根据实际的handler去找到一个合适的HandlerAdapter,方法详细逻辑同getHandler
- DispatcherServlet.getHandlerAdapter:
protected HandlerAdapter getHandlerAdapter(Object handler) {for (HandlerAdapter ha : this.handlerAdapters) {if (ha.supports(handler)) {return ha;}}
}
第一个适配器是RequestMappingHandlerAdapter,而其support方法直接返回true,这就导致了使用的适配器总是这一个
。
- 更多适配器
- SimpleControllerHandlerAdapter:适配SimpleUrlHandlerMapping和BeanNameUrlHandlerMapping的映射的,也就是实现Controller接口的Handler
- AbstractHandlerMethodAdapter:
适配RequestMappingHandlerMapping,也就是我们常用的RequestMapping注解
- HttpRequestHandlerAdapter :适配远程调用的
- SimpleServletHandlerAdapter:适配Servlet实现类的
- supports
public final boolean supports(Object handler) {return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));}
- supportsInternal
//总是返回true ,因为任何方法参数和返回值类型会以某种方式加以处理。//不被任何HandlerMethodArgumentResolver识别的方法参数被解释为一个请求参数,如果它是一个简单的类型,或者作为模型属性否则。//没有任何HandlerMethodReturnValueHandler识别的返回值将被解释为一个模型属性protected boolean supportsInternal(HandlerMethod handlerMethod) {return true;}
如果是GET请求,内容没有变化则直接返回
//如果是GET请求,如果内容没有变化的话,则直接返回,用于响应304状态码String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {//在客户端地一次输入URL时,服务器端会返回内容和状态码200, 表示请求成功,同时会添加一个“Last-Modified”属性,表示该请求资源的最后修改时间//客户端第二次请求此URL时,客户端会向服务器发送请求头 “IF-Modified-Since”,//如果服务端内容没有变化,则自动返回HTTP304状态码(只返回相应头信息,不返回资源文件内容,这样就可以节省网络带宽,提供响应速度和用户体验)long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}
- applyPreHandle
// 这段代码很有意思:执行处理器连里的拦截器们,具体参阅下面详细:
// 在Servlet规范中,设计了filter组件,可以在每个Web请求前后对它做处理,显然这种处理粒度太粗。Spring MVC增加了拦截器的概念,从HandlerMapping初始化和Handler查找的过程中,我们可以看到它的身影。
// 拦截器接口HandlerInterceptor定义了三个方法:preHandle、postHandle、afterCompletion,分别作用于处理器方法调用前后、处理器执行链全部执行后。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {for (int i = 0; i < interceptors.length; i++) {HandlerInterceptor interceptor = interceptors[i];// 注意:如果是拦截器返回了false,就立马触发所有拦截器的AfterCompletion 方法。并且马上return falseif (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}this.interceptorIndex = i;}}return true;}
- 请求处理:mv=ha.handle()
//步骤4,执行目标方法,真正处理逻辑,重要
// 真正执行我们自己书写的controller方法的逻辑。返回一个ModelAndView
// 这也是一个很复杂的过程(序列化、数据绑定等等),需要后面专题讲解
// 这里一般最后都是由RequestMappingHandlerAdapter的invocableMethod.invokeHandle执行
// 参数解析和返回值处理都在这个方法执行
// 并返回一个ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 如果异步启动了,这里就先直接返回了,也就不会再执行拦截器PostHandle之类的
if (asyncManager.isConcurrentHandlingStarted()) {return;}
- 以处理
有@PathVariable注解的方法为例
因为在 getHandlerAdapter中已经返回了一个适配器RequestMappingHandlerAdapter,
那么其调用链如下:
- mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
- RequestMappingHandlerAdapter.handerInternal
- RequestMappingHandlerAdapter.invokeHanderMethod
- ServletInvocableHandlerMethod.invokeAndHandle
- ServletInvocableHandlerMethod.invokeForRequest
- InvocableHandlerMethod.invokeForRequest
- InvocableHandlerMethod.getMethodArgumentValues
- InvocableHandlerMethod.resolveArgument
- HandlerMethodArgumentResolverComposite.resolveArgument
- AbstractNameedValueMethodArgumentResolver.resolveName
- PathVariableMethodArgumentResolver.resolveName (如果解析的是@PathVariable注解的参数)
- RequestMappingHandlerAdapter.handleInternal:
@Override
protected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod){ModelAndView mav;// Execute invokeHandlerMethod in synchronized block if required.// Session同步:可以看出,如果开启了synchronizeOnSession,那么同一个session的请求将会串行执行,//这一选项默认是关闭的,当然我们可以通过注入的方式进行改变。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 necessarymav = 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;
}
总之,在mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
中,会将方法参数进行解析,执行该方法,并封装一个ModelAndView返回
这一节可以参考这篇文章
这篇文章清楚说明了,因为RequestMappingHandlerAdapter实现了 InitializingBean接口
,所以在其初始化之后,会调用afterPropertiesSet()。
@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beans//首先执行此操作,它可能会添加 ResponseBody 建议 beaninitControllerAdviceCache();if (this.argumentResolvers == null) {//初始化SpringMVC默认的方法参数解析器,并添加至argumentResolvers(HandlerMethodArgumentResolverComposite)List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.initBinderArgumentResolvers == null) {//初始化SpringMVC默认的初始化绑定器(@InitBinder)参数解析器,并添加至initBinderArgumentResolvers(HandlerMethodArgumentResolverComposite)List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.returnValueHandlers == null) {//获取默认的方法返回值解析器List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}}
进一步的,我们可以看到spring默认的方法参数解析器,方法返回值解析器,初始化绑定器都是在这个方法中设置的
同时我们也知道,HandlerAdapter内部含有一组解析器负责对各类型的参数进行解析(策略模式)。下面我们就
再以
常用的自定义参数和Model
为例进行说明。自定义参数
解析由RequestParamMethodArgumentResolver
完成。
supportsParameter方法决定了一个解析器可以解析的参数类型,该解析器支持@RequestParam标准的参数或是简单类型的参数
,具体参见其注释。为什么此解析器可以同时解析@RequestParam注解和普通参数呢?玄机在于RequestMappingHandlerAdapter方法在初始化参数解析器时其实初始化了两个RequestMappingHandlerAdapter对象
,getDefaultArgumentResolvers方法相关源码:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));// Catch-allresolvers.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注解直接声明需要的参数名称。
- 返回值解析
返回值解析开始于ServletInvocableHandlerMethod.invokeAndHandle.invokeForRequest之后
//调用方法并通过其中一个处理返回值//ServletInvocableHandlerMethodpublic void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);//与@ResponseStatus相关的处理setResponseStatus(webRequest);//进行返回值处理if (returnValue == null) {// Request的NotModified为true有@ResponseStatus注解标注 RequestHandled=true 三个条件有一个成立,则设置请求处理完成并返回if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {disableContentCachingIfNecessary(webRequest);// 设置该请求已被处理mavContainer.setRequestHandled(true);return;}}//返回值不为null,@ResponseStatus存在reason 同样设置请求处理完成并返回else if (StringUtils.hasText(getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}// 前边都不成立,则设置RequestHandled=false即请求未完成// 继续交给HandlerMethodReturnValueHandlerComposite处理mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");//利用所有返回值处理器来处理返回值try {//处理返回值this.returnValueHandlers.handleReturnValue(returnValue,//获取返回值类型,并传入到方法中getReturnValueType(returnValue), mavContainer, webRequest);}catch (Exception ex) {if (logger.isTraceEnabled()) {logger.trace(formatErrorForReturnValue(returnValue), ex);}throw ex;}}
- 以标注了@ResponseBody的场景为例子
@Overridepublic 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());}//利用得到的返回值处理器对返回值进行处理//HandlerMethodReturnValueHandler.handleReturnValuehandler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);}@Nullableprivate HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {//先判断是不是异步返回值,遍历循环找到AsyncHandlerMethodReturnValueHandler进行判断boolean isAsyncValue = isAsyncReturnValue(value, returnType);//遍历returnValueHandlersfor (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {continue;}//判断当前HandlerMethodReturnValueHandler是否能够支持当前返回值的解析//HandlerMethodReturnValueHandler.supportsReturnType//例子:如果标了@ResponseBody,那么利用RequestResponseBodyMethodProcessor进行解析if (handler.supportsReturnType(returnType)) {return handler;}}return null;}
- 进入
RequestResponseBodyMethodProcessor
进行处理
//RequestResponseBodyMethodProcessorpublic 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);}
这里又涉及到一个知识点,消息转换器Converter:通俗的理解就是看能不能将Class对象转为MediaType类型的数据(包括写入写出)
。这里只展示部分实现类:
了解更多请参考
而在应该消息转换器前,
还会有一步与浏览器与服务器内容协商的过程
,比如浏览器能接受json类型的,服务器也能接受json类型的,那么就可以以json的类型发送数据,然后应用消息转换器,将返回值转成json数据
- writeWithMessageConverters
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {//用于接收Controller返回值Object body;//声明接收返回对象类型Class<?> valueType;Type targetType;if (value instanceof CharSequence) {body = value.toString();valueType = String.class;targetType = String.class;}else {body = value;valueType = getReturnValueType(body, returnType);targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());}if (isResourceType(value, returnType)) {outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&outputMessage.getServletResponse().getStatus() == 200) {Resource resource = (Resource) value;try {List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());body = HttpRange.toResourceRegions(httpRanges, resource);valueType = body.getClass();targetType = RESOURCE_REGION_LIST_TYPE;}catch (IllegalArgumentException ex) {outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());}}}// 选择使用的 MediaTypeMediaType selectedMediaType = null;MediaType contentType = outputMessage.getHeaders().getContentType();if (contentType != null && contentType.isConcrete()) {if (logger.isDebugEnabled()) {logger.debug("Found 'Content-Type:" + contentType + "' in response");}selectedMediaType = contentType;}else {HttpServletRequest request = inputMessage.getServletRequest();// 获取浏览器能接受的MediaType(Accept-Type)List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);// 服务器能产生的MediaType(即@RequeMapping中指定的produces)List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);if (body != null && producibleTypes.isEmpty()) {throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);}//声明匹配上的MediaTypesList<MediaType> mediaTypesToUse = new ArrayList<>();//循环匹配,协商MediaTypefor (MediaType requestedType : acceptableTypes) {for (MediaType producibleType : producibleTypes) {if (requestedType.isCompatibleWith(producibleType)) {mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));}}}//没匹配上MediaTypes就报错if (mediaTypesToUse.isEmpty()) {if (body != null) {throw new HttpMediaTypeNotAcceptableException(producibleTypes);}if (logger.isDebugEnabled()) {logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);}return;}MediaType.sortBySpecificityAndQuality(mediaTypesToUse);for (MediaType mediaType : mediaTypesToUse) {if (mediaType.isConcrete()) {selectedMediaType = mediaType;break;}else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;break;}}if (logger.isDebugEnabled()) {logger.debug("Using '" + selectedMediaType + "', given " +acceptableTypes + " and supported " + producibleTypes);}}//如果匹配到,则进行写入逻辑if (selectedMediaType != null) {//移除 quality 。例如,application/json;q=0.8 移除后为 application/jsonselectedMediaType = selectedMediaType.removeQualityValue();//遍历 messageConverters 数组for (HttpMessageConverter<?> converter : this.messageConverters) {//判断 HttpMessageConverter 是否支持转换目标类型GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {//拿到我们要响应的内容body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);//body 非空,则进行写入if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");//加头部addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {//调用消息转换器的write方法,将数据写出去genericConverter.write(body, targetType, selectedMediaType, outputMessage);}else {((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}// return 返回,结束整个逻辑return;}}}
这里有一点需要注意:使用@ResponseBody注解的类,不会经过视图解析,因为
//在使用@ResponseBody,mv=null
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
更多关于@ResponseBody的分析,参考
至此,一次@ResponseBody的解析就完成了,但我们也知道,springMVC也有视图解析的方式,直接给用户返回一个渲染后的页面,只不过在前后分离的趋势下,越来越少用
- 返回页面的逻辑
套路和上面是一样的,通常情况,我们返回的其实是view名,只不过返回的负责处理的returnValueHandlers是ViewNameMethodReturnValueHandler
,supportsReturnType方法:
@Override
public boolean supportsReturnType(MethodParameter returnType) {Class<?> paramType = returnType.getParameterType();return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}@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);}}
}
可见这里并没有进行实际的处理,只是解析得到了最终的视图名称。
- 目标方法处理的过程中,所有数据都会被放在
ModelAndViewContainer
里面。包括数据和视图地址
- 方法的参数是一个
自定义类型对象
(从请求参数中确定的),把他重新放在 ModelAndViewContainer
- 任何目标方法执行完成以后都会返回
ModelAndView(数据和视图地址)
。
bash这次返回的mv就不再是null了,
就可以进行视图解析了
- applyPostHandle:
执行拦截器post方法
//意思是:如果我们没有设置viewName,就采用默认的。否则采用我们自己的
applyDefaultViewName(processedRequest, mv);
// 步骤5,拦截器post方法,重要
// 执行所有的拦截器的postHandle方法,并且把mv给他
// 这里有一个小细节:这个时候拦截器是【倒序】执行的
mappedHandler.applyPostHandle(processedRequest, response, mv);
- 视图渲染:
processDispatchResult
,进行处理结果,渲染视图、处理异常
等等
- 由DispatcherServlet的
processDispatchResult
方法完成,源码:
//exception 执行处理器方法报错时被捕获的异常//如果是@ResponseBody,那么这个方法里面的逻辑除了拦截器的逻辑,其他都不会执行private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;//如果有异常,就进入异常处理逻辑,返回到异常页面if (exception != null) {// 含有异常页面视图的异常if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);//1、会执行所有的我们的自己配置(或者默认配置)了的HandlerExceptionResolver处理器//2、上面需要注意了,但凡处理方法返回的不是null,有mv的返回。那后面的处理器就不会再进行处理了。具有短路的效果,一定要注意 是通过null来判断的//3、处理完成后,得到error的视图mv,最后会设置一个viewName,然后返回出去mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// Did the handler return a view to render?// 如果mv不为空,且没有被清理,也就是请求在此之前还没有响应浏览器,就开始执行render()方法,开始渲染视图了if (mv != null && !mv.wasCleared()) {render(mv, request, response);// 如果有错误视图,这里清除掉所有的请求域里的所有的错误属性if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}else {if (logger.isTraceEnabled()) {logger.trace("No view rendering, null ModelAndView returned.");}}//处理异步=========我们发现,它不执行后面的AfterCompletion方法了,注意一下即可if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Concurrent handling started during a forwardreturn;}// 执行拦截器的AfterCompletion 方法if (mappedHandler != null) {mappedHandler.triggerAfterCompletion(request, response, null);}}
可以看出,处理根据是否抛出异常分为了两种情况
。
如果抛出了异常,那么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>
- DispatcherServlet.render()
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {// Determine locale for request and apply it to the response.// 通过localeResolver吧local解析出来,放到response里面去Locale locale =(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());response.setLocale(locale);//==================视图:关键中的关键==================View view;//获取视图名称String viewName = mv.getViewName();// 如果已经有viewName了(绝大多数情况)if (viewName != null) {// We need to resolve the view name.// 视图解析器 根据String类型的名字,解析出来一个视图(视图解析器有多个)// 还是那个原理:只要有一个返回了不为null的,后面的就不会再解析了// 根据我们的视图名称 解析成为我们真正的物理视图(通过视图解析器对象)view = resolveViewName(viewName, mv.getModelInternal(), locale, request);// 如果解析不出来视图,那就抛出异常,说不能解析该视图if (view == null) {throw new ServletException("Could not resolve view with name '" + mv.getViewName() +"' in servlet with name '" + getServletName() + "'");}}else {//没有视图名称,但是必须有视图内容,否则抛出异常// No need to lookup: the ModelAndView object contains the actual View object.view = mv.getView();if (view == null) {throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +"View object in servlet with name '" + getServletName() + "'");}}// Delegate to the View object for rendering.if (logger.isTraceEnabled()) {logger.trace("Rendering view [" + view + "] ");}try {//设置响应码 statusif (mv.getStatus() != null) {response.setStatus(mv.getStatus().value());}// 根据model里的数据,正式渲染(关于此部分逻辑,后续再说,也比较复杂)view.render(mv.getModelInternal(), request, response);}catch (Exception ex) {if (logger.isDebugEnabled()) {logger.debug("Error rendering view [" + view + "]", ex);}throw ex;}}
- DispatcherServlet.resolveViewName()
//解析viewName返回一个View对象
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception {//判断当前的视图解析器集合是否为空if (this.viewResolvers != null) {//循环调用我们的视图解析器对象解析视图for (ViewResolver viewResolver : this.viewResolvers) {// 一旦有我们的视图解析器能够解析出视图,后面的视图解析器不在参与解析,直接返回View view = viewResolver.resolveViewName(viewName, locale);if (view != null) {return view;}}}return null;}
resolveViewName方法将会遍历所有的ViewResolver bean,只要有一个解析的结果(View)不为空,即停止遍历。
根据MVC初始化-ViewResolver检查一节和我们的配置文件可知,容器中有两个ViewResolver ,分别是: InternalResourceViewResolver和UrlBasedViewResolver
。resolveViewName其实只做了一件事: 用反射创建并初始化我们指定的View,根据我们的配置,就是JstlView
。
- processDispatchResult :处理派发结果(小结,
以redirect为例
)
- render(mv, request, response); 进行页面渲染逻辑
- 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
■ 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
■ 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
■ 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器
,内部还是利用下面所有视图解析器得到视图对象。
■ 4、view.render(mv.getModelInternal(), request, response);视图对象调用自定义的render进行页面渲染工作
4.1 RedirectView如何渲染【重定向到一个页面】
4.2 获取目标URL地址
4.3 response.sendRedirect(encodeURL)
- 视图解析小结:
- 返回值
有异常
: 遍历所有的HandlerExceptionResolver实例进行处理,或者向上抛给Tomcat- 返回值
以 forward
: 开始: new InternalResourceView(forwardUrl); -----> 转发request.getRequestDispatcher(path).forward(request, response);- 返回值
以 redirect
: 开始: new RedirectView() —> render就是重定向- 返回值是
普通字符串
:new ThymeleafView()[假设是使用Thymeleaf模板引擎]—>
此处理器会返回一个非空的ModelAndView。然后使用writer将数据写入到前端
我们可以自定义视图解析器+自定义视图
- ModelAndView
回过头来看一下这到底是个什么东西。类图: ModelAndView类图
很直白。
怎么生成的。RequestMappingHandlerAdapter.getModelAndView相关源码:
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
- ViewResolver
resolveViewName方法的源码不再贴出,其实只做了一件事: 用反射创建并初始化我们指定的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进行转发的过程。
- Model、View、ModelAndView对比
View为服务器上的某个文件容器`,可以为JSP,FTL等动态页面文件,甚至是媒体文件等等,单单是一个文件。
Model的作用是存储动态页面属性,动态页面文件即View可以在Model中获取动态数据,
Model其实是一个ModelMap类型,它是一个LinkedHashMap的子类
,作用类似于request对象的setAttribute方法的作用
(数据在一次请求转发中有效),用来在一个请求过程中传递处理的数据。ModelAndView,顾名思义,
就是整合了Model和View,常用于动态页面
,一个后台网页就包含这两部分,前台就是基本的html代码可以参考该文章
- 总结图
其实,在如今的前后端分离的浪潮下,试图解析这一流程也是越来越少,大部分都是返回数据供前端渲染
参考文章
参考文章
springMVC九大组件及一次请求流程相关推荐
- springmvc十六:九大组件
DispatcherServlet中有九个引用类型的属性,这就是springmvc的九大组件. springmvc在工作的时候,关键位置都是由这些组件完成的. /** MultipartResolve ...
- Openstack九大组件
云是服务 云,本身没有资源,只是去管理和调度资源 虚拟化是技术 虚拟化提供资源,在云的世界里,虚拟化提供底层的计算.存储.网络资源,由云来接管 云的五大特征: 按需自助服务 广泛的网络接入 资源池化 ...
- 【springmvc】九大组件之HandlerExceptionResolver
在Spring MVC中,如果对异常不做任何处理,Spring MVC会将异常直接抛给容器. 例如下面的代码抛出了异常: @GetMapping("e1") public Stri ...
- springmvc源码阅读3--dispatcherServlet reqeust的执行流程
一.前言 太多的时候总是会遇到群里面问,报404怎么肥事呀,前台的参数怎么后台收不到呀--,上次就在群里面遇到过,围绕这一个点:input的name值是不是错了呀,人家妹子都截了好几次图说没有问题,然 ...
- ajax请求方式 问答题,java spring mvc面试题,九大常见问答题
上次已经为大家介绍过java spring面试题,八大常见问答题的主要内容了,今天再来为大家介绍一些其他的常见问答题,一起来了解一下吧. 常见问答题如下: 1.springmvc的控制器是不是单例模式 ...
- 一个字稳,云原生产品家族支撑冬奥会九大业务场景,打造云上奥运新体验
北京冬奥会已经成为收视最高的一届冬奥会,在转播时长.技术.内容制作方式等多方面都书写了新记录.云技术的应用,是本届北京冬奥会赛事转播的一大特色. 而云原生作为云计算的新界面,如何稳定支撑北京冬奥会多个 ...
- springMVC请求流程详解
SpringMVC框架是一个基于请求驱动的Web框架,并且使用了'前端控制器'模型来进行设计, 再根据'请求映射规则'分发给相应的页面控制器进行处理.核心流程: 第一步:发起请求到前端控制器(Disp ...
- 《微服务》九大特性重读笔记
今天重读了Martin Fowler的<Microservices>,在此记录一下对九大特性的理解. 服务组件化 组件,是一个可以独立更换和升级的单元.就像PC中的CPU.内存.显卡.硬盘 ...
- 信息系统项目管理师-九大知识领域必备知识点整理
场景 在备考阶段,九大知识领域有关的知识重点需要背诵的内容整理. 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获取编程相 ...
最新文章
- liunx 上get 不到url参数 java_thinkphp5.0 模板上直接获取url参数
- 图论刷水题记录(一)(最短路-----dijkstra算法)
- lsit集合去重复 顶级表达式
- [转载] 晓说——第15期:揭秘欧洲列强恩仇录
- OnClick,OnClientClick和OnServerClick的区别
- 【NOSQL 】 memcache 安装及配置分布式集群 双向复制
- 一步步创建 边栏 Gadget(一)
- CodeForces - 1437E Make It Increasing(确定首尾的最长不下降子序列)
- CCNA试验-1标准acl
- 计算机硬件基础英语ppt,计算机硬件技术基础,computer hardware technology elements,音标,读音,翻译,英文例句,英语词典...
- 一个JavaScript的小问题
- element-ui分页器的使用
- TFTP服务器的使用
- java数字转读音_java 数字转汉语读音的程序
- OpenCV笔记_20(1)基于dlib进行人脸识别( 图片检测 )
- python 文件指针详解、文件基本操作方法及在文件起始位置插入内容
- 精准目标群体,精确博客选择——谈feedsky经典博客Market力作
- python 通达信公式函数_通达信zig函数的python实现
- 数据结构实验6_压缩矩阵的转置、普通转置、快速转置算法
- 使用Keras训练Lenet网络来进行手写数字识别