Spring MVC 源码剖析
1. 从Servlet开始谈
1.1 DispactherServlet的继承层次
Spring MVC是基于Servlet开发的。核心调度器(DispactherServlet)类的继承层次
1.2 Web 容器调用的入口
Web容器会调用 HttpServlet 的service方法
protected void service(HttpServletRequest req, HttpServletResponse resp){}
然而它的子类 FrameworkServlet 重写了该方法
/**
* Override the parent class implementation in order to intercept PATCH requests.
*/
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
}
else {
super.service(request, response);
}
}
这个重写的方法只是增加了处理 HttpMethod.PATCH 的请求方式,最终还是调用父类的super.service(request, response);
父类还是根据request的请求方式(GET/POST/…)判断调用那种方法doGet()/doPost()/…
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
所以最终的逻辑处理还是落到了每种请求方式的对应的方法doGet、doPost、doPut上,然而正如我们所料HttpServlet地这些方法都被子类FrameworkServlet重写。
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
// 省略n行...
FrameworkServlet 重写的方法统一都调用了processRequest(request, response); 这些final方法不允许再被FrameworkServlet子类重写了。
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
// 重点在这里
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
}
else {
if (asyncManager.isConcurrentHandlingStarted()) {
logger.debug("Leaving response open for concurrent processing");
}
else {
this.logger.debug("Successfully completed request");
}
}
}
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
这个方法中我们关注doService(request, response); 这个方法被子类DispatcherServlet的方法重写。来看一下子类的实现
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
}
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
// 这里最重要,请看这里
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
最核心的逻辑就在DispatcherServlet的doDispatch(request, response)中完成。
这里使用的是Spring 5.0.9,比起以前低版本3.xx的代码会不一样,这也说明Spring社区很给力,经常重构代码。虽然形式不一样但是逻辑一样,而且高版本比低版本更易读。
/**
* Process the actual dispatching to the handler
*/
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 (logger.isDebugEnabled()) {
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;
}
// 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);
}
}
}
}
2. DispatcherServlet#doDispatch() 关键逻辑解析
2.1 doDispatch的执行流程图
2.2 处理器(Handler)
处理器就是定义一段功能代码,用于完成某一请求,往往被称为后端处理器,举两个实现的例子:
- 实现Controller接口或继承AbstractController
public class MyDemoController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 处理自己的业务...
//创建modelAndView准备填充数据、设置视图
ModelAndView modelAndView = new ModelAndView();
//填充数据
modelAndView.addObject("items", "MyModel");
//视图
modelAndView.setViewName("page/mypage");
return modelAndView;
}
}
这段代码就是一个处理器(Handler) JavaBean,这个类的方法handleRequest如何与请求关联,
这里给出示例配置:
<!-- controller配置 -->
<bean name="/myDemoController.action" id="myDemoController" class="com.yp.demo.MyDemoController"/>
- 使用注解方式实现Handler
@Controller
public class MyAnnoDemoController{
@RequestMapping("/myAnnoDemoController.action")
public ModelAndView queryItems() {
// 逻辑处理
// 创建modelAndView准备填充数据、设置视图
ModelAndView modelAndView = new ModelAndView();
//填充数据
modelAndView.addObject("items", "MyModel");
//视图
modelAndView.setViewName("page/mypage");
return modelAndView;
}
}
当然是RSETful风格的可一个使用@RestController注解,并返回json格式数据。注解配置那肯定要开启注解扫描
<!-- 扫描controller注解,多个包中间使用半角逗号分隔 -->
<context:component-scan base-package="com.yp"/>
对于RSETful的Controller返回json无需配置试图解析器,前端或移动端直接使用json即可,及实现前后端分离。
2.3 处理器映射器(HandlerMapping)
2.2说明了Handler的配置,那如何将request和Handler关联,处理器映射器是实现request和handler的关联。
先看一下HandlerMapping的继承关系
- 配置BeanNameUrlHandlerMapping
<!—beanName Url映射器 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
BeanNameUrlHandlerMapping:表示将定义的Bean名字作为请求的url,需要将编写的controller在spring容器中进行配置,且指定bean的name为请求的url,且必须以.action结尾。如上2.2的第一个例子name="/myDemoController.action"
- 配置SimpleUrlHandlerMapping
它可以将url和处理器bean的id进行统一映射配置。
<!—简单url映射 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/items1.action">controller的bean id</prop>
<prop key="/items2.action">controller的bean id</prop>
...
</props>
</property>
</bean>
- 配置RequestMappingHandlerMapping
注解式处理器映射器,注解式处理器映射器,对类中标记@ResquestMapping的方法进行映射,根据@ResquestMapping定义的url匹配ResquestMapping标记的方法,匹配成功返回HandlerMethod对象给前端控制器DispatcherServlet,HandlerMethod对象中封装url对应的方法Method。
<!--注解映射器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
2.4 分析调用逻辑图中2和3代码
在doDispatch中
HandlerExecutionChain mappedHandler = null;
...
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
DispatcherServlet#getHandler()
* Return the HandlerExecutionChain for this request.
* <p>Tries all handler mappings in order.
* @param request current HTTP request
* @return the HandlerExecutionChain, or {@code null} if no handler could be found
*/
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace(
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
this.handlerMappings其实就是我们配置的HandlerMapping
HandlerExecutionChain封装了我的定义的Handler 和处理其拦截器HandlerInterceptor,当然定义了HandlerInterceptor并且匹配到才会封装。
2.5 处理器适配器(HandlerAdapter)
拿到HandlerExecutionChain获取到Handler怎么执行,因为每种Handler的实现不同,执行方式就不同,所以要找到Handler对应的是配器。
在doDispatch中
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
DispatcherServlet#getHandlerAdapter()
/**
* Return the HandlerAdapter for this handler object.
* @param handler the handler object to find an adapter for
* @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
*/
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter ha : this.handlerAdapters) {
if (logger.isTraceEnabled()) {
logger.trace("Testing handler adapter [" + ha + "]");
}
if (ha.supports(handler)) {
return ha;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
可以看出是在轮询我们配置的处理器适配器,判断适配器是否支持该处理器
ha.supports(handler)
HandlerAdapter # supports()
除去测试的实现(绿色背景)有四个实现方式,本质上都是用instanceof运算符检查
- 第一个AbstractHandlerMethodAdapter#supports()就是支持注解方式的处理器适配器
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
* @param handler the handler instance to check
* @return whether or not this adapter can adapt the given handler
*/
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
- 第三个SimpleControllerHandlerAdapter#supports()就是实现Controller接口的Handler
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
- 第二个HttpRequestHandlerAdapter#supports()是一个没有返回值的http请求的Handler
@Override
public boolean supports(Object handler) {
return (handler instanceof HttpRequestHandler);
}
为什么没有返回值应为接口就是这样定义的
@FunctionalInterface
public interface HttpRequestHandler {
/**
* Process the given request, generating a response.
* @param request current HTTP request
* @param response current HTTP response
* @throws ServletException in case of general errors
* @throws IOException in case of I/O errors
*/
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
处理器适配器的定义一样是JavaBean,举例
SimpleControllerHandlerAdapter
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
SimpleControllerHandlerAdapter:即简单控制器处理适配器,所有实现了org.springframework.web.servlet.mvc.Controller 接口的Bean作为
Springmvc的后端控制器Handler。
RequestMappingHandlerAdapter
<!--注解适配器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
注解式处理器适配器,对标记@ResquestMapping的方法进行适配。
HttpRequestHandlerAdapter
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"/>
HttpRequestHandlerAdapter,http请求处理器适配器,所有实现了org.springframework.web.HttpRequestHandler 接口的Bean通过此适配器进行适配、执行。
2.6 <mvc:annotation-driven>
注解的处理器映射器和处理器适配器配置内容有点多,于是Spring提供了下面这个一箭多雕的配置
<!-- 注解驱动配置处理器映射器,和处理器适配器,自定义参数绑定,json交互等功能 -->
<mvc:annotation-driven></mvc:annotation-driven>
2.7 handler处理器的执行
doDispatch中
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
实现类
SimpleControllerHandlerAdapter适配器执行Handler
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
可以看到把handler的类型强转为Controller调用它的handleRequest方法,并返回模型视图ModelAndView
2.8 渲染视图(ViewResolver)
同样的视图解析器也需要配置
<!-- ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
- InternalResourceViewResolver:支持JSP视图解析
- viewClass:JstlView表示JSP模板页面需要使用JSTL标签库,所以classpath中必须包含jstl的相关jar 包;
- prefix 和suffix:查找视图页面的前缀和后缀,最终视图的址为:
前缀+逻辑视图名+后缀,逻辑视图名需要在controller中返回ModelAndView指定,比如逻辑视图名为hello,则最终返回的jsp视图地址 “WEB-INF/jsp/hello.jsp”
代码分析
在doDispatch中
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
DispatcherServlet#processDispatchResult()
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 看这里
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
DispatcherServlet#reder()
/**
* Render the given ModelAndView.
* <p>This is the last stage in handling a request. It may involve resolving the view by name.
* @param mv the ModelAndView to render
* @param request current HTTP servlet request
* @param response current HTTP servlet response
* @throws ServletException if view is missing or cannot be resolved
* @throws Exception if there's a problem rendering the view
*/
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// 会根据ModelAndView选择合适的视图来进行渲染
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
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 + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}
2.9 处理器拦截器(HandlerInterceptor)
Spring Web MVC 的处理器拦截器类似于Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理。
在dodispatch中
// controller执行前调用此方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// controller执行后但未返回视图前调用此方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
自定义拦截器
Public class HandlerInterceptor1 implements HandlerInterceptor{
/**
* controller执行前调用此方法
* 返回true表示继续执行,返回false中止执行
* 这里可以加入登录校验、权限拦截等
*/
@Override
Public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// TODO Auto-generated method stub
Return false;
}
/**
* controller执行后但未返回视图前调用此方法
* 这里可在返回用户前对模型数据进行加工处理,比如这里加入公用信息以便页面显示
*/
@Override
Public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
/**
* controller执行后且视图返回后调用此方法
* 这里可得到执行controller时的异常信息
* 这里可记录操作日志,资源清理等
*/
@Override
Public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
}
配置拦截器
- 针对某种mapping配置拦截器
<bean
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="handlerInterceptor1"/>
</list>
</property>
</bean>
<bean id="handlerInterceptor1" class="com.yp.HandlerInterceptor1"/>
- 针对所有mapping配置全局拦截器
<!--拦截器 -->
<mvc:interceptors>
<!--多个拦截器,顺序执行 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.yp.HandlerInterceptor1"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.yp.HandlerInterceptor2"></bean>
</mvc:interceptor>
</mvc:interceptors>
3. Spring MVC 的Web配置
3.1 在WEB-INF\web.xml中配置前端控制器
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
load-on-startup:表示servlet随服务启动;
url-pattern:*.action的请交给DispatcherServlet处理。
contextConfigLocation:指定springmvc配置的加载位置,如果不指定则默认加
载WEB-INF/[DispatcherServlet 的Servlet 名字]-servlet.xml。
3.2 Servlet拦截方式
1、拦截固定后缀的url,比如设置为 .do、.action, 例如:/user/add.action
此方法最简单,不会导致静态资源(jpg,js,css)被拦截。
2、拦截所有,设置为/,例如:/user/add /user/add.action
此方法可以实现REST风格的url,很多互联网类型的应用使用这种方式。
但是此方法会导致静态文件(jpg,js,css)被拦截后不能正常显示。需要特殊处理。
3、拦截所有,设置为/*,此设置方法错误,因为请求到Action,当action转到jsp时再次被拦截,提示不能根据jsp路径mapping成功。
3.3 springmvc配置文件
Springmvc默认加载WEB-INF/[前端控制器的名字]-servlet.xml,也可以在前端控制器定义处指定加载的配置文件,如下:
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
如上代码,通过contextConfigLocation加载classpath下的springmvc.xml配置文件。
3.4 静态资源访问<mvc:resources>
如果在DispatcherServlet中设置url-pattern为 /则必须对静态资源进行访问处理。
spring mvc 的<mvc:resources mapping="" location="">实现对静态资源进行映射访问。
如下是对js文件访问配置:
<mvc:resources location="/js/" mapping="/js/**"/>
3.5 Post时中文乱码
在web.xml中加入:
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
以上可以解决post请求乱码问题。
对于get请求中文参数出现乱码解决方法有两个:
- 修改tomcat配置文件添加编码与工程编码一致
<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
- 参数进行重新编码:
String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8")
ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码
4. 参数绑定
4.1 默认支持的参数类型
- HttpServletRequest
通过request对象获取请求信息 - HttpServletResponse
通过response处理响应信息
@RequestMapping(value = "/defaultParams", method = RequestMethod.GET)
public User userList(HttpServletRequest request, HttpServletResponse response) {
logger.info(request.getRequestURL());
User user = new User();
user.setNickname("wahaha");
user.setAge(23);
user.setGender("female");
model.addAttribute(user);
return user;
}
返回结果:
- HttpSession
通过session对象得到session中存放的对象 - Model/ModelMap
ModelMap是Model接口的实现类,通过Model或ModelMap向页面传递数据,如下:
Items item = itemService.findItemById(id);
model.addAttribute("item", item);
页面通过${item.XXXX}获取item对象的属性值。
使用Model和ModelMap的效果一样,如果直接使用Model,springmvc会实例化ModelMap。
4.2 简单类型
当请求的参数名称和处理器形参名称一致时会将请求参数与形参进行绑定。int/Integer、long/Long、float/Float、double/Double、boolean/Boolean、String。对于布尔类型的参数,请求的参数值为true或false。
如果是基本类型primitive type,则必须有参数绑定,否则会抛出下面异常:
java.lang.IllegalStateException: Optional int parameter ‘intParam’ is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.
@RequestMapping(value = "basicParamsBinding", method = RequestMethod.GET)
public String testBasicParamsBinding(int intParam, Long longParam, double doubleParam, Boolean booleanParam, String stringParam) {
String dash = "---";
String result = intParam + dash + longParam + dash + doubleParam + dash + booleanParam + dash + stringParam;
logger.info(result);
return result;
}
参数:
运行结果:
4.3 JavaBean的封装
@RequestMapping(value = "javaBeanBinding", method = RequestMethod.POST)
public User testJavaBeanBinding(User user) {
return user;
}
输入:
输出:
4.4 包装类型的JavaBean
包装Bean
public class Car {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
Controller
@RequestMapping(value = "wrapJavaBeanBinding", method = RequestMethod.POST)
public Car testWrapJavaBeanBinding(Car car) {
return car;
}
request:
reponse:
4.5 Json数据传输
-
@RequestBody注解
用于读取http请求的内容(字符串),通过springmvc提供的HttpMessageConverter接口将读到的内容转换为json、xml等格式的数据并绑定到controller方法的参数上。 -
@ResponseBody
作用:
该注解用于将Controller的方法返回的对象,通过HttpMessageConverter接口转换为指定格式的数据如:json,xml等,通过Response响应给客户端
Springmvc默认用MappingJacksonHttpMessageConverter(Spring4和5是MappingJackson2HttpMessageConverter)对json数据进行转换,在注解适配器中加入messageConverters
<!--注解适配器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
</list>
</property>
</bean>
更简洁的配置方式:
<mvc:annotation-driven />
java示例:
@RequestMapping(value = "jsonBinding", method = RequestMethod.POST)
public @ResponseBody Car testJsonBinding(@RequestBody Car car) {
return car;
}
4.6 自定义参数绑定
例如根据业务需求自定义日期格式进行参数绑定
- 自定义Converter
public class CustomDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.parse(source);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 常用配置:
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
<!-- conversionService -->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 转换器 -->
<property name="converters">
<list>
<bean class="com.yp.converter.CustomDateConverter"/>
</list>
</property>
</bean>
5. Controller方法返回值
5.1 返回ModelAndView
Controller方法中定义ModelAndView对象并返回,对象中可添加model数据、指定view。
5.2 返回void
在controller方法形参上可以定义request和response,使用request或response指定响应结果:
- 使用request转发页面
request.getRequestDispatcher("页面路径").forward(request, response);
- 也可以通过response页面重定向:
response.sendRedirect("url")
- 也可以通过response指定响应结果,例如响应json数据如下:
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("json串");
5.3 返回字符串
- 逻辑视图名
controller方法返回字符串可以指定逻辑视图名,通过视图解析器解析为物理视图地址。
return "page/saveXxx";
指定逻辑视图名,经过视图解析器(如2.8配置)解析为jsp物理路径:
/WEB-INF/jsp/page/saveXxx.jsp
- Redirect重定向
Contrller方法返回结果重定向到一个url地址,如下商品修改提交后重定向到商品查询方法,参数无法带到商品查询方法中。
return "redirect:editXxx.action";
重定向到editXxx.action地址,request无法带过去
redirect方式相当于“response.sendRedirect()”,转发后浏览器的地址栏变为转发后的地址,因为转发即执行了一个新的request和response。
由于新发起一个request原来的参数在转发时就不能传递到下一个url,如果要传参数可以/item/editXxx.action后边加参数,如下:
return "redirect:editXxx.action?AAA=aaa&BBB=bbb";
- forward转发
controller方法执行后继续执行另一个controller方法,如下商品修改提交后转向到商品修改页面,修改商品的id参数可以带到商品修改方法中。
return "forward:updateXxx.action";
结果转发到updateXxx.action,request可以带过去
forward方式相当于“request.getRequestDispatcher().forward(request,response)”,转发后浏览器地址栏还是原来的地址。转发并没有执行新的request和response,而是和转发前的请求共用一个request和response。所以转发前请求的参数在转发后仍然可以读取到。
5.4 返回Json
前后端分离的场景示例见4.5
6. 异常处理器
Spring MVC在处理请求过程中出现异常信息交由异常处理器进行处理,自定义异常处理器可以实现一个系统的异常处理逻辑。
6.1 异常处理思路
系统中异常包括两类:预期异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。
系统的dao、service、controller出现都通过throws Exception向上抛出,最后由springmvc前端控制器交由异常处理器进行异常处理,如下图:
6.2 自定义异常类
为了区别不同的异常通常根据异常类型自定义异常类,这里我们创建一个自定义系统异常,如果controller、service、dao抛出此类异常说明是系统预期处理的异常信息。
public class CustomException extends Exception {
/** serialVersionUID*/
private static final long serialVersionUID = -5212079010855161498L;
public CustomException(String message){
super(message);
this.message = message;
}
//异常信息
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
6.3 自定义异常处理器
实现HandlerExceptionResolver 接口
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
ex.printStackTrace();
CustomException customException = null;
//如果抛出的是系统自定义异常则直接转换
if(ex instanceof CustomException){
customException = (CustomException)ex;
}else{
//如果抛出的不是系统自定义异常则重新构造一个未知错误异常。
customException = new CustomException("未知错误,请与系统管理 员联系!");
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", customException.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
}
6.4 错误页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>错误页面</title>
</head>
<body>
您的操作出现错误如下:<br/>
${message }
</body>
</html>
6.5 异常处理器配置
在springmvc.xml中添加:
<!-- 异常处理器 -->
<bean id="handlerExceptionResolver" class="com.yp.exceptionResolver.CustomExceptionResolver"/>
7. 文件上传
7.1 配置多部件解析器
<!-- 文件上传 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置上传文件的最大尺寸为5MB -->
<property name="maxUploadSize">
<value>5242880</value>
</property>
</bean>
7.2 在Controller方法形成加MultipartFile
@RequestMapping(value = "fileUpload", method = RequestMethod.POST)
public void testFileUpload(MultipartFile file) throws IOException {
// 获取文件输入流
// InputStream inputStream = file.getInputStream();
// 获取文件的原始名称
String filename = file.getOriginalFilename();
file.transferTo(new File("D:\\" + filename));
}
7.3 表单标签file的name与Controller形参一致
不要忘了enctype=“multipart/form-data”
<input type="file" name="file">