在SpringMVC中, 开发者不在须要关心Servlet等组件的机制. 只须要按照SpringMVC的约定(框架使用方法): 在控制器中添加方法并声明能够处理的请求, 将数据保存至Model中返回视图便可.前端
SpringMVC在J2EE上进行了封装, 让开发者的工做更专一于业务. 在J2EE中, 只有Filter和Servlet才能够处理请求, 因为Filter更偏向于进行验证处理(如请求的合法性等), 所以处理业务请求须要由Servlet完成. 在SpringMVC中开发者不须要实现Servlet就能够处理请求. 下面来分析一下SpringMVC的实现机制.web
使用J2EE开发时, 每次请求都须要一个Servlet处理, 整个过程能够模拟为患者在医院就医的过程. 每一个患者去医院至关于一次请求, 那么处理此次请求的Servlet就是一名医生.数组
去小型诊所就医时能够直接去医生办公室办理手续后就能够接受诊治. 每一个医生都了解如何给患者办理手续. 但对于大型医院来讲, 因为患者和科室较多, 让每一个医生学习这些非专业的流程是比较浪费资源的. 大型医院会设置前台, 由前台负责给患者办理手续, 办理完成后根据患者的病情交由不一样学科的医生进行诊治. 当有医生休息, 调岗, 离职或新加入时都须要按照统一的格式填写相应的变更信息. 前台按期收集整理, 保证可以准确的了解当日的医生出诊信息. 同时, 将相同窗科的医生安排在同一办公室, 将出诊医生安排在相对集中的办公区, 这样既方便前台搜集医生出诊信息, 也便于对医生的管理.bash
全部患者来医院就医时都由前台统一接待并办理手续, 根据收集的医生出诊信息引领至对应的医生就行诊治. 经过前台实现了对患者就医流程的统一管理.服务器
SpringMVC就是按照上述思路进行处理的, 他也有这样一个前台, 叫作前端控制器.mvc
上述文案转换为SpringMVC描述:app
每一个控制器方法(医生)中声明能够处理的请求(填写能够诊治的病). 前端控制器(前台)在每日上班(每次项目启动)时, 去医生办公区(控制器所在目录)的每一个科室(控制器)收集(加载)各个医生(控制器方法)能够诊治的病(处理的请求), 汇总并整理成文档(方法与URL映射Mapping). 当患者(客户端)来就医(发送请求)时, 由前台接待(前端控制器处理全部请求), 前台根据患者的病情(访问的请求URL)从整理的文档(Mapping)中找到能够诊治该病的医生(控制器方法), 并交由(分发)相应的医生进行诊治(执行业务逻辑).框架
因而可知, 前端控制器是一个负责处理全部请求的Servlet.ide
上述已经说明了SpringMVC前端控制器的实现原理, 下面经过代码实现一个前端控制器.学习
大规模的系统中可能有成百上千个类, 若是前端控制器在加载控制器时扫描全部的类势必会严重影响加载速度, 所以咱们应当告知前端控制器须要被扫描的控制器所在的具体目录, 也就是说须要前端控制器配置参数. 这样前端控制器在扫描时只须要遍历指定目录下的类便可.
常见的方式是在定义前端控制器(web.xml
配置Servlet)时配置初始化参数. 但随着框架功能不断的增长, 前端控制器的配置项会愈来愈多, 这种方式并不灵活. 所以须要采用一种更加灵活的方式: 经过XML配置, 在前端控制器加载时经过读取XML配置文件并解析获取控制器所在目录.
配置控制器目录的XML格式以下: controller
节点为控制器相关配置, package
属性为控制器所在目录.
<mvc>
<controller package="com.atd681.xc.ssm.controller" />
</mvc>
复制代码
定义控制器方法设置能够处理请求的注解.
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
// 请求URL
String url();
// HTTP方法
String method() default "GET";
}
复制代码
控制器方法统一使用注解声明能够处理的请求.
public class UserController {
// 处理"/list"请求
@RequestMapping(url = "/list")
public String userList() {
return "";
}
// 处理"/detail"请求
@RequestMapping(url = "/detail")
public String userDetail() {
return "";
}
}
复制代码
J2EE中规定, Servlet在被加载时会执行init方法, 所以咱们能够把加载控制器的过程写在init方法中.
根据约定优于配置的原则: 咱们约定好配置文件的路径在classpath下, 名称为mvc.xml, 有些场景下用户须要自定义配置文件的路径和名称, 所以咱们也须要支持用户自定义, 自定义配置文件路径和名称时在web.xml
中经过名为configLocation
的参数传入前端控制器.
/**
* 前端控制器(负责处理全部请求)
*/
public class DispatcherServlet extends HttpServlet {
// 默认MVC配置文件路径
private static final String DEFAULT_CONFIG_LOCATION = "mvc.xml";
/**
* 初始化Servlet. 容器初始化Servlet时调用, 加载配置文件初始化MVC相关组件(控制器,视图解析器等)
*/
@Override
public void init() throws ServletException {
// 获取用户自定义的配置文件路径
String configLocation = getInitParameter("configLocation");
// 未定义配置文件路径, 使用默认配置文件路径
if (configLocation == null || "".equals(configLocation)) {
configLocation = DEFAULT_CONFIG_LOCATION;
}
try {
// 开始加载配置文件(JDom解析XML)
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
Document doc = new SAXBuilder().build(new File(classpath, configLocation));
// 解析配置文件中控制器的配置
initController(doc);
} catch (Exception e) {
throw new ServletException("加载配置文件错误", e);
}
}
}
复制代码
在前端控制器中定义全局变量, 用来保存控制器方法与URL的映射关系, 如下简称urlMapping.
// URL映射MAP(K:URL, V:对应的控制器方法)
private static Map<String, Method> urlMappings = new HashMap<String, Method>();;
复制代码
在配置文件中获取控制器目录并遍历该目录, 获取每一个控制器的文件名称. 经过JAVA反射分别加载控制器, 将每一个方法及对应的URL保存至映射urlMapping中.
/**
* 解析配置文件中的控制器配置
*
* @param doc XML配置文件
* @throws Exception
*/
@SuppressWarnings("unchecked")
private void initController(Document doc) throws Exception {
// 配置格式:<controller package="com.atd681.xc.ssm.controller"/>
// package为控制器所在目录, 模拟SpringMVC配置文件中的控制器包扫描
List<Element> controllerEle = doc.getRootElement().getChildren("controller");
if (controllerEle == null) {
throw new Exception("请配置Controller节点.");
}
// 获取配置文件中的控制器所在目录
String controllerPackage = controllerEle.get(0).getAttributeValue("package");
if (controllerPackage == null) {
throw new Exception("Controller的package属性必须设置.");
}
// 获取控制器目录的在磁盘中的绝对路径(D:\atd681-xc-ssm\com\atd681\controller)
// Java目录分隔符需转换为文件系统格式(com.atd681 -> com/atd681)
String controllerDir = controllerPackage.replaceAll("\\.", "/");
String controllerPath = getClass().getClassLoader().getResource(controllerDir).getPath();
// 遍历控制器目录下的全部CLASS
for (File controller : new File(controllerPath).listFiles()) {
String className = controller.getName().replaceAll("\\.class", ""); // 控制器类名称
Class<?> clazz = Class.forName(controllerPackage + "." + className); // 加载控制器类
// 遍历控制器类的全部方法,将每一个方法和处理的URL作映射
for (Method method : clazz.getMethods()) {
// 只处理有@RequestMapping注解的方法
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
RequestMapping rm = method.getAnnotation(RequestMapping.class);
// 同一URL可能以GET或POST提交, URL和HTTP方法(GET/POST)才能肯定是相同的请求
// 将URL和HTTP方法做为KEY, 使用统一方法生成KEY便于在分发时准确的获取对应的方法
String urlKey = wrapperKey(rm.url(), rm.method());
// 当多个方法同时声明了相同的请求时, 在前端控制器分发时没法准确的找到对应方法
if (urlMappings.containsKey(urlKey)) {
throw new Exception("URL不能重复, URL: " + rm.url());
}
// 保存URL及对应的方法
urlMappings.put(urlKey, method);
}
}
}
复制代码
多个方法配置了处理相同的URL后, 前端控制器在收到请求进行分发时将没法分辨本次请求应该由哪一个方法处理, 所以不能有两个以上的方法声明处理同一URL, 加载控制器时须要进行URL重复性验证.
HTTP支持使用GET/POST等多种方式请求同一URL. 例: GET方式请求/add
表明访问添加页, POST方式请求/add
表明提交数据. 因为两次请求业务不一样, 须要有两个方法分别处理. 所以须要有两个方法配置处理/add
请求, 用HTTP Method
区分(GET&POST). 在设置控制的方法urlMapping
时须要使用URL+HTTP Method
做为KEY. 在请求分发时也应该根据URL+HTTP Method
作为KEY找到对应处理方法.
封装统一的规则生成urlMapping
的KEY, 在设置urlMapping
和分发时使用相同的KEY.
/**
* 封装URL映射的KEY,在加添加映射和分发时使用相同的KEY
*
* @param url
* @param method
* @return url|GET
*/
private String wrapperKey(String url, String method) {
return url.toLowerCase() + "|" + method.toUpperCase();
}
复制代码
在web.xml
中配置前端控制器处理全部请求, 同时指定配置文件路径.
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>com.atd681.xc.ssm.framework.DispatcherServlet</servlet-class>
<init-param>
<param-name>configLocation</param-name>
<param-value>/com/atd681/xc/ssm/framework/mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
复制代码
至此, 项目启动时会加载前端控制器, 加载时会将全部处理的方法和处理URL一一映射后保存.
J2EE中规定, Servlet处理请求时会执行service方法. 所以分发的逻辑须要写到service方法中. 在收到请求时根据请求的URL从urlMapping
中找到对应的方法, 经过JAVA反射动态调用方法便可.
一般, 项目中的控制器会调用业务逻辑层及DAO或其余接口, 调用过程当中不免会出现未知的错误异常, 若是程序中没有处理异常信息, 那么这些异常将会返回至前端控制器中, 所以须要捕获这些异常, 便于用户统一处理. 也叫作全局异常处理机制.
定义一个doService方法, 在该方法中执行分发的逻辑. 在service方法中调用doService执行分发并捕获其抛出的全部异常便可实现对异常的统一处理.
/**
* 处理全部请求
*/
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doService(req, resp);
}
catch (Exception e) {
// 能够在这里捕获异常进行全局处理
// 模拟Spring全局异常处理
e.printStackTrace();
}
}
复制代码
请求分发在doService方法中.
/**
* 根据请求URL分发至控制器
*/
private void doService(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 获取当前请求对应的方法名称
// urlMappings保存每一个URL对应的处理方法, 详见init方法
Method method = urlMappings.get(getMappingKey(req));
// 未找到方法处理时跳转至全局错误页
// 此处忽略该过程直接跑出异常
if (method == null) {
throw new RuntimeException("没有找处处理该请求的方法");
}
// 实例化控制器
Object classInstance = method.getDeclaringClass().newInstance();
// 经过Java反射调用控制器方法
method.invoke(classInstance, new Object[] {});
}
复制代码
HTTP Method
(GET&POST)做为KEY从urlMapping
中找到对应的处理方法. 生成KEY的规则须要和设置urlMapping
时生成的KEY的规则保持一致(统一调用wrapperKey
方法).urlMapping
的URL不含有项目名称). 此时须要使用去除项目名称后的URL./**
* 根据Request取得访问地址对应的处理方法KEY
*
* <pre>
* 例: 请求路径/list, get请求. 对应的key为"/list|get"
* 若是请求路径中含有项目名称,去掉项目名称, 例请求为:/demo/list,转换为/list
* </pre>
*
* @param req
* @return
*/
private String getMappingKey(HttpServletRequest req) {
String httpMethod = req.getMethod().toUpperCase(); // HTTP Method(GET&POST)
String httpUrl = req.getRequestURI().toLowerCase(); // 请求的URL
// 因为WEB服务器配置不一样, 有些项目访问时须要在URL中加入项目名称
// 若是访问的URL中含有项目名称,将项目名称从URL中去除
if (httpUrl.startsWith(req.getContextPath())) {
httpUrl = httpUrl.replaceFirst(req.getContextPath(), "");
}
// 生成KEY的规则应和加载控制器时生成KEY的规则相同
return wrapperKey(httpUrl, httpMethod);
}
复制代码
至此, 咱们已经实现了前端控制器的分发机制, 当有请求到达时前端控制器会将请求分发至对应的控制器方法处理.
上例的控制器方法中没有任何参数, 不少场景下控制器方法中须要参数(Request, Response, Model及请求参数等), SpringMVC提供了灵活的参数绑定机制.
SpringMVC能够将请求中传递的参数绑定至控制器对应方法的参数中, 参数能够是基本数据类型, 也能够是自定义的Javabean. 如下面的URL为例, 请求时携带了3个参数.
http://localhost/list?userName=zhangsan&age=30&gender=M
控制器中使用3个参数来接收, 而且参数的位置能够任意
@RequestMapping("/list")
public String list(String userName, Integer age, String gender) {
}
复制代码
@RequestMapping("/list")
public String list(Integer age, String gender, String userName) {
}
复制代码
当参数较多时能够定义一个Javabean来接收
public class User {
private String userName;
private Integer age;
private String gender;
// Getter&Setter
}
复制代码
@RequestMapping("/list")
public String list(User user) {
}
复制代码
若是你须要Request对象, 只须要在控制器方法的参数中添加便可. 而且参数的顺序仍然没有限制.
@RequestMapping("/list")
public String list(HttpServletRequest req, User user) {
}
复制代码
@RequestMapping("/list")
public String list(User user, HttpServletRequest req) {
}
复制代码
下面分析一下SpringMVC如何作到如此灵活的参数绑定. 控制器方法中的参数能够分为几类:
Request
, Response
, Session
等.前端控制器在请求分发时已经能够获取到对应的控制器方法, 一样能够获取到方法的各个参数的类型. 每一个参数都根据其类型, 找到对应的对象赋值便可. 咱们须要根据参数类型判断参数属于哪一类:
基于上面的分析来实现对控制器方法的参数进行动态绑定.
// 自定义的ModelMap, 保存在此的数据便于在视图中使用
Map<String, Object> model = new HashMap<String, Object>();
// 处理请求的控制器方法参数类型数组
Class<?>[] classes = method.getParameterTypes();
// JAVA反射调用方法时须要传入参数的实例化对象数组
Object[] methodParams = new Object[classes.length];
// 遍历控制器方法的某个参数, 根据参数类型设置相应参数或其实例
// 控制器方法的参数位置变化时, 此处设置的参数的实例化对象数组位置也随之变化
for (int i = 0; i < classes.length; i++) {
Class<?> paramClass = classes[i];
if (paramClass.isAssignableFrom(HttpServletRequest.class)) {
methodParams[i] = req; // 将J2EE的Request对象设置到参数
}
else if (paramClass.isAssignableFrom(HttpSession.class)) {
methodParams[i] = req.getSession(); // 将J2EE的会话对象设置到参数
}
else if (paramClass.isAssignableFrom(Map.class)) {
methodParams[i] = model; // 将自定义保存数据的Map设置到参数
}
else {
// 其他的类型的参数为接收请求参数, 实例化该参数并将设置请求数据
methodParams[i] = wrapperBean(req, paramClass);
}
}
复制代码
若是请求中的参数名称和Javabean的属性名称一致时, 经过JAVA反射机制将该参数的数据设置到Javabean的属性中.
/**
* 将请求中的参数分别设置到Javabean对应的属性中
*/
@SuppressWarnings({ "unchecked" })
private <T> T wrapperBean(HttpServletRequest req, Class<?> bean) {
T beanInstance = null;
// 实例化处理方法中从参数bean
try {
beanInstance = (T) bean.newInstance();
}
catch (Exception e) {
throw new RuntimeException("请求参数映射出现错误", e);
}
// 请求中全部参数
Set<String> keySet = req.getParameterMap().keySet();
// 遍历请求中的参数将值设置到Javabean对应的属性中
for (String reqParam : keySet) {
try {
Class<?> fieldType = bean.getDeclaredField(reqParam).getType(); // Bean中参数类型
Object fieldValue = getRequestValue(req, reqParam, fieldType); // Bean中参数在请求中的值
String fieldSetter = "set" + reqParam.substring(0, 1).toUpperCase() + reqParam.substring(1); // Bean中参数的set方法
// 使用属性的Setter方法将请求中的值设置到属性中
bean.getMethod(fieldSetter, fieldType).invoke(beanInstance, fieldValue);
}
catch (Exception e) {
// BEAN中没有请求中对应的参数属性时继续下一个参数处理
// JAVA反射未找到类的属性时会抛出异常终止循环
}
}
return beanInstance;
}
复制代码
请求中取出的参数数据都是字符串类型的, 须要转换成Javabean相应属性的类型.
/**
* 将request属性值转换为对应JavaBean属性类型
*/
private Object getRequestValue(HttpServletRequest req, String name, Class<?> type) {
String value = req.getParameterValues(name)[0];
if (Integer.class.isAssignableFrom(type)) {
return Integer.valueOf(value);
}
else if (Long.class.isAssignableFrom(type)) {
return Long.valueOf(value);
}
else if (BigDecimal.class.isAssignableFrom(type)) {
return BigDecimal.valueOf(Long.valueOf(value));
}
else if (Date.class.isAssignableFrom(type)) {
try {
return new SimpleDateFormat().parse(value);
}
catch (ParseException e) {
throw new RuntimeException("参数[name]格式不正确");
}
}
return value;
}
复制代码
前端控制器编写完成, 运行项目:
/list
: 执行UserController.userList
/detail
: 执行UserController.userDetail
list?userName=zhangsan&age=30&gender=M
, 会将请求中的三个参数设置到UserController.userList
方法的参数中.前端控制器的核心思想在于分发机制与参数绑定. 面向开发者屏蔽了J2EE的冗余代码, 提高了开发效率. 使得开发者能够更专一于业务开发. 在实现前端控制器的过程当中大量运用了JAVA反射机制来实现动态处理. 对JAVA反射不太熟悉的开发者须要巩固一下相关知识点.
下一篇将分析SpringMVC基于策略模式的视图解析机制.