自己手寫一個SpringMVC框架(簡化)

??Spring框架對于Java后端程序員來說再熟悉不過了会钝,以前只知道它用的反射實現(xiàn)的工三,但了解之后才知道有很多巧妙的設(shè)計在里面俭正。如果不看Spring的源碼掸读,你將會失去一次和大師學(xué)習的機會:它的代碼規(guī)范,設(shè)計思想很值得學(xué)習伸但。我們程序員大部分人都是野路子,不懂什么叫代碼規(guī)范。寫了一個月的代碼荣月,最后還得其他老司機花3天時間重構(gòu)哺窄,相信大部分老司機都很頭疼看新手的代碼萌业。
??廢話不多說奸柬,我們進入今天的正題廓奕,在Web應(yīng)用程序設(shè)計中桌粉,MVC模式已經(jīng)被廣泛使用铃肯。SpringMVC以DispatcherServlet為核心押逼,負責協(xié)調(diào)和組織不同組件以完成請求處理并返回響應(yīng)的工作,實現(xiàn)了MVC模式挑格。想要實現(xiàn)自己的SpringMVC框架恕齐,需要從以下幾點入手:

   一瞬逊、了解SpringMVC運行流程及九大組件

   二确镊、梳理自己的SpringMVC的設(shè)計思路

   三蕾域、實現(xiàn)自己的SpringMVC框架

一旨巷、了解SpringMVC運行流程及九大組件

1采呐、SpringMVC的運行流程

image.png
   ⑴ 用戶發(fā)送請求至前端控制器DispatcherServlet

   ⑵ DispatcherServlet收到請求調(diào)用HandlerMapping處理器映射器斧吐。

   ⑶ 處理器映射器根據(jù)請求url找到具體的處理器煤率,生成處理器對象及處理器攔截器(如果有則生成)一并返回給DispatcherServlet蝶糯。

   ⑷ DispatcherServlet通過HandlerAdapter處理器適配器調(diào)用處理器

   ⑸ 執(zhí)行處理器(Controller裳涛,也叫后端控制器)端三。

   ⑹ Controller執(zhí)行完成返回ModelAndView

   ⑺ HandlerAdapter將controller執(zhí)行結(jié)果ModelAndView返回給DispatcherServlet

   ⑻ DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器

   ⑼ ViewReslover解析后返回具體View

   ⑽ DispatcherServlet對View進行渲染視圖(即將模型數(shù)據(jù)填充至視圖中)郊闯。

   ⑾ DispatcherServlet響應(yīng)用戶团赁。

??從上面可以看出欢摄,DispatcherServlet有接收請求怀挠,響應(yīng)結(jié)果绿淋,轉(zhuǎn)發(fā)等作用。有了DispatcherServlet之后佑菩,可以減少組件之間的耦合度殿漠。

2、SpringMVC的九大組件(ref:【SpringMVC】9大組件概覽

protected void initStrategies(ApplicationContext context) {
    //用于處理上傳請求啊奄。處理方法是將普通的request包裝成MultipartHttpServletRequest,后者可以直接調(diào)用getFile方法獲取File.
    initMultipartResolver(context);
    //SpringMVC主要有兩個地方用到了Locale:一是ViewResolver視圖解析的時候仪吧;二是用到國際化資源或者主題的時候薯鼠。
    initLocaleResolver(context); 
    //用于解析主題出皇。SpringMVC中一個主題對應(yīng)一個properties文件郊艘,里面存放著跟當前主題相關(guān)的所有資源纱注、
    //如圖片狞贱、css樣式等瞎嬉。SpringMVC的主題也支持國際化佑颇, 
    initThemeResolver(context);
    //用來查找Handler的挑胸。
    initHandlerMappings(context);
    //從名字上看茬贵,它就是一個適配器解藻。Servlet需要的處理方法的結(jié)構(gòu)卻是固定的螟左,都是以request和response為參數(shù)的方法胶背。
    //如何讓固定的Servlet處理方法調(diào)用靈活的Handler來進行處理呢钳吟?這就是HandlerAdapter要做的事情
    initHandlerAdapters(context);
    //其它組件都是用來干活的红且。在干活的過程中難免會出現(xiàn)問題暇番,出問題后怎么辦呢壁酬?
    //這就需要有一個專門的角色對異常情況進行處理厨喂,在SpringMVC中就是HandlerExceptionResolver蜕煌。
    initHandlerExceptionResolvers(context);
    //有的Handler處理完后并沒有設(shè)置View也沒有設(shè)置ViewName斜纪,這時就需要從request獲取ViewName了,
    //如何從request中獲取ViewName就是RequestToViewNameTranslator要做的事情了绿贞。
    initRequestToViewNameTranslator(context);
    //ViewResolver用來將String類型的視圖名和Locale解析為View類型的視圖籍铁。
    //View是用來渲染頁面的拒名,也就是將程序返回的參數(shù)填入模板里增显,生成html(也可能是其它類型)文件同云。
    initViewResolvers(context);
    //用來管理FlashMap的堵腹,F(xiàn)lashMap主要用在redirect重定向中傳遞參數(shù)炸站。
    initFlashMapManager(context); 
}

二、梳理SpringMVC的設(shè)計思路

??本文只實現(xiàn)自己的@Controller秸滴、@RequestMapping武契、@RequestParam注解起作用募判,其余SpringMVC功能讀者可以嘗試自己實現(xiàn)荡含。

1、讀取配置

image.png

??從圖中可以看出届垫,SpringMVC本質(zhì)上是一個Servlet,這個 Servlet 繼承自 HttpServlet。FrameworkServlet負責初始化SpringMVC的容器装处,并將Spring容器設(shè)置為父容器误债。因為本文只是實現(xiàn)SpringMVC,對于Spring容器不做過多講解(有興趣同學(xué)可以看看我另一篇文章:向spring大佬低頭--大量源碼流出解析)妄迁。

??為了讀取web.xml中的配置寝蹈,我們用到ServletConfig這個類,它代表當前Servlet在web.xml中的配置信息登淘。通過web.xml中加載我們自己寫的MyDispatcherServlet和讀取配置文件箫老。

2、初始化階段

??在前面我們提到DispatcherServlet的initStrategies方法會初始化9大組件黔州,但是這里將實現(xiàn)一些SpringMVC的最基本的組件而不是全部耍鬓,按順序包括:

  • 加載配置文件
  • 掃描用戶配置包下面所有的類
  • 拿到掃描到的類阔籽,通過反射機制,實例化牲蜀。并且放到ioc容器中(Map的鍵值對 beanName-bean) beanName默認是首字母小寫
  • 初始化HandlerMapping笆制,這里其實就是把url和method對應(yīng)起來放在一個k-v的Map中,在運行階段取出

3、運行階段

??每一次請求將會調(diào)用doGet或doPost方法涣达,所以統(tǒng)一運行階段都放在doDispatch方法里處理在辆,它會根據(jù)url請求去HandlerMapping中匹配到對應(yīng)的Method,然后利用反射機制調(diào)用Controller中的url對應(yīng)的方法度苔,并得到結(jié)果返回开缎。按順序包括以下功能:

  • 異常的攔截
  • 獲取請求傳入的參數(shù)并處理參數(shù)
  • 通過初始化好的handlerMapping中拿出url對應(yīng)的方法名,反射調(diào)用

三林螃、實現(xiàn)自己的SpringMVC框架

??工程文件及目錄:

image.png

首先奕删,新建一個maven項目,在pom.xml中導(dǎo)入以下依賴:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.liugh</groupId>
  <artifactId>liughMVC</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
         <dependency>
           <groupId>javax.servlet</groupId> 
           <artifactId>javax.servlet-api</artifactId> 
           <version>3.0.1</version> 
           <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

接著疗认,我們在WEB-INF下創(chuàng)建一個web.xml完残,如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <servlet>
        <servlet-name>MySpringMVC</servlet-name>
        <servlet-class>com.liugh.servlet.MyDispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>MySpringMVC</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

application.properties文件中只是配置要掃描的包到SpringMVC容器中。

scanPackage=com.liugh.core

創(chuàng)建自己的Controller注解横漏,它只能標注在類上面:

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
    /**
     * 表示給controller注冊別名
     * @return
     */
    String value() default "";

}

RequestMapping注解谨设,可以在類和方法上:

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
    /**
     * 表示訪問該方法的url
     * @return
     */
    String value() default "";

}

RequestParam注解,只能注解在參數(shù)上

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestParam {
    /**
     * 表示參數(shù)的別名,必填
     * @return
     */
    String value();

}

然后創(chuàng)建MyDispatcherServlet這個類缎浇,去繼承HttpServlet扎拣,重寫init方法、doGet素跺、doPost方法二蓝,以及加上我們第二步分析時要實現(xiàn)的功能:

package com.liugh.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.liugh.annotation.MyController;
import com.liugh.annotation.MyRequestMapping;

public class MyDispatcherServlet extends HttpServlet{

    private Properties properties = new Properties();

    private List<String> classNames = new ArrayList<>();

    private Map<String, Object> ioc = new HashMap<>();

    private Map<String, Method> handlerMapping = new  HashMap<>();

    private Map<String, Object> controllerMap  =new HashMap<>();

    @Override
    public void init(ServletConfig config) throws ServletException {

        //1.加載配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));

        //2.初始化所有相關(guān)聯(lián)的類,掃描用戶設(shè)定的包下面所有的類
        doScanner(properties.getProperty("scanPackage"));

        //3.拿到掃描到的類,通過反射機制,實例化,并且放到ioc容器中(k-v  beanName-bean) beanName默認是首字母小寫
        doInstance();

        //4.初始化HandlerMapping(將url和method對應(yīng)上)
        initHandlerMapping();

    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            //處理請求
            doDispatch(req,resp);
        } catch (Exception e) {
            resp.getWriter().write("500!! Server Exception");
        }

    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        if(handlerMapping.isEmpty()){
            return;
        }

        String url =req.getRequestURI();
        String contextPath = req.getContextPath();

        url=url.replace(contextPath, "").replaceAll("/+", "/");

        if(!this.handlerMapping.containsKey(url)){
            resp.getWriter().write("404 NOT FOUND!");
            return;
        }

        Method method =this.handlerMapping.get(url);

        //獲取方法的參數(shù)列表
        Class<?>[] parameterTypes = method.getParameterTypes();

        //獲取請求的參數(shù)
        Map<String, String[]> parameterMap = req.getParameterMap();

        //保存參數(shù)值
        Object [] paramValues= new Object[parameterTypes.length];

        //方法的參數(shù)列表
        for (int i = 0; i<parameterTypes.length; i++){  
            //根據(jù)參數(shù)名稱,做某些處理  
            String requestParam = parameterTypes[i].getSimpleName();  

            if (requestParam.equals("HttpServletRequest")){  
                //參數(shù)類型已明確指厌,這邊強轉(zhuǎn)類型  
                paramValues[i]=req;
                continue;  
            }  
            if (requestParam.equals("HttpServletResponse")){  
                paramValues[i]=resp;
                continue;  
            }
            if(requestParam.equals("String")){
                for (Entry<String, String[]> param : parameterMap.entrySet()) {
                    String value =Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ",");
                    paramValues[i]=value;
                }
            }
        }  
        //利用反射機制來調(diào)用
        try {
            method.invoke(this.controllerMap.get(url), paramValues);//第一個參數(shù)是method所對應(yīng)的實例 在ioc容器中
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void  doLoadConfig(String location){
        //把web.xml中的contextConfigLocation對應(yīng)value值的文件加載到流里面
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
        try {
            //用Properties文件加載文件里的內(nèi)容
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //關(guān)流
            if(null!=resourceAsStream){
                try {
                    resourceAsStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    private void doScanner(String packageName) {
        //把所有的.替換成/
        URL url  =this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\\.", "/"));
        File dir = new File(url.getFile());
        for (File file : dir.listFiles()) {
            if(file.isDirectory()){
                //遞歸讀取包
                doScanner(packageName+"."+file.getName());
            }else{
                String className =packageName +"." +file.getName().replace(".class", "");
                classNames.add(className);
            }
        }
    }

    private void doInstance() {
        if (classNames.isEmpty()) {
            return;
        }   
        for (String className : classNames) {
            try {
                //把類搞出來,反射來實例化(只有加@MyController需要實例化)
                Class<?> clazz =Class.forName(className);
               if(clazz.isAnnotationPresent(MyController.class)){
                    ioc.put(toLowerFirstWord(clazz.getSimpleName()),clazz.newInstance());
                }else{
                    continue;
                }

            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    private void initHandlerMapping(){
        if(ioc.isEmpty()){
            return;
        }
        try {
            for (Entry<String, Object> entry: ioc.entrySet()) {
                Class<? extends Object> clazz = entry.getValue().getClass();
                if(!clazz.isAnnotationPresent(MyController.class)){
                    continue;
                }

                //拼url時,是controller頭的url拼上方法上的url
                String baseUrl ="";
                if(clazz.isAnnotationPresent(MyRequestMapping.class)){
                    MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
                    baseUrl=annotation.value();
                }
                Method[] methods = clazz.getMethods();
                for (Method method : methods) {
                    if(!method.isAnnotationPresent(MyRequestMapping.class)){
                        continue;
                    }
                    MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);
                    String url = annotation.value();

                    url =(baseUrl+"/"+url).replaceAll("/+", "/");
                    handlerMapping.put(url,method);
                    controllerMap.put(url,clazz.newInstance());
                    System.out.println(url+","+method);
                }

            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 把字符串的首字母小寫
     * @param name
     * @return
     */
    private String toLowerFirstWord(String name){
        char[] charArray = name.toCharArray();
        charArray[0] += 32;
        return String.valueOf(charArray);
    }

}

這里我們就開發(fā)完了自己的SpringMVC刊愚,現(xiàn)在我們測試一下:

package com.liugh.core.controller;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.liugh.annotation.MyController;
import com.liugh.annotation.MyRequestMapping;
import com.liugh.annotation.MyRequestParam;

@MyController
@MyRequestMapping("/test")
public class TestController {

     @MyRequestMapping("/doTest")
    public void test1(HttpServletRequest request, HttpServletResponse response,
            @MyRequestParam("param") String param){
        System.out.println(param);
        try {
            response.getWriter().write( "doTest method success! param:"+param);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

     @MyRequestMapping("/doTest2")
    public void test2(HttpServletRequest request, HttpServletResponse response){
        try {
            response.getWriter().println("doTest2 method success!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

訪問http://localhost:8080/liughMVC/test/doTest?param=liugh如下:

image

訪問一個不存在的試試:

image

到這里我們就大功告成了!

??我是個普通的程序猿踩验,水平有限鸥诽,文章難免有錯誤,歡迎犧牲自己寶貴時間的讀者箕憾,就本文內(nèi)容直抒己見牡借,我的目的僅僅是希望對讀者有所幫助。源碼地址:https://github.com/qq53182347/liughMVC

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末袭异,一起剝皮案震驚了整個濱河市钠龙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖俊鱼,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刻像,死亡現(xiàn)場離奇詭異,居然都是意外死亡并闲,警方通過查閱死者的電腦和手機细睡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帝火,“玉大人溜徙,你說我怎么就攤上這事∠睿” “怎么了蠢壹?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長九巡。 經(jīng)常有香客問我图贸,道長,這世上最難降的妖魔是什么冕广? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任疏日,我火速辦了婚禮,結(jié)果婚禮上撒汉,老公的妹妹穿的比我還像新娘沟优。我一直安慰自己,他們只是感情好睬辐,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布挠阁。 她就那樣靜靜地躺著,像睡著了一般溯饵。 火紅的嫁衣襯著肌膚如雪侵俗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天瓣喊,我揣著相機與錄音坡慌,去河邊找鬼黔酥。 笑死藻三,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的跪者。 我是一名探鬼主播棵帽,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渣玲!你這毒婦竟也來了逗概?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忘衍,失蹤者是張志新(化名)和其女友劉穎逾苫,沒想到半個月后卿城,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡铅搓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年瑟押,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片星掰。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡多望,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出氢烘,到底是詐尸還是另有隱情怀偷,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布播玖,位于F島的核電站椎工,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜀踏。R本人自食惡果不足惜晋渺,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脓斩。 院中可真熱鬧木西,春花似錦、人聲如沸随静。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燎猛。三九已至恋捆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間重绷,已是汗流浹背沸停。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昭卓,地道東北人愤钾。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像候醒,于是被迫代替她去往敵國和親能颁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容