仿照 Spring 源碼,手寫一個自定義 Spring MVC 框架

前言

為了更好地理解 Spring MVC ,本文我們來仿寫一個 Spring MVC 框架,用到的技術比較簡單,只需要 XML 解析+反射就可以完成,不需要 JDK 動態代理。

自己手寫框架的前提是必須理解框架的底層原理和運行機制,所以我們還是先來回顧一下 Spring MVC 的實現原理。《案例上手 Spring 全家桶》

Spring MVC 實現原理

核心組件

  1. DispatcherServlet:前端控制器,負責調度其他組件的執行,可降低不同組件之間的耦合性,是整個 Spring MVC 的核心模塊。

  2. Handler:處理器,完成具體業務邏輯,相當于 Servlet 或 Action。

  3. HandlerMapping:DispatcherServlet 是通過 HandlerMapping 將請求映射到不同的 Handler。

  4. HandlerInterceptor:處理器攔截器,是一個接口,如果我們需要做一些攔截處理,可以來實現這個接口。

  5. HandlerExecutionChain:處理器執行鏈,包括兩部分內容:Handler 和 HandlerInterceptor(系統會有一個默認的 HandlerInterceptor,如果需要額外攔截處理,可以添加攔截器設置)。

  6. HandlerAdapter:處理器適配器,Handler 執行業務方法之前,需要進行一系列的操作包括表單數據的驗證,數據類型的轉換,將表單數據封裝到 POJO 等等,這一系列的操作,都是由 HandlerAdapter 來完成,DispatcherServlet 通過 HandlerAdapter 執行不同的 Handler。

  7. ModelAndView:裝載了模型數據和視圖信息,作為 Handler 的處理結果,返回給 DispatcherServlet。

  8. ViewResolver:視圖解析器,DispatcherServlet 通過它將邏輯視圖解析成物理視圖,最終將渲染結果響應給客戶端。

以上就是 Spring MVC 的核心組件。那么這些組件之間是如何進行交互的呢?

工作流程

  1. 客戶端請求被 DispatcherServlet(前端控制器)接收。

  2. 根據 Handler Mapping映射到 Handler。

  3. 生成 Handler 和 HandlerInterceptor(如果有則生成)。

  4. Handler 和 HandlerInterceptor 以 HandlerExecutionChain 的形式一并返回給 DispatcherServlet。

  5. DispatcherServlet 通過 HandlerAdapter 調用 Handler 的方法做業務邏輯處理。

  6. 返回一個 ModelAndView 對象給 DispatcherServlet。

  7. DispatcherServlet 將獲取的 ModelAndView 對象傳給 ViewResolver 視圖解析器,將邏輯視圖解析成物理視圖 View。

  8. ViewResolver 返回一個 View 給 DispatcherServlet。

  9. DispatcherServlet 根據 View 進行視圖渲染(將模型數據填充到視圖中)。

  10. DispatcherServlet 將渲染后的視圖響應給客戶端。

enter image description here

通過以上的分析,大致可以將 Spring MVC 流程理解如下:

首先需要一個前置控制器 DispatcherServlet,作為整個流程的核心,由它去調用其他組件,共同完成業務。

主要組件有兩個:

一是 Controller,調用其業務方法 Method,執行業務邏輯。

二是 ViewResolver 視圖解析器,將業務方法的返回值解析為物理視圖+模型數據,返回客戶端。

我們自己寫框架就按照這個思路來。

初始化工作

  • 根據 Spring IoC 容器的特性,需要將參與業務的對象全部創建并保存到容器中,供流程調用。首先需要創建 Controller 對象,HTTP 請求是通過注解找到對應的 Controller 對象,所以我們需要將所有的 Controller 與其注解建立關聯,很顯然,使用 key-value 結構的 Map 集合來保存最合適不過了,這樣就模擬了 IoC 容器。
  • Controller 的 Method 也是通過注解與 HTTP 請求映射的,同樣的,我們需要將所有的 Method 與其注解建立關聯, HTTP 直接通過注解的值找到對應的 Method,這里也用 Map 集合保存。
  • 實例化視圖解析器。

初始化工作完成,接下來處理 HTTP 請求,業務流程如下:

  1. DispatcherServlet 接收請求,通過映射從 IoC 容器中獲取對應的 Controller 對象。

  2. 根據映射獲取 Controller 對象對應的 Method。

  3. 調用 Method,獲取返回值。

  4. 將返回值傳給視圖解析器,返回物理視圖。

  5. 完成頁面跳轉。

思路捋清楚了,接下來開始寫代碼,我們需要創建下面這四個類:

  1. MyDispatcherServlet:模擬 DispatcherServlet。

  2. MyController:模擬 Controller 注解。

  3. MyRequestMapping:模擬 RequestMapping 注解。

  4. 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 頁面跳轉。

enter image description here
代碼實現:

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、測試。

enter image description here
enter image description here

跳轉 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 技術交流社群;

《案例上手 Spring 全家桶》

02-課程二級頁頭圖.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 16. Web MVC 框架 16.1 Spring Web MVC 框架介紹 Spring Web 模型-視圖-...
    此魚不得水閱讀 1,073評論 0 4
  • Spring MVC一、什么是 Spring MVCSpring MVC 屬于 SpringFrameWork 的...
    任任任任師艷閱讀 3,406評論 0 32
  • 1.Spring背景 1.1.Spring四大原則: 使用POJO進行輕量級和最侵入式開發; 通過依賴注入和基于借...
    嗷大彬彬閱讀 806評論 0 2
  • 前言 對于Spring MVC項目搭建相信大家按照網上教程來做基本都會,但更多時候我們應該多問幾個為什么,多思考實...
    九風萍舟閱讀 2,776評論 0 12
  • SpringMVC介紹 Spring web mvc 和Struts2都屬于表現層的框架,它是Spring框架的一...
    day_Sunny閱讀 764評論 0 0