手寫 SpringMVC 框架

手寫SpringMVC框架之前呢,我覺得有必要先了解SpringMVC的請求處理流程以及高級特性。

一、SpringMVC 的請求處理流程

1、請求處理流程

流程說明:

第一步:用戶發送請求至前端控制器DispatcherServlet

第二步:DispatcherServlet收到請求調用HandlerMapping處理器映射器。

第三步:處理器映射器根據請求Url找到具體的Handler(後端控制器),生成處理器對象及處理器攔截器 (如果有則生成) 一併返回DispatcherServlet

第四步:DispatcherServlet調用HandlerAdapter處理器適配器去調用Handler

第五步:處理器適配器執行Handler

第六步:Handler 執行完成給處理器適配器返回ModelAndView

第七步:處理器適配器向前端控制器返回 ModelAndViewModelAndView 是SpringMVC 框架的一個 底層對 象,包括 Model 和 View

第八步:前端控制器請求視圖解析器去進行視圖解析,根據邏輯視圖名來解析真正的視圖。

第九步:視圖解析器向前端控制器返回View

第十步:前端控制器進行視圖渲染,就是將模型數據 (在 ModelAndView 對象中) 填充到 request 域。

第十一步:前端控制器向用戶響應結果。

2、Spring MVC 九大組件

HandlerMapping 是用來查找 Handler 的,也就是處理器,具體的表現形式可以是類,也可以是方法。比如,標註了@RequestMapping的每個方法都可以看成是一個HandlerHandler負責具體實際的請求處理,在請求到達後,HandlerMapping 的作用便是找到請求相應的處理器 Handler 和 Interceptor

HandlerAdapter 是一個適配器。因爲 Spring MVC 中 Handler 可以是任意形式的,只要能處理請求即可。但是把請求交給 Servlet 的時候,由於 Servlet 的方法結構都是 doService(HttpServletRequest req,HttpServletResponse resp)形式的,要讓固定的 Servlet 處理方法調用 Handler 來進行處理,便是 HandlerAdapter 的職責。

HandlerExceptionResolver 用於處理 Handler 產生的異常情況。它的作用是根據異常設置
ModelAndView,之後交給渲染方法進行渲染,渲染方法會將 ModelAndView 渲染成⻚面。

ViewResolver即視圖解析器,用於將String類型的視圖名和Locale解析爲View類型的視圖,只有一 個resolveViewName()方法。從方法的定義可以看出,Controller層返回的String類型視圖名 viewName 最終會在這裏被解析成爲ViewView是用來渲染⻚面的,也就是說,它會將程序返回的參數和數據填入模板中,生成html文件。ViewResolver 在這個過程主要完成兩件事情: ViewResolver 找到渲染所用的模板 (第一件大事) 和所用的技術(第二件大事,其實也就是找到視圖的類型,如JSP) 並填入參數。默認情況下,Spring MVC會自動爲我們配置一個 InternalResourceViewResolver,是針對 JSP 類型視圖的。

RequestToViewNameTranslator 組件的作用是從請求中獲取 ViewName,因爲 ViewResolver 根據 ViewName 查找 View,但有的 Handler 處理完成之後,沒有設置 View,也沒有設置 ViewName, 便要通過這個組件從請求中查找 ViewName

ViewResolver 組件的 resolveViewName 方法需要兩個參數,一個是視圖名,一個是 Locale。 LocaleResolver 用於從請求中解析出 Locale,比如中國 Locale 是 zh-CN,用來表示一個區域。這 個組件也是 i18n 的基礎。

ThemeResolver 組件是用來解析主題的。主題是樣式、圖片及它們所形成的顯示效果的集合。 Spring MVC 中一套主題對應一個 properties文件,裏面存放着與當前主題相關的所有資源,如圖片、CSS樣式等。創建主題非常簡單,只需準備好資源,然後新建一個 “主題名.properties” 並將資源設置進去,放在classpath下,之後便可以在⻚面中使用了。SpringMVC中與主題相關的類有 ThemeResolverThemeSourceThemeThemeResolver負責從請求中解析出主題名, ThemeSource根據主題名找到具體的主題,其抽象也就是Theme,可以通過Theme來獲取主題和具體的資源。

MultipartResolver 用於上傳請求,通過將普通的請求包裝成 MultipartHttpServletRequest 來實現。MultipartHttpServletRequest 可以通過 getFile() 方法 直接獲得文件。如果上傳多個文件,還可以調用 getFileMap()方法得到Map<FileName,File>這樣的結構,MultipartResolver 的作用就是封裝普通的請求,使其擁有文件上傳的功能。

FlashMap 用於重定向時的參數傳遞,比如在處理用戶訂單時候,爲了避免重複提交,可以處理完 post請求之後重定向到一個get請求,這個get請求可以用來顯示訂單詳情之類的信息。這樣做雖然可以規避用戶重新提交訂單的問題,但是在這個⻚面上要顯示訂單的信息,這些數據從哪裏來獲得呢?因爲重定向時麼有傳遞參數這一功能的,如果不想把參數寫進URL(不推薦),那麼就可以通過FlashMap來傳遞。只需要在重定向之前將要傳遞的數據寫入請求 (可以通過
ServletRequestAttributes.getRequest()方法獲得) 的屬性OUTPUT_FLASH_MAP_ATTRIBUTE 中,這樣在重定向之後的HandlerSpring就會自動將其設置到Model中,在顯示訂單信息的⻚面 上就可以直接從Model中獲取數據。FlashMapManager 就是用來管理 FalshMap 的。

二、Spring MVC 高級技術

監聽器、過濾器和攔截器對比

作用一:做一些初始化工作,web應用中spring容器啓動ContextLoaderListener
作用二:監聽web中的特定事件,比如HttpSession,ServletRequest的創建和銷燬;變量的創建、 銷燬和修改等。可以在某些動作前後增加處理,實現監控,比如統計在線人數,利用 HttpSessionLisener等。

從配置的⻆度也能夠總結髮現:serlvet、filter、listener是配置在web.xml中的,而interceptor是配置在表現層框架自己的配置文件中的。

Handler業務邏輯執行之前攔截一次
Handler邏輯執行完畢但未跳轉⻚面之前攔截一次在跳轉⻚面之後攔截一次

三、手寫 SpringMVC 框架

好了,回顧完請求處理流程與一些高級特性後,我們開始來手寫 SpringMVC 框架了。

我們來梳理下流程,爲了更加清晰手寫 SpringMVC 框架的思路,我畫了下面這張圖:

1、自定義註解類

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {

    String value() default "";

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {

    String value() default "";

}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {

    String value() default "";

}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

    String value() default "";

}

2、DispatcherServlet 最核心的類

public class DispatcherServlet extends HttpServlet {

    private Properties properties = new Properties();

    private List<String> classNames = Lists.newArrayList(); // 緩存掃描

    private Map<String, Object> ioc = Maps.newHashMap(); // ioc容器

    // handlerMapping
    // private Map<String, Method> handlerMapping = Maps.newHashMap(); // 存儲url和method之間的映射關係
    private List<Handler> handlerMapping = Lists.newArrayList();

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        // 1.加載配置文件 springmvc.properties
        String contextConfigLocation = servletConfig.getInitParameter("contextConfigLocation");
        doLoadConfig(contextConfigLocation);

        // 2.掃描相關的類,掃描註解。
        doScan(properties.getProperty("scanPackage"));

        // 3.初始化bean對象(實現ioc容器,基於註解)
        doInstance();

        // 4.實現依賴注入
        doAutowired();

        // 5.構造一個HandlerMapping處理器映射器,將配置好的url和Method建立映射關係
        initHandlerMapping();

        System.out.println("riemann mvc init success...");

        // 6.等待請求進入,處理請求。
    }

    /**
     * 構造一個HandlerMapping處理器映射器
     * 最關鍵的步驟
     * 目的:將url和method建立關聯
     */
    private void initHandlerMapping() {
        if (ioc.isEmpty()) return;
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            // 獲取ioc容器中當前遍歷的對象的class類型
            Class<?> clazz = entry.getValue().getClass();
            if (!clazz.isAnnotationPresent(Controller.class)) continue;

            String baseUrl = "";
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping annotation = clazz.getAnnotation(RequestMapping.class);
                baseUrl = annotation.value(); // 等同於 /riemann
            }

            // 獲取方法
            Method[] methods = clazz.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                // 方法沒有標識RequestMapping,就不處理
                if (!method.isAnnotationPresent(RequestMapping.class)) continue;
                // 如果標識則處理
                RequestMapping annotation = method.getAnnotation(RequestMapping.class);
                String methodUrl = annotation.value(); // 等同於 /query
                String url = baseUrl + methodUrl; // 計算出來的url /riemann/query

                // 把method所有信息及url封裝爲一個Handler
                Handler handler = new Handler(entry.getValue(), method, Pattern.compile(url));

                // 計算方法的參數位置信息 // query(HttpServletRequest request, HttpServletResponse response, String name)
                Parameter[] parameters = method.getParameters();
                for (int j = 0; j < parameters.length; j++) {
                    Parameter parameter = parameters[j];
                    if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {
                        // 如果是request和response對象,那麼參數名稱寫HttpServletRequest和HttpServletResponse
                        handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), j);
                    } else {
                        handler.getParamIndexMapping().put(parameter.getName(), j); // <name, 2>
                    }
                }

                // 建立url和method之間的映射關係(map緩存起來)
                // handlerMapping.put(url, method);

                handlerMapping.add(handler);
            }

        }
    }

    /**
     * 實現依賴注入
     */
    private void doAutowired() {
        if (ioc.isEmpty()) return;
        // 有對象,再進行依賴注入處理
        // 遍歷ioc中所有對象,查看對象中的字段,是否有@Autowired註解,如果有需要維護依賴注入的關係
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            // 獲取bean對象中的字段信息
            Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
            // 遍歷判斷處理
            for (int i = 0; i < declaredFields.length; i++) {
                Field declaredField = declaredFields[i]; // @Autowired private RiemannService riemannService;
                if (!declaredField.isAnnotationPresent(Autowired.class)) continue;
                // 有該註解
                Autowired annotation = declaredField.getAnnotation(Autowired.class);
                String beanName = annotation.value(); // 需要注入的bean的id
                if ("".equals(beanName.trim())) {
                    // 沒有配置具體的bean id,那就需要根據當前字段類型注入(接口注入)RiemannService
                    beanName = declaredField.getType().getName();
                }
                // 開啓賦值
                declaredField.setAccessible(true);
                try {
                    declaredField.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * ioc容器
     * 基於className緩存的類的全限定類名,以及反射技術,完成對象創建和管理。
     */
    private void doInstance() {
        if (classNames.size() == 0) return;
        try {
            for (int i = 0; i < classNames.size(); i++) {
                String className = classNames.get(i); // com.riemann.controller.RiemannController
                // 反射
                Class<?> clazz = Class.forName(className);
                // 區分controller,區分service
                if (clazz.isAnnotationPresent(Controller.class)) {
                    // controller的id不做過多處理,不取value了,就拿類的首字母小寫作爲id,保存到ioc中
                    String simpleName = clazz.getSimpleName(); // RiemannController
                    String lowerLetterSimpleName = lowerLetterFirst(simpleName); // riemannController
                    Object o = clazz.newInstance();
                    ioc.put(lowerLetterSimpleName, o);
                } else if (clazz.isAnnotationPresent(Service.class)) {
                    Service annotation = clazz.getAnnotation(Service.class);
                    // 獲取註解的值
                    String beanName = annotation.value();
                    // 如果指定了id,就以指定的爲準
                    if (!"".equals(beanName.trim())) {
                        ioc.put(beanName, clazz.newInstance());
                    } else {
                        // 如果沒有指定,就以類名首字母小寫
                        beanName = lowerLetterFirst(clazz.getSimpleName());
                        ioc.put(beanName, clazz.newInstance());
                    }

                    // service層往往是有接口的,面向接口開發,此時再以接口名爲id,放入一份對象到ioc容器中,便於後期根據接口類型注入
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for (int j = 0; j < interfaces.length; j++) {
                        Class<?> anInterface = interfaces[j];
                        // 以接口的全限定類名作爲id放入
                        ioc.put(anInterface.getName(), clazz.newInstance());
                    }

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

    public String lowerLetterFirst(String str) {
        char[] chars = str.toCharArray();
        if ('A' <= chars[0] && chars[0] <= 'Z') {
            chars[0] += 32;
        }
        return String.valueOf(chars);
    }

    /**
     * 掃描類
     * scanPackage:com.riemann ---> 磁盤上的文件夾(File) com/riemann
     * @param scanPackage
     */
    private void doScan(String scanPackage) {
        String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() +
                scanPackage.replaceAll("\\.", "/");
        File packageName = new File(scanPackagePath);
        for (File file : packageName.listFiles()) {
            if (file.isDirectory()) { // 子package
                // 遞歸
                doScan(scanPackage + "." + file.getName()); // com.riemann.controller
            } else if (file.getName().endsWith(".class")) {
                String className = scanPackage + "." + file.getName().replaceAll(".class", "");
                classNames.add(className);
            }
        }
    }

    /**
     * 加載配置文件
     * @param contextConfigLocation
     */
    private void doLoadConfig(String contextConfigLocation) {
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 處理請求,根據url,找到對應的Method方法,進行調用。
        // 獲取uri
        // String requestURI = req.getRequestURI();
        // Method method = handlerMapping.get(requestURI); // 獲取到一個反射的方法
        // 反射調用,需要傳入對象,需要傳入參數,此處無法完成調用,沒有把對象緩存起來,也沒有參數!!!需要改造initHandlerMapping()
        // method.invoke();

        // 根據uri獲取到我們能夠處理當前請求的handler(從handlerMapping中(List))
        Handler handler = getHandler(req);
        if (handler == null) {
            resp.getWriter().write("404 not found");
            return;
        }

        // 參數綁定
        // 獲取所有參數類型數組,這個數組的長度就是我們最後要傳入的args數組的長度
        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();

        // 根據上述數組長度創建一個新的數組(參數數組,是要傳入反射調用的)
        Object[] paramValues = new Object[parameterTypes.length];

        // 以下就是爲了向參數數組中塞值,而且還得保證參數的順序和方法中形參順序一致
        Map<String, String[]> parameterMap = req.getParameterMap();

        // 遍歷request中所有參數(填充除了request、response之外的)
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            // name=1&name=2 name [1,2]
            String value = StringUtils.join(param.getValue(), ","); // 如同 1,2

            // 如果參數和方法中的參數匹配上了,填充數據。
            if (!handler.getParamIndexMapping().containsKey(param.getKey())) continue;

            // 方法形參確實有該參數,找到它的索引位置,對應的把參數值放入paramValues
            Integer index = handler.getParamIndexMapping().get(param.getKey()); // name在第2個位置

            paramValues[index] = value; // 把前臺傳遞過來的參數值填充到對應的位置去
        }

        int requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); // 0
        paramValues[requestIndex] = req;

        int responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); // 1
        paramValues[responseIndex] = resp;


        // 最終調用handler的method屬性
        try {
            handler.getMethod().invoke(handler.getController(), paramValues);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private Handler getHandler(HttpServletRequest req) {
        if (handlerMapping.isEmpty()) return null;
        String requestURI = req.getRequestURI();

        for (Handler handler : handlerMapping) {
            Matcher matcher = handler.getPattern().matcher(requestURI);
            if (!matcher.matches()) continue;
            return handler;
        }
        return null;
    }

}

3、pojo 類 Handler

/**
 * 封裝handler方法相關的信息
 */
@Data
public class Handler {

    private Object controller; // method.invoke(obj,);

    private Method method;

    private Pattern pattern; // spring中url是支持正則的

    private Map<String, Integer> paramIndexMapping; // 參數的順序,是爲了進行參數綁定。key是參數名,value是第幾個參數

    public Handler(Object controller, Method method, Pattern pattern) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;
        paramIndexMapping = Maps.newHashMap();
    }

}

4、web.xml 配置

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>

  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <servlet-name>riemannmvc</servlet-name>
    <servlet-class>com.riemann.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>springmvc.properties</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>riemannmvc</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

</web-app>

5、RiemannController.java

@Controller
@RequestMapping("/riemann")
public class RiemannController {

    @Autowired
    private RiemannService riemannService;

    /**
     * URL: /riemann/query
     * @param request
     * @param response
     * @param name
     * @return
     */
    @RequestMapping("/query")
    public String query(HttpServletRequest request, HttpServletResponse response, String name) {
        return riemannService.get(name);
    }

}

6、測試結果

瀏覽器輸入:http://localhost:8888/riemann/query?name=riemann

riemann mvc init success...
RiemannService 實現類中的name參數:riemann

ok,測試成功,這樣就完成了手寫 SpringMVC 框架的簡易版了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YW-HpNAX2UiUekqY0yDU_Q