1. 程式人生 > >手寫迷你SpringMVC框架

手寫迷你SpringMVC框架

前言

學習如何使用Spring,SpringMVC是很快的,但是在往後使用的過程中難免會想探究一下框架背後的原理是什麼,本文將通過講解如何手寫一個簡單版的springMVC框架,直接從程式碼上看框架中請求分發,控制反轉和依賴注入是如何實現的。

建議配合示例原始碼閱讀,github地址如下:

https://github.com/liuyj24/mini-spring

專案搭建

專案搭建可以參考github中的專案,先選好jar包管理工具,Maven和Gradle都行,本專案使用的是Gradle。

然後在專案下建兩個模組,一個是framework,用於編寫框架;另外一個是test,用於應用並測試框架(注意test模組要依賴framework模組)。

接著在framework模組下按照spring建立好beans,core,context,web等模組對應的包,完成後便可以進入框架的編寫了。

請求分發

在講請求分發之前先來梳理一下整個web模型:

  1. 首先使用者在客戶端傳送一個請求到伺服器,經作業系統的TCP/IP棧解析後會交到在某個埠監聽的web伺服器。
  2. web伺服器程式監聽到請求後便會把請求分發給對應的程式進行處理。比如Tomcat就會將請求分發給對應的java程式(servlet)進行處理,web伺服器本身是不進行請求處理的。

本專案的web伺服器選擇Tomcat,而且為了能讓專案直接跑起來,選擇了在專案中內嵌Tomcat,這樣框架在做測試的時候就能像spring boot一樣一鍵啟動,方便測試。

Servlet

既然選擇了使用Java編寫服務端程式,那就不得不提到Servlet介面了。為了規範伺服器與Java程式之間的通訊方式,Java官方制定了Servlet規範,服務端的Java應用程式必須實現該介面,把Java作為處理語言的伺服器也必須要根據Servlet規範進行對接。

在還沒有spring之前,人們是這麼開發web程式的:一個業務邏輯對應一個Servlet,所以一個大專案中會有多個Servlet,這大量的Servlet會被配置到一個叫web.xml的配置檔案中,當伺服器執行的時候,tomcat會根據請求的uri到web.xml檔案中尋找對應的Servlet業務類處理請求。

但是你想,每來一個請求就建立一個Servlet,而且一個Servlet實現類中我們通常只重寫一個service方法,另外四個方法都只是給個空實現,這太浪費資源了。而且編起程式來建立很多Servlet還很難管理。能不能改進一下?

Spring的DispatcherServlet

方法確實有:

從上圖可以看到,我們原來是經過web伺服器把請求分發到不同的Servlet;我們可以換個思路,讓web伺服器把請求都發送到一個Servlet,再由這個Servlet把請求按照uri分發給不同的方法進行處理。

這樣一來,不管收到什麼請求,web伺服器都會分發到同一個Servlet(DispatcherServlet),避免了多個Servlet所帶來的問題,有以下好處:

  1. 把分發請求這一步從web伺服器移動到框架內,這樣更容易控制,也方便擴充套件。
  2. 可以把同一個業務的處理方法集中到同一個類裡,把這種類起名為controller,一個controller中有多個處理方法,這樣配置分散不雜亂。
  3. 配置uri對映路徑的時候可以不使用配置檔案,直接在處理方法上用註解配置即可,解決了配置集中,大而雜的問題。

實操

建議配合文章開頭給出的原始碼進行參考

  1. 首先在web.mvc包中建立三個註解:Controller,RequestMapping,RequestParam,有了註解我們才能在框架啟動時動態獲得配置資訊。
  2. 由於處理方法都是被註解的,要想解析被註解的類,首先得獲得專案中相關的所有類,對應是原始碼中core包下的ClassScanner類
public class ClassScanner {
    public static List<Class<?>> scanClass(String packageName) throws IOException, ClassNotFoundException {
        //用於儲存結果的容器
        List<Class<?>> classList = new ArrayList<>();
        //把檔名改為檔案路徑
        String path = packageName.replace(".", "/");
        //獲取預設的類載入器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        //通過檔案路徑獲取該資料夾下所有資源的URL
        Enumeration<URL> resources = classLoader.getResources(path);

        int index = 0;//測試

        while(resources.hasMoreElements()){
            //拿到下一個資源
            URL resource = resources.nextElement();
            //先判斷是否是jar包,因為預設.class檔案會被打包為jar包
            if(resource.getProtocol().contains("jar")){
                //把URL強轉為jar包連結
                JarURLConnection jarURLConnection = (JarURLConnection)resource.openConnection();
                //根據jar包獲取jar包的路徑名
                String jarFilePath = jarURLConnection.getJarFile().getName();
                //把jar包下所有的類新增的儲存結果的容器中
                classList.addAll(getClassFromJar(jarFilePath, path));
            }else{//也有可能不是jar檔案,先放下
                //todo
            }
        }
        return classList;
    }

    /**
     * 獲取jar包中所有路徑符合的類檔案
     * @param jarFilePath
     * @param path
     * @return
     */
    private static List<Class<?>> getClassFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();//儲存結果的集合
        JarFile jarFile = new JarFile(jarFilePath);//建立對應jar包的控制代碼
        Enumeration<JarEntry> jarEntries = jarFile.entries();//拿到jar包中所有的檔案
        while(jarEntries.hasMoreElements()){
            JarEntry jarEntry = jarEntries.nextElement();//拿到一個檔案
            String entryName = jarEntry.getName();//拿到檔名,大概是這樣:com/shenghao/test/Test.class
            if (entryName.startsWith(path) && entryName.endsWith(".class")){//判斷是否是類檔案
                String classFullName = entryName.replace("/", ".")
                        .substring(0, entryName.length() - 6);
                classes.add(Class.forName(classFullName));
            }
        }
        return classes;
    }
}
  1. 然後在handler包建立MappingHandler類,在將來框架執行的過程中,一個MappingHandler就對應一個業務邏輯,比如說增加一個使用者。所以一個MappingHandler中要有“請求uri,處理方法,方法的引數,方法所處的類”這四個欄位,其中請求uri用於匹配請求uri,後面三個引數用於執行時通過反射呼叫該處理方法
public class MappingHandler {

    private String uri;
    private Method method;
    private Class<?> controller;
    private String[] args;

    MappingHandler(String uri, Method method, Class<?> cls, String[] args){
        this.uri = uri;
        this.method = method;
        this.controller = cls;
        this.args = args;
    }

    public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
        //拿到請求的uri
        String requestUri = ((HttpServletRequest)req).getRequestURI();
        if(!uri.equals(requestUri)){//如果和自身uri不同就跳過
            return false;
        }
        Object[] parameters = new Object[args.length];
        for(int i = 0; i < args.length; i++){
            parameters[i] = req.getParameter(args[i]);
        }
        Object ctl = BeanFactory.getBean(controller);
        Object response = method.invoke(ctl, parameters);
        res.getWriter().println(response.toString());
        return true;
    }
}
  1. 接下來在handler包建立HandlerManager類,這個類擁有一個靜態的MappingHandler集合,這個類的作用是從獲得的所有類中,找到被@controller註解的類,並將controller類中每個被@ReqeustMapping註解的方法封裝成一個MappingHandler,然後把MappingHandler放入靜態集合中
public class HandlerManager {

    public static List<MappingHandler> mappingHandlerList = new ArrayList<>();

    /**
     * 處理類檔案集合,挑出MappingHandler
     * @param classList
     */
    public static void resolveMappingHandler(List<Class<?>> classList){
        for(Class<?> cls : classList){
            if(cls.isAnnotationPresent(Controller.class)){//MappingHandler會在controller裡面
                parseHandlerFromController(cls);//繼續從controller中分離出一個個MappingHandler
            }
        }
    }

    private static void parseHandlerFromController(Class<?> cls) {
        //先獲取該controller中所有的方法
        Method[] methods = cls.getDeclaredMethods();
        //從中挑選出被RequestMapping註解的方法進行封裝
        for(Method method : methods){
            if(!method.isAnnotationPresent(RequestMapping.class)){
                continue;
            }
            String uri = method.getDeclaredAnnotation(RequestMapping.class).value();//拿到RequestMapping定義的uri
            List<String> paramNameList = new ArrayList<>();//儲存方法引數的集合
            for(Parameter parameter : method.getParameters()){
                if(parameter.isAnnotationPresent(RequestParam.class)){//把有被RequestParam註解的引數新增入集合
                    paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
                }
            }
            String[] params = paramNameList.toArray(new String[paramNameList.size()]);//把引數集合轉為陣列,用於反射
            MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);//反射生成MappingHandler
            mappingHandlerList.add(mappingHandler);//把mappingHandler裝入集合中
        }
    }
}
  1. 完成上面四步後,我們在框架啟動的時候就獲得了一個MappingHandler集合,當請求來到時,我們只要根據請求的uri從集合中找到對應的MappingHandler,就可以通過反射呼叫對應的處理方法,到此也就完成了框架請求分發的功能。

控制反轉和依賴注入

完成了請求分發功能後,進一步想這麼一個問題:

假設現在處理一個請求需要建立A,B,C三個物件,而
A 有個欄位 D
B 有個欄位 D
C 有個欄位 B

如果按照順序建立ABC的話,
首先要建立一個D,然後建立一個A;
接著先建立一個D,然後建立一個B;
接著先建立一個D,然後建立一個B,才能創建出一個C
總共建立了一個A,兩個B,一個C,三個D。

上述是我們編寫程式的一方建立物件的方式,可以看到由於物件不能被重複引用,導致建立了大量重複物件。

為了解決這個問題,spring提出了bean這麼個概念,你可以把一個bean理解為一個物件,但是他對比普通的物件有如下特點:

  1. 不像普通物件一樣朝生暮死,宣告週期較長
  2. 在整個虛擬機器內可見,不像普通物件只在某個程式碼塊中可見
  3. 維護成本高,以單例形式存在

為了製作出上述的bean,我們得有個bean工廠,bean工廠的原理也很簡單:在框架初始化的時候建立相關的bean(也可以在用到的時候建立),當需要使用bean的時候直接從工廠中拿。也就是我們把建立物件的權力交給框架,這就是控制反轉

有了bean工廠後按順序建立ABC的過程如下:
首先建立一個D,把D放入工廠,然後建立一個A,把A放入工廠;
接著從工廠拿出一個D,建立一個B,把B也放入工廠;
接著從工廠拿出一個B,建立一個C,把C也放入工廠;
總共建立了一個A,一個B,一個C,一個D
達到了物件重複利用的目的

至於創建出一個D,然後把D設定為A的一個欄位這麼個過程,叫做依賴注入

所以控制反轉和依賴注入的概念其實很好理解,控制反轉是一種思想,而依賴注入是控制反轉的一種具體實現。

實操

  1. 首先在bean包下建立@Bean和@AutoWired兩個註解,同樣是用於框架解析類的。
  2. 接著在bean包下建立BeanFactory,BeanFactory要能提供一個根據類獲取例項的功能,這就要求他要有一個靜態的getBean()方法,和一個儲存Bean的對映集合。
  3. 為了初始化Bean,要有一個根據類檔案集合解析出bean的方法。該方法會遍歷集合中所有的類,把有註解的,屬於bean的類提取出來,建立該類的物件並放到靜態集合中。
  4. 在這裡有個有意思的點——按什麼順序建立bean?在本文給出的原始碼中,用了一個迴圈來建立bean,如果該bean沒有依賴其他的bean就直接建立,如果有依賴其他bean就看其他bean有沒被創建出來,如果沒有就跳過當前的bean,如果有就建立當前的bean。
  5. 在迴圈建立bean的過程中可能出現一種bean之間相互依賴的現象,原始碼中暫時對這種現象丟擲異常,沒作處理。
public class BeanFactory {

    //儲存Bean例項的對映集合
    private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();

    /**
     * 根據class型別獲取bean
     * @param cls
     * @return
     */
    public static Object getBean(Class<?> cls){
        return classToBean.get(cls);
    }

    /**
     * 初始化bean工廠
     * @param classList 需要一個.class檔案集合
     * @throws Exception
     */
    public static void initBean(List<Class<?>> classList) throws Exception {
        //先建立一個.class檔案集合的副本
        List<Class<?>> toCreate = new ArrayList<>(classList);
        //迴圈建立bean例項
        while(toCreate.size() != 0){
            int remainSize = toCreate.size();//記錄開始時集合大小,如果一輪結束後大小沒有變證明有相互依賴
            for(int i = 0; i < toCreate.size(); i++){//遍歷建立bean,如果失敗就先跳過,等下一輪再建立
                if(finishCreate(toCreate.get(i))){
                    toCreate.remove(i);
                }
            }
            if(toCreate.size() == remainSize){//有相互依賴的情況先丟擲異常
                throw new Exception("cycle dependency!");
            }
        }
    }

    private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
        //建立的bean例項僅包括Bean和Controller註釋的類
        if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)){
            return true;
        }
        //先建立例項物件
        Object bean = cls.newInstance();
        //看看例項物件是否需要執行依賴注入,注入其他bean
        for(Field field : cls.getDeclaredFields()){
            if(field.isAnnotationPresent(AutoWired.class)){
                Class<?> fieldType = field.getType();
                Object reliantBean = BeanFactory.getBean(fieldType);
                if(reliantBean == null){//如果要注入的bean還未被建立就先跳過
                    return false;
                }
                field.setAccessible(true);
                field.set(bean, reliantBean);
            }
        }
        classToBean.put(cls, bean);
        return true;
    }
}
  1. 有了bean工廠之後,凡是用到bean的地方都能直接通過bean工廠拿了
  2. 最後我們可以寫一個小Demo測試一下自己的框架是否能正確地處理請求完成響應。相信整個迷你框架擼下來,Spring的核心功能,以及控制反轉,依賴控制等名詞在你腦海中不再只是概念,而是一行行清晰的程式碼了。