前言
為了更好地理解 Spring MVC ,本文我們來仿寫一個 Spring MVC 框架,用到的技術比較簡單,只需要 XML 解析+反射就可以完成,不需要 JDK 動態代理。
自己手寫框架的前提是必須理解框架的底層原理和運行機制,所以我們還是先來回顧一下 Spring MVC 的實現原理。《案例上手 Spring 全家桶》
Spring MVC 實現原理
核心組件
DispatcherServlet:前端控制器,負責調度其他組件的執行,可降低不同組件之間的耦合性,是整個 Spring MVC 的核心模塊。
Handler:處理器,完成具體業務邏輯,相當于 Servlet 或 Action。
HandlerMapping:DispatcherServlet 是通過 HandlerMapping 將請求映射到不同的 Handler。
HandlerInterceptor:處理器攔截器,是一個接口,如果我們需要做一些攔截處理,可以來實現這個接口。
HandlerExecutionChain:處理器執行鏈,包括兩部分內容:Handler 和 HandlerInterceptor(系統會有一個默認的 HandlerInterceptor,如果需要額外攔截處理,可以添加攔截器設置)。
HandlerAdapter:處理器適配器,Handler 執行業務方法之前,需要進行一系列的操作包括表單數據的驗證,數據類型的轉換,將表單數據封裝到 POJO 等等,這一系列的操作,都是由 HandlerAdapter 來完成,DispatcherServlet 通過 HandlerAdapter 執行不同的 Handler。
ModelAndView:裝載了模型數據和視圖信息,作為 Handler 的處理結果,返回給 DispatcherServlet。
ViewResolver:視圖解析器,DispatcherServlet 通過它將邏輯視圖解析成物理視圖,最終將渲染結果響應給客戶端。
以上就是 Spring MVC 的核心組件。那么這些組件之間是如何進行交互的呢?
工作流程
客戶端請求被 DispatcherServlet(前端控制器)接收。
根據 Handler Mapping映射到 Handler。
生成 Handler 和 HandlerInterceptor(如果有則生成)。
Handler 和 HandlerInterceptor 以 HandlerExecutionChain 的形式一并返回給 DispatcherServlet。
DispatcherServlet 通過 HandlerAdapter 調用 Handler 的方法做業務邏輯處理。
返回一個 ModelAndView 對象給 DispatcherServlet。
DispatcherServlet 將獲取的 ModelAndView 對象傳給 ViewResolver 視圖解析器,將邏輯視圖解析成物理視圖 View。
ViewResolver 返回一個 View 給 DispatcherServlet。
DispatcherServlet 根據 View 進行視圖渲染(將模型數據填充到視圖中)。
DispatcherServlet 將渲染后的視圖響應給客戶端。
通過以上的分析,大致可以將 Spring MVC 流程理解如下:
首先需要一個前置控制器 DispatcherServlet,作為整個流程的核心,由它去調用其他組件,共同完成業務。
主要組件有兩個:
一是 Controller,調用其業務方法 Method,執行業務邏輯。
二是 ViewResolver 視圖解析器,將業務方法的返回值解析為物理視圖+模型數據,返回客戶端。
我們自己寫框架就按照這個思路來。
初始化工作
- 根據 Spring IoC 容器的特性,需要將參與業務的對象全部創建并保存到容器中,供流程調用。首先需要創建 Controller 對象,HTTP 請求是通過注解找到對應的 Controller 對象,所以我們需要將所有的 Controller 與其注解建立關聯,很顯然,使用 key-value 結構的 Map 集合來保存最合適不過了,這樣就模擬了 IoC 容器。
- Controller 的 Method 也是通過注解與 HTTP 請求映射的,同樣的,我們需要將所有的 Method 與其注解建立關聯, HTTP 直接通過注解的值找到對應的 Method,這里也用 Map 集合保存。
- 實例化視圖解析器。
初始化工作完成,接下來處理 HTTP 請求,業務流程如下:
DispatcherServlet 接收請求,通過映射從 IoC 容器中獲取對應的 Controller 對象。
根據映射獲取 Controller 對象對應的 Method。
調用 Method,獲取返回值。
將返回值傳給視圖解析器,返回物理視圖。
完成頁面跳轉。
思路捋清楚了,接下來開始寫代碼,我們需要創建下面這四個類:
MyDispatcherServlet:模擬 DispatcherServlet。
MyController:模擬 Controller 注解。
MyRequestMapping:模擬 RequestMapping 注解。
MyViewResolver:模擬 ViewResolver 視圖解析器。
首先創建 MyDispatcherServlet,init 方法完成初始化:
1、將 Controller 與注解進行關聯,保存到 iocContainer 中,哪些 Controller 是需要添加到 iocContainer 中的?
必須同時滿足兩點:
- springmvc.xml 中配置掃描的類。
- 類定義處添加了注解。
注意這兩點必須同時滿足。
代碼思路:
- 解析 springmvc.xml。
- 獲取 component-scan 標簽配置的包下的所有類。
- 判斷若這些類添加了 @MyController 注解,則創建實例對象,并且保存到 iocContainer。
- @MyRequestMapping 的值為鍵,Controller 對象為值。
2、將 Controller 中的 Method 與注解進行關聯,保存到 handlerMapping 中。
代碼思路:
- 遍歷 iocContainer 中的 Controller 實例對象。
- 遍歷每一個 Controlle r對象的 Method。
- 判斷 Method 是否添加了 @MyRequestMapping 注解,若添加,則進行映射并保存。
- 保存到 handlerMapping 中,@MyRequestMapping 的值為鍵,Method 為值。
3、實例化 ViewResolver。
代碼思路:
- 解析 springmvc.xml。
- 根據 bean 標簽的 class 屬性獲取需要實例化的 MyViewResolver。
- 通過反射創建實例化對象,同時獲取 prefix 和 suffix 屬性,以及 setter 方法。
- 通過反射調用 setter 方法給屬性賦值,完成 MyViewResolver 的實例化。
doPost 方法處理 HTTP 請求的流程:
1、解析 HTTP,分別得到 Controller 和 Method 對應的 uri。
2、通過 uri 分別在 iocContainer 和 handlerMapping 中獲取對應的 Controller 以及 Method。
3、通過反射調用 Method,執行業務方法,獲取結果。
4、將結果傳給 MyViewResolver 進行解析,返回真正的物理視圖(JSP 頁面)。
5、完成 JSP 頁面跳轉。
代碼實現:
1、創建 MyController 注解,作用目標為類。
/**
* 自定義Controller注解
* @author southwind
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
String value() default "";
}
2、創建 MyRequestMapping 注解,作用目標為類和方法。
/**
* 自定義RequestMapping注解
* @author southwind
*
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestMapping {
String value() default "";
}
3、創建 MyDispatcherServlet,核心控制器,init 完成初始化工作,doPost 處理 HTTP 請求。
/**
* DispatcherServlet
* @author southwind
*
*/
public class MyDispatcherServlet extends HttpServlet{
//模擬IOC容器,保存Controller實例對象
private Map<String,Object> iocContainer = new HashMap<String,Object>();
//保存handler映射
private Map<String,Method> handlerMapping = new HashMap<String,Method>();
//自定視圖解析器
private MyViewResolver myViewResolver;
@Override
public void init(ServletConfig config) throws ServletException {
// TODO Auto-generated method stub
//掃描Controller,創建實例對象,并存入iocContainer
scanController(config);
//初始化handler映射
initHandlerMapping();
//加載視圖解析器
loadViewResolver(config);
}
/**
* 掃描Controller
* @param config
*/
public void scanController(ServletConfig config){
SAXReader reader = new SAXReader();
try {
//解析springmvc.xml
String path = config.getServletContext().getRealPath("")+"\\WEB-INF\\classes\\"+config.getInitParameter("contextConfigLocation");
Document document = reader.read(path);
Element root = document.getRootElement();
Iterator iter = root.elementIterator();
while(iter.hasNext()){
Element ele = (Element) iter.next();
if(ele.getName().equals("component-scan")){
String packageName = ele.attributeValue("base-package");
//獲取base-package包下的所有類名
List<String> list = getClassNames(packageName);
for(String str:list){
Class clazz = Class.forName(str);
//判斷是否有MyController注解
if(clazz.isAnnotationPresent(MyController.class)){
//獲取Controller中MyRequestMapping注解的value
MyRequestMapping annotation = (MyRequestMapping) clazz.getAnnotation(MyRequestMapping.class);
String value = annotation.value().substring(1);
//Controller實例對象存入iocContainer
iocContainer.put(value, clazz.newInstance());
}
}
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 獲取包下的所有類名
* @param packageName
* @return
*/
public List<String> getClassNames(String packageName){
List<String> classNameList = new ArrayList<String>();
String packagePath = packageName.replace(".", "/");
ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL url = loader.getResource(packagePath);
if(url != null){
File file = new File(url.getPath());
File[] childFiles = file.listFiles();
for(File childFile : childFiles){
String className = packageName+"."+childFile.getName().replace(".class", "");
classNameList.add(className);
}
}
return classNameList;
}
/**
* 初始化handler映射
*/
public void initHandlerMapping(){
for(String str:iocContainer.keySet()){
Class clazz = iocContainer.get(str).getClass();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
//判斷方式是否添加MyRequestMapping注解
if(method.isAnnotationPresent(MyRequestMapping.class)){
//獲取Method中MyRequestMapping注解的value
MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);
String value = annotation.value().substring(1);
//method存入methodMapping
handlerMapping.put(value, method);
}
}
}
}
/**
* 加載自定義視圖解析器
* @param config
*/
public void loadViewResolver(ServletConfig config){
SAXReader reader = new SAXReader();
try {
//解析springmvc.xml
String path = config.getServletContext().getRealPath("")+"\\WEB-INF\\classes\\"+config.getInitParameter("contextConfigLocation");
Document document = reader.read(path);
Element root = document.getRootElement();
Iterator iter = root.elementIterator();
while(iter.hasNext()){
Element ele = (Element) iter.next();
if(ele.getName().equals("bean")){
String className = ele.attributeValue("class");
Class clazz = Class.forName(className);
Object obj = clazz.newInstance();
//獲取setter方法
Method prefixMethod = clazz.getMethod("setPrefix", String.class);
Method suffixMethod = clazz.getMethod("setSuffix", String.class);
Iterator beanIter = ele.elementIterator();
//獲取property值
Map<String,String> propertyMap = new HashMap<String,String>();
while(beanIter.hasNext()){
Element beanEle = (Element) beanIter.next();
String name = beanEle.attributeValue("name");
String value = beanEle.attributeValue("value");
propertyMap.put(name, value);
}
for(String str:propertyMap.keySet()){
//反射機制調用setter方法,完成賦值。
if(str.equals("prefix")){
prefixMethod.invoke(obj, propertyMap.get(str));
}
if(str.equals("suffix")){
suffixMethod.invoke(obj, propertyMap.get(str));
}
}
myViewResolver = (MyViewResolver) obj;
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// TODO Auto-generated method stub
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// TODO Auto-generated method stub
//獲取請求
String handlerUri = req.getRequestURI().split("/")[2];
//獲取Controller實例
Object obj = iocContainer.get(handlerUri);
String methodUri = req.getRequestURI().split("/")[3];
//獲取業務方法
Method method = handlerMapping.get(methodUri);
try {
//反射機制調用業務方法
String value = (String) method.invoke(obj);
//視圖解析器將邏輯視圖轉換為物理視圖
String result = myViewResolver.jspMapping(value);
//頁面跳轉
req.getRequestDispatcher(result).forward(req, resp);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
4、創建視圖解析器 MyViewResolver。
/**
* 自定義視圖解析器
* @author southwind
*
*/
public class MyViewResolver {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public String jspMapping(String value){
return this.prefix+value+this.suffix;
}
}
5、創建 TestController,處理業務請求。
@MyController
@MyRequestMapping(value = "/testController")
public class TestController {
@MyRequestMapping(value = "/test")
public String test(){
System.out.println("執行test相關業務");
return "index";
}
}
6、測試。
跳轉 index.jsp,同時控制臺打印業務日志,訪問成功。
總結
本節課我們講解了 Spring MVC 的底層原理,同時仿照 Spring MVC 手寫了一個簡單的框架,目的不是讓大家自己去寫框架,在實際開發中我們也不需要自己寫框架,直接使用成熟的第三方框架即可。
手寫框架的目的在于讓大家更透徹地理解 Spring MVC 的底層流程,學習優秀框架的編程思想,理解了原理,才能更熟練地應用。
源碼:
https://github.com/southwind9801/SpringMVCImitate.git
本文內容節選自達人課《案例上手 Spring 全家桶》,從 Spring 到 Spring Boot 再到 Spring Cloud,結合 3 個項目實戰案例,真正做到學以致用。
Spring 技術零基礎輕松入門;
68 講更全面地覆蓋 Spring 全家桶核心模塊;
100+ 段代碼示例,理解 Spring 全家桶要領;
3 大項目實戰,掌握 Spring 全家桶實際應用;
精選 70 道 Spring 高頻面試題檢驗學習成果;
免費贈送 16+ 小時的 Spring 實戰視頻;
進入專業的 Spring 技術交流社群;