手寫迷你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的核心功能,以及控制反轉,依賴控制等名詞在你腦海中不再只是概念,而是一行行清晰的代碼了。

本文參考自慕課網同名課程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM