一、九大组件
- 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<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方法。
- 请求上下文
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);
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;
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
} else {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
} else {
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的请求将会串行执行,这一选项默认是关闭的 ,当然我们可以通过注入的方式进行改变。
- 参数解析
解析由RequestParamMethodArgumentResolver完成。
supportsParameter方法决定了一个解析器可以解析的参数类型,该解析器支持@RequestParam标准的参数或是简单类型的参数,具体参见其注释。为什么此解析器可以同时解析@RequestParam注解和普通参数呢?玄机在于RequestMappingHandlerAdapter方法在初始化参数解析器时其实初始化了两个RequestMappingHandlerAdapter对象,getDefaultArgumentResolvers方法相关源码:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
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);
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);
}
}
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;
if (mv.isReference()) {
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
} else {
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<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
exposeModelAsRequestAttributes(model, request);
String dispatcherPath = prepareForRendering(request, response);
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (useInclude(request, response)) {
response.setContentType(getContentType());
rd.include(request, response);
} else {
rd.forward(request, response);
}
}
可以看出,对jsp来说,所谓的渲染其实就是将Model中的属性设置到Request,再利用原生Servlet RequestDispatcher API进行转发的过程。
- 总结图
其实,在如今的前后端分离的浪潮下,试图解析这一流程也是越来越少,大部分都是返回数据供前端渲染
|