二、SpringMVC配置式开发
二、SpringMVC配置式开发
根据下面的图,我们一步一步配置所需内容。

2.1 处理器映射器HandlerMapping
首先我们先配置处理器映射器 HandleMapping。
HandlerMapping 接口负责 根据 request 请求 找到对应的 **Handler 处理器 **及 Interceptor 拦截器,并将它们封装在 HandlerExecutionChain 对象中,返回给中央调度器。其常用的实现类有两种:
- BeanNameUrlHandlerMapping
- SimpleUrlHandlerMapping
2.1.1 BeanNameUrlHandlerMapping
根据【DispatcherServlet.properties】文件,可以得知我们默认的 HandlerMapping 就是 BeanNameUrlHandlerMapping。
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
BeanNameUrlHandlerMapping.class: 接下来我们去看下其源码,第 8 ~ 11 行,执行的是将多个 beanName 进行判断前缀是否存在 “ / ”,若存在则存放到 List 里。
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
/**
* Checks name and aliases of the given bean for URLs, starting with "/".
*/
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<String>();
if (beanName.startsWith("/")) {
urls.add(beanName);
}
String[] aliases = getApplicationContext().getAliases(beanName);
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
return StringUtils.toStringArray(urls);
}
}
2.1.2 SimpleUrlHandlerMapping
若使用上述类,有几个问题:
- 处理器 Bean 的 id 为 一个 url 请求路径,而不是 Bean 的名称,有些奇特。
- 处理器 Bean 的定义与请求 url 绑定在了一起。若出现多个 url 请求同一个处理器的情况,就需要在 Spring 容器中配置多个该处理器类的 <bean/>。这将导致容器会创建多个该处理器类实例。
所以可以使用 SimpleUrlHandlerMapping 处理器映射器,不仅可以将 url 与处理器的定义分离,还可以对 url 进行统一映射管理。
- 使用 SimpleUrlHandlerMapping 类,需要配置 mappings 属性或者 urlMap 属性。
- 配置 mappings 属性需要使用 <props/> 标签;
- 配置 urlMap 属性需要使用 <map/> 标签
<!-- 注册处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<!--
<property name="mappings">
<props>
<prop key="/hello.do">myController</prop>
<prop key="/my.do">myController</prop>
</props>
</property>
-->
<property name="urlMap">
<map>
<entry key="/hello.do" value="myController"/>
<entry key="/my.do" value="myController"/>
</map>
</property>
</bean>
<!-- 注册处理器 -->
<bean id="myController" class="com.hahg.handlers.MyController"/>
但如果不是多个路径访问一个控制器 Controller ,那就没必要用这个类,直接使用默认的 BeanNameUrlHandlerMapping 即可。
2.1.3 执行流程
接下来,我们来看处理器映射器的相关源码,打开 DispatchServlet.class 类,寻找到 doDispatch 方法。
第 12 行: 调用本类即 DispatchServlet 里的 getHandler 方法来获取 handler,然后继续跟进 getHandler 方法的实现。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 。。。。。
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
根据注释可知,其方法的作用是 尝试所有的 HandlerMapping 种类来获取处理器 handler,但这个并不是真正的处理器,根据 第 13 行 代码可知,其返回类型是 HandlerExecutionChain —— 处理器执行链。
/**
* 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
*/
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
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;
}
2.2 处理器适配器HandlerAdapter
适配器模式解决的问题是,使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
所以处理器适配器所起的作用是,将多种处理器(实现了不同接口的处理器),通过处理器适配器的适配,使它们可以进行统一标准的工作,对请求进行统一方式的处理。
详情见 四、SSM框架前言-适配器模式
继续跟进 doDispatch 方法,第 2 行代码是执行 getHandlerAdapter 方法来获取处理器适配器。
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// 。。。
继续跟进 getHandlerAdapter 方法的代码,发现其也是循环 所有的处理器适配器 来找我们所定义的 MyController 处理器所适合的适配器。
/**
* 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 {
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");
}
第 11 行,其遍历出来的适配器使用了 supports 这个方法,我们回顾一下适配器模式。
- 适配器模式有一个接口,每个适配器都会去实现这个接口。如下图所示,实现了 supports 这个方法的有很多类。

- 在之前的例子里,接口有两个方法:
- 一个是 support 方法,来 测试这个类是否符合这个适配器。
- 另外一个是工作方法,就是 执行所需操作的方法,之前的例子里是 work 方法,在本例子是 handle 方法。
下面的代码可以对比下,发现极其相似:
- supports 这个方法里的实现都是 使用 instanceof 运算符;
- 在工作方法里,都需要 类型强转 后才进行操作,当然这里类型强转不会出现问题。
public class CookerAdapter implements IWorkerAdapter {
@Override
public String work(Object worker) {
return ((ICooker)worker).cook();
}
@Override
public boolean supports(Object worker) {
// 根据传进来的对象来判断该对象是否符合此适配器
return (worker instanceof ICooker);
}
}
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
2.3 处理器
处理器除了实现 Controller 接口外,还可以继承自一些其它的类来完成一些特殊的功能。
2.3.1 继承自 AbstractController 类
若处理器继承自 AbstractController 类,那么该控制器就具有了一些新的功能。
先看下 AbstractController 的源码:
- 里面有 handleRequest 方法,这个方法是模板方法,其调用了 handleRequestInternal 方法。
- 而 handleRequestInternal 方法是我们需要实现的,这个就是之前的 模板方法设计模式,这个方法就是 抽象方法,是要求子类 必须 实现的方法。
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return null;
}
// Delegate to WebContentGenerator for checking and preparing.
checkRequest(request);
prepareResponse(response);
// Execute handleRequestInternal in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
return handleRequestInternal(request, response);
}
}
}
return handleRequestInternal(request, response);
}
/**
* Template method. Subclasses must implement this.
* The contract is the same as for {@code handleRequest}.
* @see #handleRequest
*/
protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception;
因为 AbstractController 类还继承自一个父类 WebContentGenerator。WebContentGenerator 类具有 supportedMethods属性,可以设置支持的 HTTP 数据提交方式。默认支持 GET、POST。
若处理器继承自 AbstractController 类,那么处理器就可以通过属性 supportedMethods 来限制 HTTP 请求提交方式了。例如,指定 只支持 POST 的 HTTP 请求提交方式。
<!-- 注册处理器 -->
<bean id="/my.do" class="com.hahg.handlers.MyController">
<property name="supportedMethods" value="POST"/>
</bean>
能提交 POST 请求的只有两个种类:表单请求 和 AJAX 请求
2.3.2 继承自 MultiActionController 类
MultiActionController 类继承自 AbstractController,所以继承自 MultiActionController 类的子类也可以设置HTTP请求提交方式。但该类的最重要的作用是 执行一个 Controller 处理器里的不同方法。
注意
该类在 Spring4 被标识为过时方法,在 Spring5 已被删除
(1)在处理器执定义多个方法
public class MyController extends MultiActionController {
public ModelAndView doFirst(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("message1", "first");
mv.setViewName("/WEB-INF/jsp/welcome.jsp");
mv.setViewName("welcome");
return mv;
}
public ModelAndView doSecond(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("message1", "second");
mv.setViewName("/WEB-INF/jsp/welcome.jsp");
mv.setViewName("welcome");
return mv;
}
}
(2)使用 InternalPathMethodNameResolver 方法名解析器(默认)
MultiActionController类具有一个默认的 MethodNameResolver 解析器。
该方法名解析器要求 方法名以 URI 中资源名称的身份出现,即方法作为一种可以被请求的资源出现。例如:/ xxx / 方法名。
这里只要访问 " / my / xxx.do " 处理器就会在代码中寻找名字为 xxx 的方法。
<!-- 注册处理器 -->
<bean
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/my/*.do">myController</prop>
</props>
</property>
</bean>
<bean id="myController" class="com.hahg.handlers.MyController" />
<!-- 配置视图解析器 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
(3)PropertiesMethodNameResolver方法名解析器
该方法名解析器中主要用于将不同访问链接映射到指定的方法。
- 第 1 ~ 9 行,注册处理器代码不需要改变;
- 第 11 ~ 19 行,配置链接所映射的方法;
- 第 21 ~ 23 行,注入所需要的属性。
<!-- 注册处理器 -->
<bean
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/my/*.do">myController</prop>
</props>
</property>
</bean>
<bean id="propertiesMethodNameResolver"
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
<property name="mappings">
<props>
<prop key="/my/doFirst.do">doSecond</prop>
<prop key="/my/doSecond.do">doFirst</prop>
</props>
</property>
</bean>
<bean id="myController" class="com.hahg.handlers.MyController">
<property name="methodNameResolver" ref="propertiesMethodNameResolver"/>
</bean>
(4)ParameterMethodNameResolver方法名解析器
该方法名解析器中的方法名 作为请求参数的值 出现。
例如请求时可以写为 /xxx?ooo=doFirst,则会访问 xxx 所映射的处理器的 doFirst() 方法。
其中 ooo 为该请求所携带的参数名,而 doFirst 则作为其参数值出现。
<bean id="parameterMethodNameResolver"
class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="paramName" value="method"/>
</bean>
<bean id="myController" class="com.hahg.handlers.MyController">
<property name="methodNameResolver" ref="parameterMethodNameResolver"/>
</bean>
若不指定
2.4 ModelAndView
2.4.1 Model
ModelAndView 即模型与视图,通过 addObject() 方法向模型中添加数据,通过 setViewName() 方法向模型添加视图名称。
看下 ModelAndView 的源码,发现其属性的确有视图 view 和 模型 model。
public class ModelAndView {
/** View instance or view name String */
private Object view;
/** Model Map */
private ModelMap model;
再继续看 model 的类型—— ModelMap 的源码。发现其继承自 LinkedHashMap。而 LinkedHashMap 又继承自 HashMap ,所以 ModelMap 本质上就是哈希表 HashMap 。
public class ModelMap extends LinkedHashMap<String, Object> {
/**
* Construct a new, empty {@code ModelMap}.
*/
public ModelMap() {
}
// 。。。。
}
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
而我们再来看下 HashMap 的源码:里面有一个内部类,是存放哈希表每个元素的值,里面有四个属性,分别是 哈希值、键、值、下一个节点的地址。由此可知 HashMap 本质上是一个单向链表。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
::: warings
在 JDK 1.8 之后 HashMap 中的数组元素和链表节点都采用 Node类 实现,之前是 Entry 类实现。
:::
我们再来看下 LinkedHashMap 的源码,发现其比 HashMap 拥有多了两个属性 before 和 after,分别指向前一个结点和下一个结点。由此可知 LinkedHashMap 本质上是一个双向链表。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
2.4.2 View
View 视图是用来设置跳转页面的,其里面的字符串会经过 视图解析器进行解析,最终转换为相应的页面。后续会详细讲解。
这里需要知道的是若不需要 Model 来携带数据而只使用 View 来跳转页面,则可直接使用 View 的一个构造方法即可。
public class ModelAndView {
/**
* Convenient constructor when there is no model data to expose.
* Can also be used in conjunction with {@code addObject}.
* @param view View object to render
* @see #addObject
*/
public ModelAndView(View view) {
this.view = view;
}
2.5 视图解析器ViewResolver
视图解析器 ViewResolver 接口负责将处理结果生成 View 视图。常用的实现类有四种。
2.5.1 InternalResourceViewResolver 视图解析器
该视图解析器用于完成对 当前 Web 应用内部资源的封装与跳转。
而对于拼接规则是:前辍+ 视图名称+ 后辍。
InternalResourceView 解析器会把处理器方法返回的模型属性都存放到对应的 request 中,然后将请求转发到目标URL。当然,若不指定前辍与后辍,直接将内部资源路径写到setViewName()中也可以。相当于前辍与后辍均为空串。
<!-- 配置视图解析器 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
已知该视图解析器存在两个问题,使其使用很不灵活:
- 只可以完成将内部资源封装后的跳转。但无法转向外部资源,如外部网页。
- 对于内部资源的定义,也只能定义一种格式的资源 ——存放于同一目录的同一文件类型的资源文件。就如同上的代码, Controller 类的提交的视图都只会到【/WEB-INF/jsp/】文件夹下查找。
2.5.2 BeanNameViewResolver 视图解析器
顾名思义就是将资源封装为 " Spring 容器中的 Bean 实例 "。
ModelAndView 通过 设置视图名称为该 Bean 的 id 属性值来完成对该资源的访问。所以在springmvc.xml中,可以定义多个 View 视图Bean,让处理器中 ModelAndView 通过对这些 Bean 的 id 的引用来完成向View中封装资源的跳转。
需要使用到的类:
- RedirectView:定义外部资源视图对象;
- JstlView:定义内部资源视图对象
<!-- 注册处理器 -->
<bean id="/my.do" class="com.hahg.handlers.MyController"/>
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<!-- 定义一个内部资源View对象 -->
<bean id="myInternalView"
class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/welcome.jsp"></property>
</bean>
<!-- 定义两个外部资源View对象 -->
<bean id="taobao"
class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="https://www.taobao.com/"></property>
</bean>
<bean id="jingdong"
class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="https://www.jd.com/"></property>
</bean>
在处理器 MyController 里,将视图设置为上面代码中的 bean 的 id 值,即可访问到指定的链接。
注意
在 JDK 1.8 以后,JDK 不自带服务器相关的 Jar 包,所以在使用 JstlView 类来访问内部资源时,需要手动导入【jstl.jar】
下载链接:[ http://archive.apache.org/dist/jakarta/taglibs/standard/binaries/ ]
public class MyController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("message1", "first");
mv.setViewName("myInternalView");
return mv;
}
}
2.5.3 XmlViewResolver 视图解析器
当需要定义的 View 视图对象很多时,就会使 springmvc.xml 文件变得很大,很臃肿,不便于管理。
所以可以将这些 View 视图对象 专门抽取出来,单独定义为一个xml文件。这时就需要使用 XmlViewResolver 解析器了。
先定义一个 myView.xml 文件,专门存放我们定义的 View 视图。
<!-- myView.xml -->
<!-- 定义一个内部资源View对象 -->
<bean id="myInternalView"
class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/welcome.jsp"></property>
</bean>
<!-- 定义两个外部资源View对象 -->
<bean id="taobao"
class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="https://www.taobao.com/"></property>
</bean>
<bean id="jingdong"
class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="https://www.jd.com/"></property>
</bean>
</beans>
然后修改 springmvc.xml 文件,将视图解析器所用到的类修改成 XmlViewResolver,并 配置 location 属性 来指定我们自定义的 xml 的位置。
<!-- 注册处理器 -->
<bean id="/my.do" class="com.hahg.handlers.MyController"/>
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="location" value="classpath:myViews.xml"/>
</bean>
2.5.4 ResourceBundleViewResolver视图解析器
对于 View 视图对象的注册,除了使用 xml 文件外,也可以在 properties 文件中进行注册。
只不过,此时的视图解析器需要更换为 ResourceBundleViewResolver 解析器。该属性文件需要定义在类路径下,即src下。
而对于属性文件的写法,是有格式要求的:
- 资源名称.(class)=封装资源的View全限定性类名
- 资源名称.url=资源路径
# views.properties
myInternalView.(class)=org.springframework.web.servlet.view.JstlView
myInternalView.url=/WEB-INF/jsp/welcome.jsp
taobao.(class)=org.springframework.web.servlet.view.RedirectView
taobao.url=https://www.taobao.com/
然后需要在容器里进行配置 basename 属性,其值为 properties 文件名。
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
2.5.5 视图解析器的优先级
有时经常需要应用一些视图解析器策略来解析视图名称,即当同时存在多个视图解析器均可解析 ModelAndView 中的同一视图名称时,哪个视图解析器会起作用呢?
视图解析器有一个 order 属性,专门用于设置多个视图解析器的优先级。
数字越小,优先级越高。数字相同,先注册的优先级高。
一般不为 InternalResourceViewResolver 解析器指定优先级,即让其优先级是最低的。
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
<property name="order" value="5"></property>
</bean>