内容一览:
1. 对 SpringMVC 执行流程的一个概述
2. 介绍在搭建 SpringMVC 开发环境时三种常用的处理器映射器(HandlerMapping)和处理器适配器(HandlerAdapter):
- BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
- SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
- RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
SpringMVC 执行流程
先附上一张 SpringMVC 的执行流程图:
更加详细的一幅图:
- 客户端发来一个请求。
- 前端控制器(DispatcherServlet)拦截到这个请求加载相应的配置文件,通过 HandlerMapping(处理器映射器) 去查找该请求是否有对应的 Handler(就是我们写的那个 Controller类,这里专业术语叫 Handler) 来处理。
- 如果有就通过 HandlerAdapter(处理器适配器)去调用这个 Handler 来处理请求
- 适配器返回一个 ModelAndView(数据 + 视图名)。
- 前端控制器通过 ViewResolver(视图解析器)把返回的 ModelAndView 中的 View 解析成一个视图(其实就是页面)。
- 前端控制器将 Model(数据)渲染到 View(视图)。
- 返回给客户端响应。
可能大家看完上面有点偏专业性的解释还是不太懂,所以我就用一个形象的例子来解释一下:
比方说我要去某公司现场面试。那么我去面试这就相当于发起了一个请求,面试总得有前台接待人员吧,这个 DispatcherServlet 就相当于一个前台接待人员。于是,我走到前台接待的小姐姐跟前说:我要面试 xxx 岗位。小姐姐哪记得住公司有没有安排你来面试啊,所以,小姐姐就问你叫什么名字?“我叫 xxx ”,“哦,稍等,我先查一下面试表”,这个面试表就相当于一个 HandlerMapping,它存储了面试者与对应的面试官之间的映射关系。然后她就拿着那张表找到了面试我的面试官(这里的面试官就相当于 Handler,是用来处理我这个面试请求的),然后就通知对应的面试官来面试我。但是,不同的面试官的风格可能不同,有的面试官就希望接待人员能通过打电话的方式来通知他去面试,而有的面试官就有可能喜欢接待人员发个邮件给他来通知他去面试。但是接待小姐姐怎么忙的过来呢?又要去为来应聘的面试者查找对应的面试官,还要通知对应的面试官来面试应聘者,还要以不同的方式来通知,好累的。所以小姐姐就找了几个小哥哥(HandlerAdapter)来帮她以不同的方式来通知面试官来面试。通知到了对应的面试官之后,面试官就开始面试应聘者(处理请求),面试结束之后,面试官就写评语,给一些反馈信息(ModelAndView)。然后再由专门处理面试反馈信息的人员(ViewResolver)来根据反馈信息来评估该面试者(对应图中的第 7 步,将数据渲染到视图),最后,评估结束,给面试者发条短信(响应),通过面试就获得 offer,没通过,下次继续努力!
相信看到这里,大家对 SpringMVC 执行流程的理解又进了一步,不至于那么抽象了。那接下来我们就来讨论一下执行流程中的一些细节:
- 接待的小姐姐是通过一张表(HandlerMapping)来确认面试请求是否有对应的面试官(Handler)来处理,那么到底有多少种 HandlerMapping 呢?
- 接待的小姐姐是通过小哥哥(HandlerAdapter)来通知不同风格的面试官来面试应聘者的,那么又有多少种 HandlerAdapter 呢?
BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
BeanNameUrlHandlerMapping:看名字应该可以猜出来它是通过 bean 的 name 来确定请求所对应的处理器(Handler),需要在注册的 Handler 的 bean 标签上 配置 name 属性为对应的所要处理的请求。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置处理器映射器(面试表) -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- 配置处理器适配器(小哥哥) -->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>
<!-- 配置视图解析器(面试结果评估人员) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
<!-- 注册我们实现的Controller(面试官),该面试官只能处理网站根目录下的 login.do 的请求 -->
<bean name="/login.do" class="com.lyu.qjl.interview.controller.LoginController"></bean>
</beans>
这种方式缺点就很明显了:一个面试官只能处理一个面试请求,因为一个 bean 只能配一个 name 属性。
SimpleControllerHandlerAdapter:如果使用这种处理器适配器来调用 Handler 的话,对应的 Handler 要求必须实现 Controller 接口,重写 handlerRequest 方法,并在配置文件中注册。
上面注册的 LoginController 的源码如下:
package com.lyu.qjl.interview.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
/**
* 类名称:用于处理登录请求的处理器
* 全限定性类名: com.lyu.qjl.interview.controller.LoginController
* @author 曲健磊
* @date 2018年5月21日下午7:02:03
* @version V1.0
*/
public class LoginController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
String username = request.getParameter("username");
String password = request.getParameter("password");
ModelAndView modelAndView = new ModelAndView();
if (username != null && password != null) {
if ("admin".equals(username) && "123".equals(password)) {
modelAndView.addObject("loginFlag", "登录成功");
modelAndView.setViewName("/main.jsp");
} else {
modelAndView.addObject("loginFlag", "用户名或密码错误");
modelAndView.setViewName("/login.jsp");
}
} else {
// 只是把模型放到request域里面,并没有真正的安排好
modelAndView.addObject("loginFlag", "用户名或密码错误");
modelAndView.setViewName("/login.jsp");
}
return modelAndView;
}
}
SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
SimpleUrlHandlerMapping:这种映射器的一个特点是可以把请求以及所对应的处理器之间的映射关系放到一个 HashMap 中,统一进行管理。在映射器的 bean 标签内部定义 property 标签,其 name 属性值为 mappings,在 property 标签内部定义 props 标签,在 props 标签内部就可以定义 prop 标签用来存储对应的请求以及对应的在配置文件中注册的 handler 的 id 了。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 注册另一种映射器(面试表) -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<!-- key的值请求的名称,标签体内为对应的handler的id,可配置多个 -->
<prop key="/getUserList.do">userController</prop>
<prop key="/xxx.do">xxxController</prop>
<prop key="/yyy.do">yyyController</prop>
</props>
</property>
</bean>
<!-- 另一种HandlerAdapter(通知面试官来面试的小哥哥) -->
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"></bean>
<!-- 配置视图解析器(面试评估人员) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
<!-- 注册handler(面试官) -->
<bean id="userController" class="com.lyu.qjl.interview.controller.UserController"></bean>
</beans>
可以看出来,这种配置方式还是一个处理器只能处理一个请求,只不过把它们之间的映射关系整理到了一起,放到了一个 HashMap 中,便于管理。
Question:从哪里看出来它是把请求与处理器之间的映射关系放到了 HashMp 中?
Answer:源码如下
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
// 用来存储请求以及对应的处理器之间映射关系的一个 HashMap
private final Map<String, Object> urlMap = new HashMap<String, Object>();
/**
* Map URL paths to handler bean names.
* This is the typical way of configuring this HandlerMapping.
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
* details, see the {@link org.springframework.util.AntPathMatcher} javadoc.
* @param mappings properties with URLs as keys and bean names as values
* @see #setUrlMap
*/
// 标签中配置的 mappings 属性,按照小驼峰命名法在 mappings 前面加上 set,然后通过反射调用该方法注入到 this.urlMap 中,向上看
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
...
}
一目了然。
HttpRequestHandlerAdapter:使用这种处理器适配器来调用 Handler 的话,对应的 Handler 要求必须实现 HttpRequestHandler 接口,重写 handlerRequest 方法,并在配置文件中注册。
package com.lyu.qjl.interview.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.HttpRequestHandler;
import com.lyu.qjl.interview.entity.User;
/**
* 类名称:处理用户请求的handler
* 全限定性类名: com.lyu.qjl.interview.controller.UserController
* @author 曲健磊
* @date 2018年5月21日下午7:24:07
* @version V1.0
*/
public class UserController implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("进入UserController");
List<User> userList = new ArrayList<User>();
User user1 = new User("arry", 18, "男");
User user2 = new User("cc", 28, "女");
User user3 = new User("dd", 38, "男");
userList.add(user1);
userList.add(user2);
userList.add(user3);
request.setAttribute("userList", userList);
request.getRequestDispatcher("/userList.jsp").forward(request, response);
}
}
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
RequestMappingHandlerMapping:使用这种处理器映射器的话,只需要在编写的处理器的方法上加上 @RequestMapping(“/xxx.do”) 注解就可以完成请求到处理请求的方法上的映射,而且使用这种方式的话是一个方法对应一个请求,相比于上面两种映射器,高效许多。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置注解方式的处理器映射器(面试表) -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean>
<!-- 配置注解方式的处理器适配器(小哥哥) -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
<!-- 配置视图解析器(面试评估人员) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
</beans>
RequestMappingHandlerAdapter:对应的 handler 只需要使用注解来标示即可,并且在配置文件中要开启注解扫描,便于 Spring 在初始化的时候就加载并实例化对应的 Handler。
package com.lyu.qjl.interview.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* 类名称:通过注解的方式来实现一个Controller
* 全限定性类名: com.lyu.qjl.interview.controller.AnnoLoginController
* @author 曲健磊
* @date 2018年5月21日下午7:53:21
* @version V1.0
*/
@Controller
public class AnnoLoginController {
@RequestMapping("/loginAnno")
public ModelAndView login(String username, String password) {
ModelAndView modelAndView = new ModelAndView();
if (username != null && password != null) {
if ("admin".equals(username) && "123".equals(password)) {
modelAndView.addObject("loginFlag", "登录成功");
modelAndView.setViewName("main");
} else {
modelAndView.addObject("loginFlag", "用户名或密码错误");
modelAndView.setViewName("loginAnno");
}
} else {
// 只是把模型放到request域里面,并没有真正的安排好
modelAndView.addObject("loginFlag", "用户名或密码错误");
modelAndView.setViewName("loginAnno");
}
return modelAndView;
}
}
总结
前面的那两种 HandlerMapping 和 HandlerAdapter 之间可以任意搭配,如果是使用 RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 的话必须配对出现,其实 SpringMVC 对此也做出了简化,使用最后一种方式来配置的话可以在配置文件中用一个 <mvc:annotation-driven />
来替代,配置文件就可以简化成下面这样:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 开启spring的组件扫描,自动扫描包,加载bean -->
<context:component-scan base-package="com.lyu.qjl.interview.controller" />
<!-- 可以用mvc的注解驱动来代替 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter -->
<mvc:annotation-driven />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
</beans>
对应的方法返回的视图名通常是 /userList.jsp
、 /main.jsp
这样的格式,我们可以把视图名的前缀和后缀提取出来放到配置文件中,最终的配置文件的内容就像下面这样:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 开启spring的组件扫描,自动加载bean -->
<context:component-scan base-package="com.lyu.qjl.interview.controller" />
<!-- 可以用mvc的注解驱动来代替 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter -->
<mvc:annotation-driven />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
这样视图解析器在解析视图名的时候就会自动把前缀(prefix)和后缀(suffix)与返回的 view 的值进行拼接了,在方法内设置视图名的时候就只写 userList
、main
就可以了。
最后,说了这么多,嘿嘿,其实 SpringMVC 最常用的配置方式还是最后一种。前面那两种是 SpringMVC 以前的版本的使用方式,所以以前的时候 SpringMVC 并不火,直到出了 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 这种配置方式之后大大简化了处理器的配置,使 SpringMVC 在处理请求方面相比于 Struts2 做到了接近零配置,才彻底的火了起来。