SpingMVC 模块
简介
Spring MVC是一种基于MVC架构模式的轻量级Web框架。
SpringMVC处理过程
Spring MVC的处理过程:
-
接收用户的请求DispatcherServlet
- 找到用于处理request的 handler 和Interceptors,构造成 HandlerExecutionChain执行链
- 找到 handler 相对应的 HandlerAdapter
- 执行所有注册拦截器的preHandler方法
- 调用 HandlerAdapter 的 handle() 方法处理请求,返回 ModelAndView
- 倒序执行所有注册拦截器的postHandler方法
- 请求视图解析和视图渲染
处理流程中各个组件的功能:
- 前端控制器(DispatcherServlet):接收用户请求,给用户返回结果。
- 处理器映射器(HandlerMapping):根据请求的url路径,通过注解或xml配置,寻找匹配的Handler。
- 处理器适配器(HandlerAdapter):Handler的适配器,调用 handler 的方法处理请求。
- 处理器(Handler):执行相关的请求处理逻辑,并返回响应的数据和视图信息,将其封装到ModelAndView对象中。
- 视图解析器(ViewResolver):将逻辑视图名解析成真正的视图View。
- 视图(View):接口类,实现类可支持不同的View类型(JSP、FreeMarker、Excel等)。
SpringMVC web.xml配置:
<!-- 配置applicationContext.xml
如果不写任何参数,默认位置在WEB-INF下,且默认名字为applicationContext.xml
如果想自定义文件名,需要在web.xml中加入contextConfigLocation这个context参数
<context-param>:该元素用来声明应用范围(整个WEB项目)内的上下文初始化参数。
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 默认位置在WEB-INF下 默认名字为:springDispacherServlet-servlet -->
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!--监听器
作用:配置文件加载监听器;
ContextLoaderListener:作用是启动web容器,自动装配applicationContext.xml配置文件
-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--前端控制器-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定SpringMVC配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-conf.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--处理请求编码 该过滤器一定要配置在其他过滤器之前-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--开启Spring支持REST风格-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
前端控制器:
前端控制器(DispatcherServlet)类最核心的方法是 doDispatch()
在web.xml中配置了名为 dispatcher 的Servlet,拦截所有的请求。当Web应用接收到这种请求时,会调用前端控制器。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//检测request是否包含多媒体类型(File文件),并将request转化为processedRequest
processedRequest = this.checkMultipart(request);
//判断processedRequest是否为原始的request
multipartRequestParsed = processedRequest != request;
//确定当前请求的处理程序(处理器)。(就是判断当前请求可以被哪个Controller处理)
mappedHandler = this.getHandler(processedRequest);
//如果没有找到哪个处理器(控制器)能处理当前请求就会报404或者抛出异常
if (mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//确定当前请求的处理程序(处理器)的适配器。
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
//如果处理程序支持,则处理最后修改的标头。
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//实际上调用处理程序。(适配器来执行目标方法,将目标方法执行完的返回值作为视图名,保存到ModelAndView中。所以说,无论目标方法怎么写,适配器都会将最终返回值封装成ModelAndView )
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//使用默认视图名
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var19) {
dispatchException = var19;
}
//处理ModelAndView,包含render()方法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception var20) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var20);
} catch (Error var21) {
this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var21);
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
详解:
- 所有请求过来
拦截请求DispatcherServlet
- 调用doDispatch() 方法进行处理
- getHandler():根据当前请求地址找到能处理这个请求的目标处理器类
- 根据当前请求在HandlerMapping中找到当前request的映射信息,获取到handler(目标处理器类)
- getHandlerAdapter():根据当前处理器类获取到能执行这个处理器方法的适配器
- 根据当前处理器类找到当前的HandlerAdapter(适配器)
- 使用刚才获取到的适配器(AnnotationMethodHandlerAdapter)执行目标方法
- 目标方法执行后会返回一个ModelAndView对象
- 根据ModelAndView的信息转发到具体的页面,并可以在请求域中取出ModelAndView中的模型数据
- getHandler():根据当前请求地址找到能处理这个请求的目标处理器类
那么getHandler()是如何根据当前请求就能找到哪个类能来处理当前请求?
首先,该方法回返目标处理器的执行链 HandlerExecutionChain 。然后遍历 handlerMappings(处理器映射),它里面保存了每一个处理器能处理哪些请求的映射信息。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
**getHandlerAdapter()又是怎么找到目标处理器类的适配器的呢?**与上边一样,遍历所有 handlerAdapters ,如果当前遍历的 adapter 支持当前处理器就返回该适配器(适配器本质上就是反射工具,要拿适配器才去执行目标方法)。其实是由HandlerExecutionChain HandlerMapping.getHandler(HttpServletRequest)方法获取到的。从这里可以看出,HandlerMapping的作用主要是根据当前request请求获取能够处理当前request的handler,而HandlerAdapter的作用在于将request中的各个属性适配为handler能够处理的形式。
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 [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
关于HandlerAdapter.supports()方法,有这个方法的主要原因是HandlerMapping是可以有多中实现的,Spring会遍历这些具体的实现类,判断哪一个能够根据当前request产生一个handler。对HandlerAdapter而言,其实不知道当前获取到的handler具体是什么形式的,不同的HandlerMapping产生的handler形式是不一样的,比如,RequestMappingHandlerMapping产生的handler则是封装在HandlerMethod对象中的,因而这里HandlerAdapter需要一个方法能够快速过滤掉当前产生的handler是否为其能够进行适配的,这个方法就是HandlerAdapter.supports()方法。如下是该方法的具体实现:
// AbstractHandlerMethodAdapter
@Override
public final boolean supports(Object handler) {
// 判断当前handler是否为HandlerMethod类型,并且判断supportsInternal()方法返回值是否为true,
// 这里supportsInternal()方法是提供给子类实现的一个方法,对于RequestMappingHandlerAdapter
// 而言,其返回值始终是true,因为其只需要处理的handler是HandlerMethod类型的即可
return (handler instanceof HandlerMethod
&& supportsInternal((HandlerMethod) handler));
}
// RequestMappingHandlerAdapter
@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
// 这里RequestMappingHandlerAdapter只是对supportsInternal()返回true,因为其只需要
// 处理的handler类型是HandlerMethod类型即可
return true;
}
在supports()方法判断了所处理的handler是HandlerMethod类型之后,RequestMappingHandlerAdapter就会调用handle() 方法处理当前请求。该方法主要作用在于有五点:
- 获取当前Spring容器在方法上配置的标注了@ModelAttribute但是没有标注@RequestMapping注解的方法,在真正调用具体的handler之前会将这些方法依次进行调用
- 获取当前Spring容器中标注了@InitBinder注解的方法,调用这些方法以对一些用户自定义的参数进行转换并且绑定
- 根据当前handler的方法参数标注的注解类型,如@RequestParam,@ModelAttribute等,获取对应的ArgumentResolver,以将request中的参数转换为当前方法中对应注解的类型
- 配合转换而来的参数,通过反射调用具体的handler方法
- 通过ReturnValueHandler对返回值进行适配,比如ModelAndView类型的返回值就由ModelAndViewMethodReturnValueHandler处理,最终将所有的处理结果都统一封装为一个ModelAndView类型的返回值,这也是RequestMappingHandlerAdapter.handle()方法的返回值类型。
这里先看看RequestMappingHandlerAdapter.handle()方法的实现源码:
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
// 检查给定的请求中所支持的方法和所需的会话(如果有的话)。
checkRequest(request);
// 判断当前是否需要支持在同一个session中只能线性地处理请求
if (this.synchronizeOnSession) {
// 获取当前请求的session对象
HttpSession session = request.getSession(false);
if (session != null) {
// 为当前session生成一个唯一的可以用于锁定的key
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
// 对HandlerMethod进行参数等的适配处理,并调用目标handler
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 没有HttpSession可用->不需要互斥锁
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 如果当前不需要对session进行同步处理,则直接对HandlerMethod进行适配
mav = invokeHandlerMethod(request, response, handlerMethod);
}
// 判断当前请求头中是否包含Cache-Control请求头,如果不包含,则对当前response进行处理,
// 为其设置过期时间
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
// 如果当前SessionAttribute中存在配置的attributes,则为其设置过期时间。
// 这里SessionAttribute主要是通过@SessionAttribute注解生成的
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
// 如果当前不存在SessionAttributes,则判断当前是否存在Cache-Control设置,
// 如果存在,则按照该设置进行response处理,如果不存在,则设置response中的
// Cache的过期时间为-1,即立即失效
prepareResponse(response);
}
}
return mav;
}
上述代码主要做了两部分处理:
- 判断当前是否对session进行同步处理,如果需要,则对其调用进行加锁,不需要则直接调用
- 判断请求头中是否办函Cache-Control请求头,如果不包含,则设置其Cache立即失效。可以看到,对于HandlerMethod的具体处理是在
方法中进行的,如下是该方法的具体实现:invokeHandlerMethod()
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 获取容器中全局配置的InitBinder和当前HandlerMethod所对应的Controller中配置的InitBinder,用于进行参数的绑定
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 获取容器中全局配置的ModelAttribute和当前HandlerMethod所对应的Controller中配置的ModelAttribute,这些配置的方法将会在目标方法调用之前进行调用
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 将handlerMethod 封装为一个ServletInvocableHandlerMethod对象,该对象用于对当前request的整体调用流程进行了封装
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
// 设置当前容器中配置的所有ArgumentResolver
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
// 设置当前容器中配置的所有ReturnValueHandler
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
//将前面创建的WebDataBinderFactory设置到ServletInvocableHandlerMethod中
invocableMethod.setDataBinderFactory(binderFactory);
// 设置ParameterNameDiscoverer,该对象将按照一定的规则获取当前参数的名称
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
// 这里initModel()方法主要作用是调用前面获取到的@ModelAttribute标注的方法,
// 从而达到@ModelAttribute标注的方法能够在目标Handler调用之前调用的目的
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 获取当前的AsyncWebRequest,这里AsyncWebRequest的主要作用是用于判断目标
// handler的返回值是否为WebAsyncTask或DefferredResult,如果是这两种中的一种,
// 则说明当前请求的处理应该是异步的。所谓的异步,指的是当前请求会将Controller中
// 封装的业务逻辑放到一个线程池中进行调用,待该调用有返回结果之后再返回到response中。
// 这种处理的优点在于用于请求分发的线程能够解放出来,从而处理更多的请求,只有待目标任务
// 完成之后才会回来将该异步任务的结果返回。
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
// 封装异步任务的线程池,request和interceptors到WebAsyncManager中
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// 这里就是用于判断当前请求是否有异步任务结果的,如果存在,则对异步任务结果进行封装
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
// 封装异步任务的处理结果,虽然封装的是一个HandlerMethod,但只是Spring简单的封装
// 的一个Callable对象,该对象中直接将调用结果返回了。这样封装的目的在于能够统一的
// 进行右面的ServletInvocableHandlerMethod.invokeAndHandle()方法的调用
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
// 对请求参数进行处理,调用目标HandlerMethod,并且将返回值封装为一个ModelAndView对象
invocableMethod.invokeAndHandle(webRequest, mavContainer);//真正执行目标方法
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
// 对封装的ModelAndView进行处理,主要是判断当前请求是否进行了重定向,如果进行了重定向,
// 还会判断是否需要将FlashAttributes封装到新的请求中
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
// 调用request destruction callbacks和对SessionAttributes进行处理
webRequest.requestCompleted();
}
}
上述代码是RequestMappingHandlerAdapter处理请求的主要流程,其主要包含四个部分:
- 获取当前容器中使用@InitBinder注解注册的属性转换器
- 获取当前容器中使用@ModelAttribute标注但没有使用@RequestMapping标注的方法,并且在调用目标方法之前调用这些方法
- 判断目标handler返回值是否使用了WebAsyncTask或DefferredResult封装,如果封装了,则按照异步任务的方式进行执行
- 处理请求参数,调用目标方法和处理返回值。这里我们首先看RequestMappingHandlerAdapter是如何处理标注@InitBinder的方法的,如下是getDataBinderFactory()方法的源码:
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod)
throws Exception {
// 判断当前缓存中是否缓存了当前bean所需要装配的InitBinder方法,如果存在,则直接从缓存中取,
// 如果不存在,则在当前bean中进行扫描获取
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 在当前bean中查找所有标注了@InitBinder注解的方法,这里INIT_BINDER_METHODS就是一个
// 选择器,表示只获取使用@InitBinder标注的方法
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
// 这里initBinderAdviceCache是在RequestMappingHandlerAdapter初始化时同步初始化的,
// 其内包含的方法有如下两个特点:①当前方法所在类使用@ControllerAdvice进行标注了;
// ②当前方法使用@InitBinder进行了标注。也就是说其内保存的方法可以理解为是全局类型
// 的参数绑定方法
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
// 这里判断的是当前配置的全局类型的InitBinder是否能够应用于当前bean,
// 判断的方式主要在@ControllerAdvice注解中进行了声明,包括通过包名,类所在的包,
// 接口或者注解的形式限定的范围
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
// 这里是将当前HandlerMethod所在bean中的InitBinder添加到需要执行的initBinderMethods中。
// 这里从添加的顺序可以看出,全局类型的InitBinder会在当前bean中的InitBinder之前执行
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
// 将需要执行的InitBinder封装到InitBinderDataBinderFactory中
return createDataBinderFactory(initBinderMethods);
}
这里获取InitBinder的方式主要有两种,一种是获取全局配置的InitBinder,全局类型的InitBinder需要声明的类上使用@ControllerAdvice进行标注,并且声明方法上使用@InitBinder进行标注;另一种则是获取当前handler所在类中的使用@InitBinder注解标注的方法。这两种InitBinder都会执行,只不过全局类型的InitBinder会先于局部类型的InitBinder执行。关于使用@InitBinder标注的方法执行时间点,需要说明的是,因为其与参数绑定有关,因而其只会在参数绑定时才会执行。
这里我们继续看RequestMappingHandlerAdapter是如何获取@ModelAttribute标注的方法并且执行的,如下是
getModelFactory()
方法的源码:
private ModelFactory getModelFactory(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
// 这里SessionAttributeHandler的作用是声明几个属性,使其能够在多个请求之间共享,
// 并且其能够保证当前request返回的model中始终保有这些属性
SessionAttributesHandler sessionAttrHandler =
getSessionAttributesHandler(handlerMethod);
// 判断缓存中是否保存有当前handler执行之前所需要执行的标注了@ModelAttribute的方法
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.modelAttributeCache.get(handlerType);
if (methods == null) {
// 如果缓存中没有相关属性,那么就在当前bean中查找所有使用@ModelAttribute标注,但是
// 没有使用@RequestMapping标注的方法,并将这些方法缓存起来
methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
}
// 获取全局的使用@ModelAttribute标注,但是没有使用@RequestMapping标注的方法,
// 这里全局类型的方法的声明方式需要注意的是,其所在的bean必须使用@ControllerAdvice进行标注
List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {
// 判断@ControllerAdvice中指定的作用的bean范围与当前bean是否匹配,匹配了才会对其应用
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
});
// 将当前方法中使用@ModelAttribute标注的方法添加到需要执行的attrMethods中。从这里的添加顺序
// 可以看出,全局类型的方法将会先于局部类型的方法执行
for (Method method : methods) {
Object bean = handlerMethod.getBean();
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
// 将需要执行的方法等数据封装为ModelFactory对象
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}
上述
getModelFactory()
方法主要工作还是获取当前需要先于目标handler执行的方法,并且获取的方式与前面的InitBinder非常的相似,这里就不再赘述。关于这里获取的方法,其具体的执行过程实际上是在后面的
ModelFactory.initModel()
方法中进行。这里我们直接阅读该方法的源码:
public void initModel(NativeWebRequest request, ModelAndViewContainer container,
HandlerMethod handlerMethod) throws Exception {
// 在当前request中获取使用@SessionAttribute注解声明的参数
Map<String, ?> sessionAttributes =
this.sessionAttributesHandler.retrieveAttributes(request);
// 将@SessionAttribute声明的参数封装到ModelAndViewContainer中
container.mergeAttributes(sessionAttributes);
// 调用前面获取的使用@ModelAttribute标注的方法
invokeModelAttributeMethods(request, container);
// 这里首先获取目标handler执行所需的参数中与@SessionAttribute同名或同类型的参数,
// 也就是handler想要直接从@SessionAttribute中声明的参数中获取的参数。然后对这些参数
// 进行遍历,首先判断request中是否包含该属性,如果不包含,则从之前的SessionAttribute缓存
// 中获取,如果两个都没有,则直接抛出异常
for (String name : findSessionAttributeArguments(handlerMethod)) {
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '"
+ name + "'", name);
}
container.addAttribute(name, value);
}
}
}
这里
initModel()
方法主要做了两件事:
- 保证@SessionAttribute声明的参数的存在
- 调用使用@ModelAttribute标注的方法。我们直接阅读invokeModelAttributeMethods()方法的源码:
private void invokeModelAttributeMethods(NativeWebRequest request,
ModelAndViewContainer container) throws Exception {
while (!this.modelMethods.isEmpty()) {
// 这里getNextModelMethod()方法始终会获取modelMethods中的第0号为的方法,
// 后续该方法执行完了之后则会将该方法从modelMethods移除掉,因而这里while
// 循环只需要判断modelMethods是否为空即可
InvocableHandlerMethod modelMethod =
getNextModelMethod(container).getHandlerMethod();
// 获取当前方法中标注的ModelAttribute属性,然后判断当前request中是否有与该属性中name字段
// 标注的值相同的属性,如果存在,并且当前ModelAttribute设置了不对该属性进行绑定,那么
// 就直接略过当前方法的执行
ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
Assert.state(ann != null, "No ModelAttribute annotation");
if (container.containsAttribute(ann.name())) {
if (!ann.binding()) {
container.setBindingDisabled(ann.name());
}
continue;
}
// 通过ArgumentResolver对方法参数进行处理,并且调用目标方法
Object returnValue = modelMethod.invokeForRequest(request, container);
// 如果当前方法的返回值不为空,则判断当前@ModelAttribute是否设置了需要绑定返回值,
// 如果设置了,则将返回值绑定到请求中,后续handler可以直接使用该参数
if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue,
modelMethod.getReturnType());
if (!ann.binding()) {
container.setBindingDisabled(returnValueName);
}
// 如果request中不包含该参数,则将该返回值添加到ModelAndViewContainer中,
// 供handler使用
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
}
这里调用使用@ModelAttribute标注的方法的方式比较简单,主要需要注意的是,对于调用结果,如果当前request中没有同名的参数,则会将调用结果添加到ModelAndViewContainer中,以供给后续handler使用。
在调用完使用上述方法之后,Spring会判断当前handler的返回值是否为WebAsyncTask或DefferedResult类型,如果是这两种类型的一种,那么就会讲这些任务放入一个线程池中进行异步调用,而当前线程则可以继续进行请求的分发。这里这种设计的目的是,默认情况下Spring处理请求都是同步的,也就是说进行请求分发的线程是会调用用户所声明的handler方法的,那么如果用户声明的handler执行时间较长,就可能导致Spring用于请求处理的线程都耗在了处理这些业务代码上,也就导致后续的请求必须等待,这在高并发的场景中是不能被允许的,因而这里Spring提供了一种异步任务处理的方式,也就是进行请求分发的线程只需要将用户的业务任务放到线程池中执行即可,其自身可以继续进行其他的请求的分发。如果线程池中的任务处理完成,其会通知Spring将处理结果返回给调用方。关于异步任务的处理流程,我们后面会使用专门的章节进行讲解,这里只是简单的讲解其主要功能。
在进行了相关前置方法调用和异步任务的判断之后,RequestMappingHandlerAdapter就会开始调用目标handler了。调用过程在
ServletInvocableHandlerMethod.invokeAndHandle()
方法中,如下是该方法的源码:
public void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 对目标handler的参数进行处理,并且调用目标handler
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 设置相关的返回状态
setResponseStatus(webRequest);
// 如果请求处理完成,则设置requestHandled属性
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null
|| mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
// 如果请求失败,但是有错误原因,那么也会设置requestHandled属性
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 遍历当前容器中所有ReturnValueHandler,判断哪种handler支持当前返回值的处理,
// 如果支持,则使用该handler处理该返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value",
returnValue), ex);
}
throw ex;
}
}
对于handler的调用过程,这里主要分为三个步骤:
- 处理请求参数进行处理,将request中的参数封装为当前handler的参数的形式。
- 通过反射调用当前handler
- 对方法的返回值进行处理,以将其封装为一个ModelAndView对象。这里第一步和第二步封装在了
方法中,我们首先看该方法的源码:invokeForRequest()
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 将request中的参数转换为当前handler的参数形式
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(),
getBeanType()) + "' with arguments " + Arrays.toString(args));
}
// 这里doInvoke()方法主要是结合处理后的参数,使用反射对目标方法进行调用
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(),
getBeanType()) + "] returned [" + returnValue + "]");
}
return returnValue;
}
// 本方法主要是通过当前容器中配置的ArgumentResolver对request中的参数进行转化,
// 将其处理为目标handler的参数的形式
private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 获取当前handler所声明的所有参数,主要包括参数名,参数类型,参数位置,所标注的注解等等属性
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// providedArgs是调用方提供的参数,这里主要是判断这些参数中是否有当前类型
// 或其子类型的参数,如果有,则直接使用调用方提供的参数,对于请求处理而言,默认情况下,
// 调用方提供的参数都是长度为0的数组
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 如果在调用方提供的参数中不能找到当前类型的参数值,则遍历Spring容器中所有的
// ArgumentResolver,判断哪种类型的Resolver支持对当前参数的解析,这里的判断
// 方式比较简单,比如RequestParamMethodArgumentResolver就是判断当前参数
// 是否使用@RequestParam注解进行了标注
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
// 如果能够找到对当前参数进行处理的ArgumentResolver,则调用其
// resolveArgument()方法从request中获取对应的参数值,并且进行转换
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
} catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug(getArgumentResolutionErrorMessage("Failed to resolve",
i), ex);
}
throw ex;
}
}
// 如果进行了参数处理之后当前参数还是为空,则抛出异常
if (args[i] == null) {
throw new IllegalStateException("Could not resolve method parameter at index "
+ parameter.getParameterIndex() + " in "
+ parameter.getExecutable().toGenericString()
+ ": " + getArgumentResolutionErrorMessage("No suitable resolver for",i));
}
}
return args;
}
关于handler的调用,可以看到,这里的实现也是比较简单的,首先是遍历所有的参数,并且查找哪种ArgumentResolver能够处理当前参数,找到了则按照具体的Resolver定义的方式进行处理即可。在所有的参数处理完成之后,RequestMappingHandlerAdapter就会使用反射调用目标handler。
对于返回值的处理,其形式与对参数的处理非常相似,都是对ReturnValueHandler进行遍历,判断哪种Handler能够支持当前返回值的处理,如果找到了,则按照其规则进行处理即可。如下是该过程的主要流程代码:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 获取能够处理当前返回值的Handler,比如如果返回值是ModelAndView类型,那么这里的handler就是
// ModelAndViewMethodReturnValueHandler
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: "
+ returnType.getParameterType().getName());
}
// 通过获取到的handler处理返回值,并将其封装到ModelAndViewContainer中
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
// 本方法的主要作用是获取能够处理当前返回值的ReturnValueHandler
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value,
MethodParameter returnType) {
// 判断返回值是否为异步类型的返回值,即WebAsyncTask或DefferredResult
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
// 对所有的ReturnValueHandler进行遍历,判断其是否支持当前返回值的处理。这里如果当前返回值
// 是异步类型的返回值,还会判断当前ReturnValueHandler是否为
// AsyncHandlerMethodReturnValueHandler类型,如果不是,则会继续查找
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
// 判断是否支持返回值处理的主要位置,比如ModelAndViewMethodReturnValueHandler就会
// 判断返回值是否为ModelAndView类型,如果是,则表示其是当前ReturnValuleHandler所支持的类型
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
总的来讲,RequestMappingHandlerAdapter的主要作用就是调用RequestMappingHandlerMapping所获取到的handler,然后将返回值封装为一个ModelAndView对象,该对象中保存了所要渲染的视图名称和渲染视图时所需要的参数值,而具体的渲染过程则是通过View对象进行的。
SpringMVC九大组件:
/** 文件上传解析器 */
@Nullable
private MultipartResolver multipartResolver;
/** 区域信息解析器(国际化视图) */
@Nullable
private LocaleResolver localeResolver;
/** 主题解析器 */
@Nullable
private ThemeResolver themeResolver;
/** Handler映射信息 */
@Nullable
private List<HandlerMapping> handlerMappings;
/** List of Handler适配器 */
@Nullable
private List<HandlerAdapter> handlerAdapters;
/** 异常解析器 */
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;
/** RequestToViewNameTranslator used by this servlet. */
@Nullable
private RequestToViewNameTranslator viewNameTranslator;
/** FlashMap管理器:SpringMVC中运行重定向携带数据的功能 */
@Nullable
private FlashMapManager flashMapManager;
/** 视图解析器 */
@Nullable
private List<ViewResolver> viewResolvers;
共同点:九大组件全都是接口;接口就是规范,提供了强大的扩展性。
浅谈 SpringMVC 九大组件动作原理:
在DispatcherServlet中有一个
initStrategies()
方法,该方法是一个初始化策略方法,而这个方法中就是九大组件的初始化方法。如下:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
这里以
initHandlerMappings(context)
方法为例进行说明SpringMVC是如何对九大组件进行初始化的。进入到 initHandlerMappings(context)方法,代码如下:
/**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
首先,先看这个方法上的一段注释,意思是:初始化HandlerMapping 所使用的的这个类,如果在BeanFactory中没有为这个名称空间定义HandlerMapping bean将使用默认的BeanNameUrlHandlerMapping。
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
detectAllHandlerMappings
是DispatcherServlet的一个属性,默认值为true,在这里做了判断条件。
这段代码是检测容器中所有的HandlerMapping,如果找到进行赋值并排序。但如果没有找到,就去容器中找id为HANDLER_MAPPING_BEAN_NAME 的 HandlerMapping。
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
如果还没有找到,就会注册一个默认的HandlerMapping。
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
那么这个默认的HandlerMapping是如何注册的呢?有上边代码知道,通过
getDefaultStrategies()
方法来注册的。
getDefaultStrategies()
方法实现:
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Unresolvable class definition for DispatcherServlet's default strategy class [" +
className + "] for interface [" + key + "]", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
defaultStrategies:实现
static {
// 从属性文件加载默认策略实现。这目前是严格的内部设置,不允许应用程序开发人员定制。
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}
}
为给定的策略接口创建一个默认策略对象的列表,通过
defaultStrategies.getProperty(key)
实现使用DispatcherServlet.properties属性文件(在相同包下的DispatcherServlet类)来确定类名。它通过上下文的BeanFactory实例化策略对象。
总结后就一句话:组件的初始时去容器中找这个组件,如果没有找到就是用默认的。
对静态资源的处理:
1 :容器启动时是否加载servlet,值大于0表示容器在应用启动时就加载这个servlet,小于0或不指定,则在该servlet的第一个请求时才会去加载。正数的值越小,应用启动是越先被加载,值相同则由容器选择加载顺序
*.form:也可以使用这种后缀匹配,只拦截后缀为 .form 的请求,不回拦截静态资源的请求。
/:拦截 *.html,不拦截 *.jsp 请求。因为 *.jsp 请求会被Tomcat中默认的jsp Servlet处理,不会被dispatcherServlet拦截。
/*:拦截所有请求,包括 *.jsp 和 *.html。
“/” 和 “/" 的区别在于 "/” 的优先级高于 ”/“ 和 “*.后缀” 的路径,而“/”在所有的匹配路径中,优先级最低。即当别的路径都无法匹配时,“/”所匹配的Servlet才会进行相应的请求资源处理。
处理 *.jsp 是 Tomcat 做的事;所有项目的 web.xml 皆是继承 Tomcat中的 web.xml
Tomcat中 web.xml :
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
Tomcat中的web.xml中有一个DefaultServlet是url-pattern = /
DefaultServlet 是 Tomcat 中处理静态资源的,除了jsp和servlet外剩下的都是静态资源;如html、图片等。tomcat会在服务器下找到这个资源并返回。
前端控制器的 / 相当于重写了tomcat服务器中的DefaultServlet。
静态资源的处理:
方法一:激活Tomcat的default Servlet来处理静态资源
<!--Tomcat, Jetty, JBoss, and GlassFish默认Servlet的名字为“default”-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.html</url-pattern>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
default Servlet无法解析jsp页面,直接输出html源码。
方式二:使用
<mvc:default-servlet-handler/>
请求的url若是静态资源请求,则转由org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler 处理并返回,否则才由DispatcherServlet处理。DefaultServletHttpRequestHandler使用的是各个Servlet容器自己默认的Servlet(如jsp servlet)。
方式三:Spring3.0.4以后版本
<mvc:resources/>
<mvc:annotation-driven />
<!--两个*,它表示映射/resources/下所有的URL,包括子路径-->
<mvc:resources mapping="/resources/**" location="/WEB-INF/, classpath:config, /resources/" cache-period="31536000"/>
<mvc:default-servlet-handler/>
将静态资源的处理经由 Spring MVC 框架交回 Web 应用服务器处理。而
<mvc:resources/>
更进一步,由 Spring MVC 框架自行处理静态资源。
mvc:resources/>
允许静态资源放在任何地方,如 WEB-INF 目录下、类路径下,甚至 JAR 包中。可通过 cache-period 设置客户端数据缓存时间。
使用
mvc:resources/>
元素,把 mapping 的 URL 注册到 SimpleURLHandlerMapping的urlMap中, key 为 mapping 的 URl pattern值,而 value为ResourceHttpRequestHandler 处理并返回,所以就支持 classpath 目录 和jar包内静态资源的访问。
默认配置文件
默认SpringMCVC配置文件位置:WEB-INF\servletName-servlet.xml
使用 @RequestMapping 映射请求:
@RequestMapping
作用是为控制器指定可以处理那些 URL 请求,该注解可以放在类上或者方法上。
- 标注在类上:提供初步的请求请求映射信息。相对于 WEB 应用的根目录
- 标注在方法上:提供进一步的细分映射信息。相对于类定义出的 URL。
若类上未标注 @RequestMapping ,则方法出标记的 URL 相对于 WEB 应用的根目录
DispatcherServlet 截获请求后,就通过控制器上@RequestMapping 提供的映射信息确定请求所对应的处理方法。
@RequestMapping 的 method 属性用于指定处理哪些HTTP方法。
@RequestMapping(value = "/hello" , method = {RequestMethod.GET,RequestMethod.POST} )
//共有这几个 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE;常用的:GET,POST,PUT,DELETE
@RequestMapping的param属性可以指定某一种参数名类型,当请求数据中包含该名称的请求参数时,才能进行响应,否则拒绝此次请求。
// 发送请求时必须带上一个名为 username 的参数,否则 404
@RequestMapping(value = "/hello" , params = {"username"})
@RequestMapping的headers属性可以指定某一种请求头类型。
consumes属性表示处理请求的提交内容类型(Content-Type),例如"application/json,text/html"。
而produces表示返回的内容类型,仅当request请求头中的Accept包含该指定类型时才会返回。
@RequestMapping(value = "/hello" , consumes = "application/json")
@RequestMapping(value = "/hello" , produces = "application/json")
参数绑定
当用户发送请求时,前端控制器会请求处理映射器返回一个处理器链,然后请求处理器适配器执行响应的Handler。此时,HandlerAdapter会调用SpringMVC提供的参数绑定组件将请求的key/value数据绑定到Controller处理器方法对应的形参上。
简单类型参数绑定
通过RequestParam将某个请求参数绑定到方法的形参上。value属性不指定时,则请求参数名称要与形参名称相同。required参数表示是否必须传入。defaultValue参数可以指定参数的默认值。
路径变量
@RequestMapping(value = "/hello/{id}")
public String test01(@PathVariable("id")String id ){
System.out.println("程序启动..."+id);
return "success";
}
}
包装类型参数绑定
<form action="/springmvcdemo/user/findUserByCondition.action" method="post">
id: <input type="int" name="id"/>
name: <input type="text" name="name"/>
<input type="submit"/>
</form>
name属性名称与User类属性对应,Spring MVC的HandlerAdapter会解析请求参数生成具体的实体类,将相关的属性值通过set方法绑定到实体类中。
@RequestMapping("/findUserByCondition")
@ResponseBody
public User findUserByCondition(User user) {
return user;
}
集合类型参数绑定
@RequestMapping("/findUsers")
@ResponseBody
public List<User> findUsers(UserList userList) {
List<User> users = userList.getUsers();
for(User user : users) {
LOGGER.info("user_id: " + user.getId() + " " + "user_name: " + user.getName());
}
return users;
}
<form action="/springmvcdemo/user/findUsers.action" method="post">
<input name="users[0].id" value="1"/>
<input name="users[0].name" value="tyson"/>
<input name="users[1].id" value="2"/>
<input name="users[1].name" value="sophia"/>
<input type="submit"/>
</form>
包装类中定义的List属性的名称要与前端页面的集合名一致。
public class UserList {
private List<User> users;
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
REST风格的URL地址约束
REST是一种开发风格,有四种请求:GET、POST、PUT、DELETE。分别对应着四种请求操作:GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。REST风格提倡URL地址使用统一的风格设计。
通常的浏览器只支持发送POST和GET请求,不支持DELETE和PUT,要想支持,则需要使用HiddenHttpMethodFilter过滤器来将post请求转换为put跟delete请求。
<!--开启Spring支持REST风格-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<form action="/hello" method="post">
<!-- Form 表单中加入此句-->
<input type="hidden" name="_method" value="DELETE">
</form>
源码分析:
HiddenHttpMethodFilter首先继承了OncePerRequestFilter,那么就先看他的重写方法
doFilterInternal
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//为替换原有的request做准备
HttpServletRequest requestToUse = request;
//如果请求方式为 post 且没有异常
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
//获取表单上_method的值(put、delete)
String paramValue = request.getParameter(this.methodParam);
//如果_method的值不为空
if (StringUtils.hasLength(paramValue)) {
//将获取的这个值转为大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
//判断这个值是否包含(PUT、DELETE、PATCH等)这些常量之一
if (ALLOWED_METHODS.contains(method)) {
//开时替换request请求。 1.调用自己的内部类(HttpMethodRequestWrapper)的构造器,将request和method传入;2.内部类(HttpMethodRequestWrapper)重写了getMehod方法,返回的是你设置传入的method (public String getMethod() { return this.method;}),此时requestToUse已经是自己设置的method了
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
//放行 将新的requestToUse代替原有的那个request执行后面的代码。
filterChain.doFilter((ServletRequest)requestToUse, response);
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
public String getMethod() {
return this.method;
}
}
小坑:
请求处理
获取请求参数的值:
@RequestParam
获取请求参数:
@RequestMapping("/demo02")
public String demo02(@RequestParam("user") String param){
System.out.println("默认获取请求参数"+param);
return "success";
}
@RequestParam可以指定请求参数的名。@RequestParam(“user”)相当于 request.getParameter(“user”);
而@PathVariable 是路径变量 ,如 demo01/{user} 。
默认获取请求参数:
直接在方法上入参上写一个和请求参数名相同的变量,这个变量就自动接收请求参数的值。如果这个参数在发送请求时没有传或者发送请求时的参数不是param,那么默认值为null。
@RequestMapping("/demo02")
public String demo02(Srting param){
System.out.println("默认获取请求参数"+param);
return "success";
}
获取请求头中的信息:
浏览器中的头信息:
获取请求中带的某个@Cookie:
以前获取某个Cookie
Cookie[] cookies = request.getCookie();
for(Cookie cookie : cookies){
if(cookie.getName.equals("JSESSIONID")){
cookie.getValue();
}
}
使用@CookieValue:
这两种都能获取Cookie
传入POJO,SpringMVC自动封装
如果请求参数太多了,可以封装成一个POJO,SpringMVC会自动为这个POJO赋值。
- 将POJO中的每一个属性,从request请求中获取出来并封装。
- 还支持级联属性(属性的属性)。
- 请求参数的参数名和对象中的属性一一对应。
CharacterEncodingFilter解决乱码问题:
提交的数据可能有乱码
请求乱码:
- GET请求:在Tomcat文件中改server.xml的8080处加入URIEncoding=“utf-8”。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"
URIEncoding="utf-8" />
- POST请求:在第一次获取参数之前设置CharacterEncodingFilter过滤器(该过滤器一定要设置在其他过滤去之前)
web.xml
<!--字符编码过滤器 该过滤器一定要配置在其他过滤器之前-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--解决POST请求乱码-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--解决响应乱码-->
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
为什么要放在最前面呢?处理请求乱码其实就是设置request.setCharacterEncoding(“UTF-8”);这句话要在第一次获取请求参数之前调用。而CharacterEncodingFilter过滤器的本质就是这句话。要是其他过滤器(这里举例HiddenHttpMethodFilter)要是放在CharacterEncodingFilter,如下图就已经获取参数了。所以会导致处理编码的过滤器无效。
数据输出
SpringMVC除过在方法上传入原生的request和session外还能怎样把数据带给页面?
方法参数:
可以在方法处传入Map、或者Model或者ModelMap。在这些参数中保存的数据都会被放在域中。可以在页面获取。
@RequestMapping("/demo04")
public String demo04(Map<String,Object> map){
map.put("msg", "Map");
return "success";
}
@RequestMapping("/demo05")
public String demo05(Model model){
model.addAttribute("msg", "Model");
return "success";
}
@RequestMapping("/demo06")
public String demo06(ModelMap modelMap){
modelMap.addAttribute("msg","ModelMap" );
return "success";
}
这三种参数的数据都是放在request域中。
Map、Model、ModelMap的关系:
在控制台输出三种参数的类型
可以看到,他们都是 BindingAwareModelMap。也就是在 BindingAwareModelMap 中保存的数据会被放在请求域中。
方法返回值:ModelAndView
方法的返回值可以变为ModelAndView类型。ModelAndView既包含视图信息(页面地址)也包含模型数据(给页面带的数据),而且数据是放在请求域中。
@RequestMapping("/demo07")
public ModelAndView demeo07(){
ModelAndView view = new ModelAndView("success");
view.addObject("msg", "ModelAndView");
return view ;
}
SessionAttributes给session域暂存数据:
@SessionAttributes
只能标注在类上。给BindingAwareModelMap中保存的数据,或者ModelAndView中的数据同时给Session中存方一份。该注解的三个属性如图。
value指定保存数据时要给session中放的数据的key。types指定改数据的类型。
总之该注解不推荐使用,原因是,如果使用该注解,就必须确保Session中存有数据,否则会抛出异常。如果要在session中存放数据 推荐使用原生API。
视图解析
视图和视图解析:
- 请求处理方法执行完成后,最终返回一个 ModelAndView对象。对于那些返回 String ,View或ModelMap等类型的处理方法,SpringMVC 也会在内部将他们装配成一个ModelAndView对象,它包含了逻辑名和模型对象的视图。
- SpringMVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP,也可能是Excel、JFreeChart等各种表现形式的视图
- 对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重电聚焦在生产模型数据的工作上,从而实现MVC的充分解耦
视图:
- 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。
- 为了实现视图模型和具体实现技术的解耦,Spring在 servlet 包中定义了一个高度抽象的 View接口:
- 视图对象有视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题
常用的视图实现类:
视图解析器:
- SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB上下文中配置一种或多种解析策略,并指定他么之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
- 视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。
- 所有的视图解析器都必须实现ViewResolver接口
常用的视图解析器实现类:
- 程序员可以选择一种视图解析器或混用多种视图解析器
- 每个视图解析器都实现了Ordered接口并开放出一个order属性,可以通过 order属性指定解析器的优先顺序,order越小优先级越高。
- SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出ServletException异常。
forward前缀指定一个转发操作:
以前转发页面是通过视图解析器进行拼串。
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程序启动...");
return "success";
}
<!--配置视图解析器-->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="suffix" value=".jsp"></property>
</bean>
通过 给 success 拼接后缀 .jsp。
除了这种方法外也有以下方式进行页面转发
- 使用相对路径:
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程序启动...");
return "/success";
}
- 使用 forward 前缀转发到当前项目下的一个页面。这种方法但不会使用视图解析器拼接。
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程序启动...");
return "forward:/success.jsp";
}
并且这种方法还可以实现请求之间的调用,如下:
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程序启动...");
return "forward:/demo06";
}
@RequestMapping("/demo06")
public String demo06(ModelMap modelMap){
modelMap.addAttribute("msg","ModelMap" );
System.out.println(modelMap.getClass());
return "success";
}
redirect前缀指定重定向到页面:
redirect:重定向的路径
/success.jsp :代表就是当前项目下开始;SpringMVC会为路径自动的拼接上项目名。
原生的Servlet重定向 / 路径需要加上项目名才能成功:response.sendRedirect(“项目名/success.jsp”)
所以说,有前缀的转发和重定向操作,配置的视图解析器就不会进行拼串。
视图解析流程:
上面前端控制器中说到过,所有方法的返回值都会被包装成 ModelAndView 对象
该方法是进行
视图渲染
(将域中的数据在页面展示),下面来到
processDispatchResult
方法看视图渲染流程:
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);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 处理程序是否返回要渲染的视图?
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.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
首先,该方法是处理 处理程序选择和处理程序调用的结果,它要么是ModelAndView,要么是要解析为ModelAndView的异常。如果ModelAndView不为空且没有并清理掉,就调用
render
方法,接下来详细看一下
render
方法:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 确定请求的语言环境,并将其应用于响应。
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != 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 {
// 不需要查找:ModelAndView对象包含实际的View对象。
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() + "'");
}
}
// 委托给View对象进行渲染。
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
该方法是渲染给定的ModelAndView。这是处理请求的最后一个阶段。它可能涉及按名称解析视图。这个方法有一个对象
View view;
那么该对象与ViewResolver又有什么关系呢?
public interface ViewResolver {
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
可以看到,ViewResolver是一个接口,只定义了一个空方法
resolveViewName
,该方法根据视图名(方法的返回值)得到View对象。那么,
resolveViewName
是如何根据视图名得到View对象的呢?我们详细来看
resolveViewName
方法:
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;
}
该方法就是将给定的视图名称解析为一个视图对象。它默认实现询问这个调度程序的所有ViewResolvers(九大组件之一)。可以覆盖自定义解析策略,可能基于特定的模型属性或请求参数。
这个 viewResolvers 可以是我们自己定义的,如果自己没有定义,那么默认也是使用的
InternalResourceViewResolver
可以在DispatcherServlet.properties中查看九大组件在没有自定义的情况下使用的默认值。
通过上面的代码可以看到视图的渲染是通过
resolveViewName
方法,细看该方法:
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 第一次没有缓存
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
// 如果没有view对象,
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// 要求子类创建View对象。
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
真正创建view对象:
protected View createView(String viewName, Locale locale) throws Exception {
// 如果这个解析器不应该处理给定的视图,则返回null以传递给链中的下一个解析器。
if (!canHandle(viewName, locale)) {
return null;
}
// 检查特殊的“redirect:”前缀。
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
// 检查特殊的“forward:”前缀。
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
// 没有前缀或后缀则调用父类实现:调用loadView。
return super.createView(viewName, locale);
}
执行完createView后,就会创建出一个
InternalResourceView
类型的View对象,并返回出去。如果没有创建就换下一个视图解析器。
然后在调用
view.render()
方法。细看 render 方法:
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("View " + formatViewName() +
", model " + (model != null ? model : Collections.emptyMap()) +
(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
根据指定的模型准备视图,并将其与静态属性和RequestContext属性合并(如果需要的话)。委托renderMergedOutputModel进行实际渲染,也就是
renderMergedOutputModel()
方法要做的事情,而InternalResourceView重写了该方法,细看该方法:
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将模型对象公开为请求属性。
exposeModelAsRequestAttributes(model, request);
// 将帮助程序作为请求属性公开(如果有的话)。
exposeHelpers(request);
// 确定请求分配器的路径。
String dispatcherPath = prepareForRendering(request, response);
// 获取目标资源(通常是JSP)的RequestDispatcher(请求转发器)
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// 如果已经包含或响应已经提交,则执行include,否则转发。
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
其中
exposeModelAsRequestAttributes(model, request);
是将给定映射中的模型对象公开为请求属性。名称将取自模型Map。这个方法适用于所有可以通过{@link javax.servlet.RequestDispatcher}访问的资源。细看该方法:
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
该方法将模型中所有数据取出来放在request域中。
看了这么多,总结成一句话:
视图解析器只是为了得到视图对象;视图对象才能真正的转发(将模型数据全部放在请求域中)或者重定向到页面视图对象才能真正的渲染视图
JSTL支持国际化:
当我们导入这两个包时,
可以发现,view对象自动变成了JstkView,这时候可以快速方便的支持国际化功能;
而如果没有导入这两个包,也可以使用配置指定使用的view视图。在视图解析器中有一个名为
viewClass
的属性,指定他的Value值为自己想设置的View对象即可。
View对象依旧是JstlView。
国际化中的坑:
问题:发送一个请求直接来到一个页面,除了做国际化的请求外,其它请求都不能访问。
分析:
- 一定要过SpringMVC的视图解析流程,才会创建一个JSTLView来支持国际化功能;不能直接把请求交给Tomcat。
- 不能使用forwar、redirect前缀(原因上边创建view对象处提到)
解决:SpringMVC配置文件中加入
<mvc:view-controller path="指定请求路径" view-name="映射给那个视图"></mvc:view-controller>
,但这样做了之后,其他请求将不能访问,这时需要加上开启MVC注解驱动模式:
<mvc:annotation-driven></mvc:annotation-driven>
,他会自动注册 DefaultAnnotationHandlerMapping 与 AnnotationMethodHandlerAdapter 两个 bean, 是 spring MVC 为 @Controllers 分发请求所必须的
自定义视图和自定义视图解析器:
步骤:
- 编写自定义的视图解析器,和视图实现类
/**
* 自定义视图解析器
* 视图解析器必须放在 IOC 容器中
*/
@Component
public class MyViewResolver implements ViewResolver {
/**
*
* @param viewName 根据视图名返回视图对象
* @param locale 国际化
* @return
* @throws Exception
*/
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 如果视图名以customize开头,如果不能处理则返回null
if(viewName.startsWith("customize:")){
return new MyView();
}else{
return null;
}
}
}
/**
* 自定义视图
*/
public class MyView implements View {
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws Exception {
// 从model中取出数据
System.out.println("model中的数据:"+model.get("msg"));
response.setContentType("text/html");
response.getWriter().write("<h1>自定义转发视图页面</h1>");
}
}
注:视图解析器必须放在IOC容器中
然后在SpringMVC的配置文件中,配置如下:
调试项目:
可以看到viewResolvers中有两个值,在Spring4中viewResolvers中的值还需要排序,而在Spring5中则不需要排序。
数据绑定
前边提到过SpringMVC可以自动将POJO进行封装,那么是如何进行封装的呢?原理又是什么呢?下边来探索一下。
SpringMVC封装自定义类型的对象的时候,JavaBean要和页面提交的数据进行一一绑定,而在进行绑定期间需要解决很多问题,如:页面提交的所有数据都是字符串,而Javabean中的类型有Internet、Date、Double等数据类型,要进行绑定,需要将String类型转换成Javabean中的数据类型;再者,数据绑定期间数据的格式化问题,如birth字段在前端是String类型,而在Javabean中是一个Date,且格式有xxxx-xx-xx、xxxx/xx/xx、xxxx.xx.xx等格式;另外还有如何进行数据校验来确保用户所提交的数据的合法性。而这个过程是如何实现的呢?
来看ModelAttributeMethodProcessor类中一段相关的源码:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// 创建属性实例
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// 无BindingResult参数-> fail with BindException
throw ex;
}
// 否则,公开null/空值和相关的BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean属性绑定和验证;
// 如果在构造时绑定失败,则跳过。
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// 值类型适应,也包括java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// 在模型末尾添加解析属性和BindingResult
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
看这段源码的注释意思是:
从模型中解析参数,如果未找到参数,则使用默认值实例化它。然后,模型属性通过数据绑定填充请求值,如果{@code @java.validation则可选验证。Valid}存在于实参中。
再来具体分析:
在这段源码有一个
WebDataBinderFactory
:他是一个接口,其中有一个创建binder(绑定器)的方法,如图:
他根据给定的对象创建一个`WebDataBinder`类型的Web数据绑定器,用于从web请求参数到JavaBean对象的数据绑定。专为web环境设计,但不依赖于Servlet API; 其中参数`webRequest`:当前请求;`target`:为其创建数据绑定器的对象,如果为简单类型创建则传入null;`objectName`:目标对象的名称
数据的绑定则有它来完成。在这个绑定器中有许多组件,如下图。处理绑定期间的一系列问题就是有这些组件来完成的。
比如,数据类型转换、数据格式化的工作就是有ConversionService组件来完成的,数据校验则是由Validator来完成的。而这个完成过程又是怎样的呢?
SpringMVC通过反射机制对目标方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,运行机制如下:
详解 <mvc:annotation-driven/>
注解
<mvc:annotation-driven/>
首先,SpringMVC配置文件中加入
<mvc:annotation-driver>
注解后,SpringMVC会自动注册
RequestMappingHandlerMapping
、
RequestMappingHandlerAdapter
、
ExceptionHandlerExceptionResolver
三个组件。还将提供以下支持:
- 支持使用 ConversionService 实例对表单参数进行类型转换
- 支持使用 @NumberFormat 、@DateTimeFormat注解完成数据类型的格式转换
- 支持使用 @Valid 注解对 JavaBan 实例进行 JSR303验证
- 支持使用 @RequestBody 和 @ResponseBody 注解
可以在
BeanDefinitionParser
接口中看到很多定义解析器,其中AnnotationDrivenBeanDefinitionParser就是解析
<mvc:annotation-driven/>
这个标签的。
在开发中,如果请求报错就简单粗暴加上该注解驱动标签
<mvc:default-servlet-handler/>
。而且必要时要配合另一个注解标签
<mvc:default-servlet-handler/>
一起使用。具体分析:
①:如果这两个标签都没有加:@RequestMapping映射资源可以访问,但静态资源(.html、.js、.img等)不可访问
这种情况下,动态资源能访问是因为 RequestMappingHandlerMapping 中的handlerMap中保存了每一个资源的映射信息。而没有保存静态资源的映射信息,所以静态资源不能访问。
②:如果只加上
<mvc:default-servlet-handler/>
,静态资源可以访问,但动态资源不可以访问
这种之所以静态资源能访问,是因为 RequestMappingHandlerMapping 没有了,所以动态资源不能访问,却出现了 SimpleUrlHandlerMapping 替换了,起作用是将所有请求直接交给Tomcat。
③:如果只加上
<mvc:annotation-driven/>
,@RequestMapping映射资源可以访问,但静态资源不可访问
这种情况与两者都不加的情况一样,不多介绍。
④:只有两者都加上的情况下,既可以访问动态资源又可以访问静态资源。
这种情况下可以看到既有RequestMappingHandlerMapping也有SimpleURlHandlerMapping,且前者中保存的是动态请求的信息,后者中保存的是静态请求的信息。
日期格式化
数据格式化
FormattingConversionServiceFactroyBean内部已经注册了:
- NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用 @NumberFormat注解
- JodaDateTimeFormatAnnotationFormatterFactory:支持对日期类型的属性使用 @DateTimeForma 注解
日期格式化
@DateTimeFormat注解可对java.util.Date、java.util.Calendar、java.lang.Long时间类型进行标注:
- pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:“yyyy-MM-dd hh:mm:ss”
- iso 属性:类型为DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用)默认的、ISO.DATE(yyyy-MM-dd)、ISO.TIME(hh:mm:ss.SSSZ)、ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
- style属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式
数值格式化
@NumberFormat 可对类似数字类型的属性进行标注,他拥有两个互斥的属性:
- style:类型为NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数据类型)、Style.CURRENCY(货币类型)、Style.PERCENT(百分数类型)
- pattern:类型为String,自定义样式,如patter=“#,###”
数据校验
在开发中只做前端数据安全性校验是不够的,因为有很多办法能够避开前端校验,比如:浏览器禁用js后,前端校验就不能使用了。所以重要的数据一定要加上后端验证。
SpringMVC 可以用JSR303来做数据校验,JSR303是JAVAEE中的一项子规范,叫做Bean Validation ,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置constraint 的实现。
如何快速的进行后端校验呢?
- 导入Jar包
- hibernate-validator-annotation-processor-5.0.0.CR2.jar
- hibernate-validator-5.0.0.CR2.jar
- validation-api-1.1.0.CR1.jar
- jboss-logging-3.1.1.GA.jar
- classmate-0.8.0.jar
- 给 JavaBean 加上校验注解
- 在 SpringMVC 封装对象的时候,告诉SpringMVC这个 JavaBean需要校验,只需在这个对象参数前加上注解
@Valid
-
如何知道校验结果呢?
只需在这个对象参数后紧跟
参数。该参数封装了校验结果。BindingResult
SpringMVC与Ajax
后端返回JSON数据
有时需要后端返回Json数据时,SpringMVC要如何实现呢?
导包:(使用的5.+的SpringMVC要使用2.9+的版本)
- jackson-annotations-2.9.8.jar
- jackson-databind-2.9.8.jar
- jackson-core-2.9.8.jar
在请求的方法上加上注解
@ResponseBody
:将返回数据放到响应体重,即可将返回的数据转换成 Json 格式。
@ResponseBody
@RequestMapping("/demo03")
public Book demo03(@Valid Book book, BindingResult bindingResult){
boolean hasErrors = bindingResult.hasErrors();
if(hasErrors){
// 如果校验结果有错,就不进行业务处理
}else{
// 进行业务处理
System.out.println("SpringMVC自动封装对象"+book);
}
return book;
}
可以在浏览器看到,数据已经被转换成JSON格式了。
在jackson-annotations这个jar包下,有一些注解可以规定返回格式,比如:JsonIgnore ,标注该注解的属性,不会以json的格式返回。
就是因为加了该注解,上边的JSON格式的数据中才没有bookName这个属性的值。
前端页面使用Ajax获取JSON数据:
<script type="text/javascript" src="js/jquery-3.1.1.js"></script>
<script>
$("#btn_Ajax").click(function () {
$.ajax({
url: "/demo03",
type: "POST",
data:$("#form_Ajax").serialize(),
success: function (data) {
console.log(data);
//如果是一个集合 可以使用each函数遍历取值
$.each(data,function () {
})
}
});
//禁用掉默认行为
return false;
});
</script>
@ResponseBody
注解标注在方法上,方法向前端返回JSON格式的数据。那么与之对应的有一个注解
@RequestBody
是干什么的呢?
@RequestBody
从上图可以看到,该注解能够获取一个请求的请求体,该请求必须是一个POST请求。那么问题来了,他可不可以把一个表单中的数据封装成一个对象呢?就像前边提到的自定义的转换器那种思想。我们来细看:
@RequestMapping("/demo01")
public String demo01(@RequestBody Book book){
System.out.println("请求体"+ book);
System.out.println("程序启动...");
return "success";
}
$("#btn_requestBody").click(function () {
// js对象
var book = {
bookName:$("#bookName").val(),
author:$("#author").val(),
price:$("#price").val()
}
// 转换成json字符串
var bookJson = JSON.stringify(book);
$.ajax({
url:"/demo01",
type:"POST",
data:bookJson,
contentType:"application/json"
});
//禁用默认行为
return false;
});
可以看到,前端传入的 json格式的数据能被后端接收并转换成一个对象。
**注意:**如果报错:JSON parse error: Unrecognized token XXX: was expecting (‘true‘, ‘false‘ or ‘null‘);说明前端传入的不是标准的json格式,我这里之所以会出现这个错误,是应为传的数据直接给序列化了。
这就是
@RequestBody
的作用:前端传一个json字符串,它可以映射成成一个对象
补充:
HttpEntity 参数
以前我们想拿请求头都是使用
@RequestHeader
注解,它有点鸡肋,因为它只能拿到某一个请求头。如果我们想拿到整个请求头,可以给请求方法增加一个参数
HttpEntity<String>
,该参数可以拿到整个请求头。
ResponseEntity
请求方法返回类型为ResponseEntity 意思是可以自定义一个响应体。(一般用不到)
@RequestMapping("/demoResponseEntity")
public ResponseEntity<String> demoResponseEntity(){
String body = "自定义响应体";
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add("Sec-Fetch-Dest", new Date().toString());
// 枚举类型
HttpStatus status = HttpStatus.OK;
return new ResponseEntity<String>(body,headers,status);
}
文件上传
SpringMVC实现文件上传步骤:
1、文件上传表单
<form action="" method="post" enctype="multipart/form-data">
用户头像:<input type="file" name="fileName" >
用户名:<input type="text" name="userName">
<input type="submit" value="上传">
</form>
**注意:**文件上传的表单一定要有 enctype=“multipart/form-data”
2、导包
commons-fileupload-1.3.jar
commons-io-2.4.jar
3、在SpringMVC配置文件中配置文件上传解析器
<!-- 文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--文件上传表单中可能会有普通项-->
<property name="defaultEncoding" value="utf-8"></property>
</bean>
4、文件上传请求处理
只需在文件上传请求方法上加一个
MultipartFile
类型的参数,该参数封装当前文件的信息,可以直接保存
@RequestMapping("/uploadDemo")
public String uploadDemo(@RequestParam("fileName") MultipartFile file , String userName, Model model){
// SpringMVC自动把文件流封装给 file
System.out.println("文件项name的值"+file.getName());
System.out.println("文件名称"+file.getOriginalFilename());
// 文件保存
try {
file.transferTo(new File("G:\\"+file.getOriginalFilename()));
model.addAttribute("msg", "文件上传成功");
} catch (IOException e) {
model.addAttribute("msg",e.getMessage() );
}
return "success";
}
多文件上传:
<form action="${path}/uploadDemo" method="post" enctype="multipart/form-data">
用户头像:<input type="file" name="fileName" ><br/>
用户简历:<input type="file" name="fileName" ><br/>
用户名:<input type="text" name="userName">
<input type="submit" value="上传">
</form>
@RequestMapping("/uploadDemo")
public String uploadDemo(@RequestParam("fileName") MultipartFile[] file , String userName, Model model){
// SpringMVC自动把文件流封装给 file
for (MultipartFile multipartFile : file) {
if(!multipartFile.isEmpty()){
try {
multipartFile.transferTo(new File("G:\\"+multipartFile.getOriginalFilename()));
} catch (IOException e) {
model.addAttribute("msg",e.getMessage() );
}
}
System.out.println("文件项name的值"+multipartFile.getName());
System.out.println("文件名称"+multipartFile.getOriginalFilename());
}
return "success";
}
拦截器
SpringMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作,或者目标方法运行之后进行一些其他处理;
SpringMVC中的拦截器是一个接口
HandlerInterceptor
preHandle:在目标方法运行之前调用;返回 boolean ,如果是true 意味着放行,否则不放行。
postHandle:在目标方法运行之后调用;
afterCompletion:整个请求(来到页面之后、chain.doFilter()放行、资源响应之后)完成后调用;
要实现拦截器就要实现该接口:
1、编写拦截器(实现
HandlerInterceptor
接口)
public class InterceptorController implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle......");
// 返回true则不拦截
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle......");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion......");
}
}
2、配置编写的拦截器
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 拦截所有请求 -->
<bean class="com.atcpl.controller.InterceptorController"></bean>
<!-- 自定义拦截请求 -->
<!--<mvc:interceptor>
<mvc:mapping path="/demo01"/>
<bean class="com.atcpl.controller.InterceptorController"></bean>
</mvc:interceptor>-->
</mvc:interceptors>
拦截器运行流程:
拦截器源码分析:
以DeBug模式调试代码,在执行到控制台打印出东西位置,就是要调试的源码。如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
在获取处理器时可以看到有两个拦截器(最后一个是自定义的)
这句话是执行目标方法,在这之前进行了判断,判断拦截器的返回结果。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
进入
preHandle()
方法细看:
方法注释:
应用注册拦截器的preHandle方法。@return {@code true}如果执行链应该继续下一个拦截器或处理程序本身。否则,DispatcherServlet假定这个拦截器已经处理了响应本身。
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];
//调用拦截器的preHandle方法 ,如果返回false (即!false为true)
//执行拦截器的afterCompletion ,然后再返回一个false
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
// 定义一个索引,用来记录哪些拦截器已经放行了
this.interceptorIndex = i;
}
}
return true;
}
执行完上边的方法会有一个返回值,即
//上边的方法返回的是false,(!false即为true) 目标方法及以后的流程都不会执行,直接执行afterCompletion
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
如果返回值是true,也就是目标方法正常执行完后会执行
mappedHandler.applyPostHandle()
方法,如果目标方法异常,会跳过
mappedHandler.applyPostHandle()
且被两个catch捕获,然后来到页面渲染,这也映证了上边说的拦截器的运行流程。
再来看
postHandle()
方法:
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
// 拿到所有的拦截器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 倒序遍历
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
运行期间有任何异常都会被捕获,执行完 postHandle() 就会执行页面渲染方法。
细看
processDispatchResult()
方法:
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);
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);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
render();
页面渲染方法执行完,如果没有异常则会执行afterCompletion()方法。
当然,如果有异常的话,也会被捕获,所有说只要目标方法正常执行,那么afterCompletion总会执行,这也映证了上边的拦截器的运行流程。
然后我们再来细看
afterCompletion()
方法:
方法注释:
在映射的HandlerInterceptors上触发completion后回调。将只调用所有拦截器的afterCompletion,其预处理句柄调用已成功完成并返回true。
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
//拿到所有处理器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//倒叙遍历
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
已经放行的拦截器的afterCompletion就执行,那么他是如何知道那个拦截器已经放行了呢?
在这个倒叙遍历中,interceptorIndex就是代表着已经放行的拦截器。
这个interceptorIndex就是
preHandle()
方法中遍历时保存已放行的拦截器的索引。从最后一个放行的拦截器开始,把该拦截器之前已经放行了的拦截器的afterCompletion方法都执行了。
异常处理
SpringMVC通过HandlerExceptionResolver处理程序的异常,包括Handler映射、数据绑定以及目标方法执行时发生的异常。
SpringMVC 提供的 HandlerExceptionReslover 的实现类如下:
下面来分析源码:
先来看
initHandlerExceptionResolvers()
:
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// 在ApplicationContext中找到所有HandlerExceptionResolvers,包括祖先上下文。
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// 如果没有找到其他解析器,请注册默认的HandlerExceptionResolvers,
// 以确保我们至少有一些HandlerExceptionResolvers。
if (this.handlerExceptionResolvers == null) {
// 如果没有找到就去找默认的策略
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
默认的策略:(来到spring-webmvc.jar/DispatcherServlet.properties中)
可以看到 HandlerExceptionResolver 的默认值:
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
那么当出现异常时,程序是如何做的呢?
DeBug模式调试代码,如下图中标注。
进入到processDispatchResult方法内:
继续细看异常处理方法
processHandlerException()
:
这三个异常解析器轮流尝试解析。如果这三个异常解析器都不能处理,就把异常抛出去。
而这三个异常都有对应使用环境:
- ExceptionHandlerExceptionResolver:@ExceptionHandler
- ResponseStatusExceptionResolver:@ResponseStatus
- DefaultHandlerExceptionResolver:判断是否SpringMVC自带的异常
第一种解析器(ExceptionHandlerExceptionResolver)所使用的场景:@ExceptionHandler
/**
* 告诉SpringMVC该方法专门处理异常的
* @return
*/
@ExceptionHandler(Exception.class)
public String customizeExceptionDemo(){
return "customizeError";
}
如果想要携带异常信息可以在方发处加上参数 Exception ,SpringMVC只认这一个参数,不能在参数位置写Model、Map之类的参数。那么还想将异常信息携带导页面可以将方法的返回类型设置为 ModelAndView。
@ExceptionHandler(Exception.class)
public ModelAndView customizeExceptionDemo(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
结果如下图:
如果有两个或者多个这种处理异常的方法,如下图,出现异常时精确优先。
抽取异常处理方法:
自定义一个专门处理异常的类 CustomizeException
/**
* 自定义专门处理异常的类
* 把该类加入到容器中使用的注解是 {@ControllerAdvice}而不是那四个
*/
@ControllerAdvice
public class CustomizeException {
@ExceptionHandler(Exception.class)
public ModelAndView customizeExceptionDemo(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
@ExceptionHandler(value = {ArithmeticException.class})
public ModelAndView customizeArithmeticException(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
}
那么问题来了,我这个类中有异常处理的方法,而我某个具体类中也有异常处理的方法,如下两张图,当
exceptionDemo()
方法发生异常时,是具体类(ExceptionController)中的异常处理方法执行呢,还是专门处理异常的类(CustomizeException)中的异常处理方法执行呢?
答案是:具体类中的方法出现异常,具体类中的异常处理方法优先执行。也就是全局异常处理与本类异常处理同事存在时,本类优先且具体异常优先。
第二类异常处理解析器使用场景:@ResponseStatus
@RequestMapping("/loginDemo")
public String loginDemo(@RequestParam("username") String param, Model model){
// 用户不存在抛出自定义异常
if(!"admin".equals(param)){
model.addAttribute("msg","用户名不存在" );
throw new UserNotFoundException();
}
return "success";
}
发送一个请求,且请求带参数
username
,假设username不等admin,抛出自定义的异常;
//自定义异常
@ResponseStatus(reason = "用户不存在",value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException{
}
第三类异常异常处理解析器(DefaultHandlerExceptionResolver)使用场景:SpringMVC自己的异常
for循环遍历三个异常解析器,当当前解析器是 DefaultHandlerExceptionResolver 进入resolverException方法,如下图:
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
可以看到该方法中有很多判断,判断当前异常属于哪一种,然后进行相应的处理。
处理以上三个异常解析器还有一个SimpleMappingExceptionResolver
他是通过配置的方式来进行异常处理。一般不常用,不在多做解释。
总结:
r);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn(“Failure while trying to resolve exception [” + ex.getClass().getName() + “]”, handlerEx);
}
}
return null;
}
可以看到该方法中有很多判断,判断当前异常属于哪一种,然后进行相应的处理。
处理以上三个异常解析器还有一个**SimpleMappingExceptionResolver**
[外链图片转存中...(img-wSapeRTv-1665832263248)]
他是通过配置的方式来进行异常处理。一般不常用,不在多做解释。
------
### 总结:
[外链图片转存中...(img-WFSphuaj-1665832263250)]