在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基於策略模式的視圖解析機制.