從零開始實現一個簡易的Java MVC框架(七)--實現MVC

前言

標題是‘從零開始實現一個簡易的Java MVC框架’,結果寫了這麼多才到實現MVC的時候...只能說前戲確實有點多了。不過這些前戲都是必須的,若是隻是簡簡單單實現一個MVC的功能那就沒有意思了,要有Bean容器、IOC、AOP和MVC纔像是一個'框架'嘛。html

實現準備

爲了實現mvc的功能,先要爲pom.xml添加一些依賴。前端

<properties>
    ...
    <tomcat.version>8.5.31</tomcat.version>
    <jstl.version>1.2</jstl.version>
    <fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
	...
    <!-- tomcat embed -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <version>${tomcat.version}</version>
    </dependency>

    <!-- JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>${jstl.version}</version>
        <scope>runtime</scope>
    </dependency>

    <!-- FastJson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>
</dependencies>
複製代碼
  • tomcat-embed-jasper這個依賴是引入了一個內置的tomcat,spring-boot默認就是引用這個嵌入式的tomcat包實現直接啓動服務的。這個包除了加入了一個嵌入式的tomcat,還引入了java.servlet-apijsp-api這兩個包,若是不想用這種嵌入式的tomcat的話,能夠去除tomcat-embed-jasper而後引入這兩個包。java

  • jstl用於解析jsp表達式的,好比在jsp頁面編寫下面這樣c:forEach語句就須要這個包。git

    <c:forEach items="${list}" var="user">
    	<tr>
            <td>${user.id}</td>
            <td>${user.name}</td>
    	</tr>
    </c:forEach>
    複製代碼
  • fastjson是阿里開發的一個json解析包,用於將實體類轉換成json。相似的包還有GsonJackson等,這裏就不具體比較了,能夠挑選一個本身喜歡的。github

實現MVC

MVC實現原理

首先咱們要了解到MVC的實現原理,在使用spring-boot編寫項目的時候,咱們一般都是經過編寫一系列的Controller來實現一個個連接,這是'現代'的寫法。可是在之前springmvc甚至是struts2這類mvc框架都還沒流行的時候,都是經過編寫Servlet來實現。web

每個請求都會對應一個Servlet,而後還要在web.xml中配置這個Servlet,而後對請求的接收和處理啥的都分佈在一大堆的Servlet中,代碼十分混雜。spring

爲了讓人們編寫的時候更專一於業務代碼而減小對請求的處理,springmvc就經過一箇中央的Servlet,處理這些請求,而後再轉發到對應的Controller中,這樣就只有一個Servlet統一處理請求了。下面的一段話來自spring的官方文檔docs.spring.io/spring/docs…apache

Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.json

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification using Java configuration or in web.xml. In turn the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.api

這段大體意思就是:springmvc經過中心Servlet(DispatcherServlet)來實現對控制controller的操做。這個Servlet要經過java配置或者配置在web.xml中,它用於尋找請求的映射(即找到對應的controller),視圖解析(即執行controller的結果),異常處理(即對執行過程的異常統一處理)等等

因此實現MVC的效果就是如下幾點:

  1. 經過一箇中央sevlet如DispatcherServlet來接收全部請求
  2. 根據請求找到對應的controller
  3. 執行controller獲取結果
  4. 對controller的結果解析並轉到對應視圖
  5. 如有異常則統一處理異常

根據上面的步驟,咱們先從步驟二、三、四、5開始,最後再實現1完成mvc。

建立註解

爲了方便實現,先在com.zbw.mvc.annotation包下建立三個註解和一個枚舉:RequestMappingRequestParamResponseBodyRequestMethod

package com.zbw.mvc.annotation;
import ...

/** * http請求路徑 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /** * 請求路徑 */
    String value() default "";

    /** * 請求方法 */
    RequestMethod method() default RequestMethod.GET;
}
複製代碼
package com.zbw.mvc.annotation;

/** * http請求類型 */
public enum RequestMethod {
    GET, POST
}
複製代碼
package com.zbw.mvc.annotation;
import ...

/** * 請求的方法參數名 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    /** * 方法參數別名 */
    String value() default "";

    /** * 是否必傳 */
    boolean required() default true;
}
複製代碼
package com.zbw.mvc.annotation;
import ...

/** * 用於標記返回json */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

複製代碼

這幾個類的做用就不解釋了,都是springmvc最多見的註解。

建立ModelAndView

爲了可以方便的傳遞參數到前端,建立一個工具bean,至關於spring中簡化版的ModelAndView。這個類建立於com.zbw.mvc.bean包下

package com.zbw.mvc.bean;
import ...

/** * ModelAndView */
public class ModelAndView {

    /** * 頁面路徑 */
    private String view;

    /** * 頁面data數據 */
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView setView(String view) {
        this.view = view;
        return this;
    }
    public String getView() {
        return view;
    }
    public ModelAndView addObject(String attributeName, Object attributeValue) {
        model.put(attributeName, attributeValue);
        return this;
    }
    public ModelAndView addAllObjects(Map<String, ?> modelMap) {
        model.putAll(modelMap);
        return this;
    }
    public Map<String, Object> getModel() {
        return model;
    }
}
複製代碼

實現Controller分發器

Controller分發器相似於Bean容器,只不事後者是存放Bean的而前者是存放Controller的,而後根據一些條件能夠簡單的獲取對應的Controller。

先在com.zbw.mvc包下建立一個ControllerInfo類,用於存放Controller的一些信息。

package com.zbw.mvc;
import ...

/** * ControllerInfo 存儲Controller相關信息 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
    /** * controller類 */
    private Class<?> controllerClass;

    /** * 執行的方法 */
    private Method invokeMethod;

    /** * 方法參數別名對應參數類型 */
    private Map<String, Class<?>> methodParameter;
}
複製代碼

而後再建立一個PathInfo類,用於存放請求路徑和請求方法類型

package com.zbw.mvc;
import ...

/** * PathInfo 存儲http相關信息 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
    /** * http請求方法 */
    private String httpMethod;

    /** * http請求路徑 */
    private String httpPath;
}
複製代碼

接着建立Controller分發器類ControllerHandler

package com.zbw.mvc;
import ...

/** * Controller 分發器 */
@Slf4j
public class ControllerHandler {

    private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();

    private BeanContainer beanContainer;

    public ControllerHandler() {
        beanContainer = BeanContainer.getInstance();
        Set<Class<?>> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
        for (Class<?> clz : classSet) {
            putPathController(clz);
        }
    }

    /** * 獲取ControllerInfo */
    public ControllerInfo getController(String requestMethod, String requestPath) {
        PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
        return pathControllerMap.get(pathInfo);
    }

    /** * 添加信息到requestControllerMap中 */
    private void putPathController(Class<?> clz) {
        RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
        String basePath = controllerRequest.value();
        Method[] controllerMethods = clz.getDeclaredMethods();
        // 1. 遍歷Controller中的方法
        for (Method method : controllerMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                // 2. 獲取這個方法的參數名字和參數類型
                Map<String, Class<?>> params = new HashMap<>();
                for (Parameter methodParam : method.getParameters()) {
                    RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
                    if (null == requestParam) {
                        throw new RuntimeException("必須有RequestParam指定的參數名");
                    }
                    params.put(requestParam.value(), methodParam.getType());
                }
                // 3. 獲取這個方法上的RequestMapping註解
                RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
                String methodPath = methodRequest.value();
                RequestMethod requestMethod = methodRequest.method();
                PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
                if (pathControllerMap.containsKey(pathInfo)) {
                    log.error("url:{} 重複註冊", pathInfo.getHttpPath());
                    throw new RuntimeException("url重複註冊");
                }
                // 4. 生成ControllerInfo並存入Map中
                ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
                this.pathControllerMap.put(pathInfo, controllerInfo);
                log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}",
                        pathInfo.getHttpMethod(), pathInfo.getHttpPath(),
                        controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
            }
        }
    }
}
複製代碼

這個類最複雜的就是構造函數中調用的putPathController()方法,這個方法也是這個類的核心方法,實現了controller類中的信息存放到pathControllerMap變量中的功能。大概講解一些這個類的功能流程:

  1. 在構造方法中獲取Bean容器BeanContainer的單例實例
  2. 獲取並遍歷BeanContainer中存放的被RequestMapping註解標記的類
  3. 遍歷這個類中的方法,找出被RequestMapping註解標記的方法
  4. 獲取這個方法的參數名字和參數類型,生成ControllerInfo
  5. 根據RequestMapping裏的value()method()生成PathInfo
  6. 將生成的PathInfoControllerInfo存到變量pathControllerMap
  7. 其餘類經過調用getController()方法獲取到對應的controller

以上就是這個類的流程,其中有個注意的點:

步驟4的時候,必須規定這個方法的全部參數名字都被RequestParam註解標註,這是由於在java中,雖然咱們編寫代碼的時候是有參數名的,好比String name這樣的形式,可是被編譯成class文件後‘name’這個字段就會被擦除,因此必需要經過一個RequestParam來保存名字。

可是你們在springmvc中並不用必須每一個方法都用註解標記的,這是由於spring中藉助了*asm* ,這種工具能夠在編譯以前拿到參數名而後保存起來。還有一種方法是在java8以後支持了保存參數名,可是必須修改編譯器的參數來支持。這兩種方法實現起來都比較複雜或者有限制條件,這裏就不實現了,你們能夠查找資料本身實現

實現結果執行器

接下來實現結果執行器,這個類中實現剛纔mvc流程中的步驟三、四、5。

在com.zbw.mvc包下建立類ResultRender

package com.zbw.mvc;
import ...

/** * 結果執行器 */
@Slf4j
public class ResultRender {

    private BeanContainer beanContainer;

    public ResultRender() {
        beanContainer = BeanContainer.getInstance();
    }

    /** * 執行Controller的方法 */
    public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
        // 1. 獲取HttpServletRequest全部參數
        Map<String, String> requestParam = getRequestParams(req);
        // 2. 實例化調用方法要傳入的參數值
        List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);

        Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
        Method invokeMethod = controllerInfo.getInvokeMethod();
        invokeMethod.setAccessible(true);
        Object result;
        // 3. 經過反射調用方法
        try {
            if (methodParams.size() == 0) {
                result = invokeMethod.invoke(controller);
            } else {
                result = invokeMethod.invoke(controller, methodParams.toArray());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 4.解析方法的返回值,選擇返回頁面或者json
        resultResolver(controllerInfo, result, req, resp);
    }

    /** * 獲取http中的參數 */
    private Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        //GET和POST方法是這樣獲取請求參數的
        request.getParameterMap().forEach((paramName, paramsValues) -> {
            if (ValidateUtil.isNotEmpty(paramsValues)) {
                paramMap.put(paramName, paramsValues[0]);
            }
        });
        // TODO: Body、Path、Header等方式的請求參數獲取
        return paramMap;
    }

    /** * 實例化方法參數 */
    private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
        return methodParams.keySet().stream().map(paramName -> {
            Class<?> type = methodParams.get(paramName);
            String requestValue = requestParams.get(paramName);
            Object value;
            if (null == requestValue) {
                value = CastUtil.primitiveNull(type);
            } else {
                value = CastUtil.convert(type, requestValue);
                // TODO: 實現非原生類的參數實例化
            }
            return value;
        }).collect(Collectors.toList());
    }


    /** * Controller方法執行後返回值解析 */
    private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
        if (null == result) {
            return;
        }
        boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
        if (isJson) {
            // 設置響應頭
            resp.setContentType("application/json");
            resp.setCharacterEncoding("UTF-8");
            // 向響應中寫入數據
            try (PrintWriter writer = resp.getWriter()) {
                writer.write(JSON.toJSONString(result));
                writer.flush();
            } catch (IOException e) {
                log.error("轉發請求失敗", e);
                // TODO: 異常統一處理,400等...
            }
        } else {
            String path;
            if (result instanceof ModelAndView) {
                ModelAndView mv = (ModelAndView) result;
                path = mv.getView();
                Map<String, Object> model = mv.getModel();
                if (ValidateUtil.isNotEmpty(model)) {
                    for (Map.Entry<String, Object> entry : model.entrySet()) {
                        req.setAttribute(entry.getKey(), entry.getValue());
                    }
                }
            } else if (result instanceof String) {
                path = (String) result;
            } else {
                throw new RuntimeException("返回類型不合法");
            }
            try {
                req.getRequestDispatcher("/templates/" + path).forward(req, resp);
            } catch (Exception e) {
                log.error("轉發請求失敗", e);
                // TODO: 異常統一處理,400等...
            }
        }
    }
}
複製代碼

經過調用類中的invokeController()方法反射調用了Controller中的方法並根據結果解析對應的頁面。主要流程爲:

  1. 調用getRequestParams() 獲取HttpServletRequest中參數
  2. 調用instantiateMethodArgs() 實例化調用方法要傳入的參數值
  3. 經過反射調用目標controller的目標方法
  4. 調用resultResolver()解析方法的返回值,選擇返回頁面或者json

經過這幾個步驟算是凝聚了MVC核心步驟了,不過因爲篇幅問題,幾乎每一步驟得功能都有所精簡,如

  • 步驟1獲取HttpServletRequest中參數只獲取get或者post傳的參數,實際上還有 Body、Path、Header等方式的請求參數獲取沒有實現
  • 步驟2實例化調用方法的值只實現了java的原生參數,自定義的類的實例化沒有實現
  • 步驟4異常統一處理也沒具體實現

雖然有缺陷,可是一個MVC流程是完成了。接下來就要把這些功能組裝一下了。

實現DispatcherServlet

終於到實現開頭說的DispatcherServlet了,這個類繼承於HttpServlet,全部請求都從這裏通過。

在com.zbw.mvc下建立DispatcherServlet

package com.zbw.mvc;
import ...

/** * DispatcherServlet 全部http請求都由此Servlet轉發 */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    private ControllerHandler controllerHandler = new ControllerHandler();

    private ResultRender resultRender = new ResultRender();

    /** * 執行請求 */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 設置請求編碼方式
        req.setCharacterEncoding("UTF-8");
        //獲取請求方法和請求路徑
        String requestMethod = req.getMethod();
        String requestPath = req.getPathInfo();
        log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
        if (requestPath.endsWith("/")) {
            requestPath = requestPath.substring(0, requestPath.length() - 1);
        }

        ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
        log.info("{}", controllerInfo);
        if (null == controllerInfo) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        resultRender.invokeController(req, resp, controllerInfo);
    }
}

複製代碼

在這個類裏調用了ControllerHandlerResultRender兩個類,先根據請求的方法和路徑獲取對應的ControllerInfo,而後再用ControllerInfo解析出對應的視圖,而後就能訪問到對應的頁面或者返回對應的json信息了。

然而一直在說的全部請求都從DispatcherServlet通過好像沒有體現啊,這是由於要配置web.xml才行,如今不少都在使用spring-boot的朋友可能不大清楚了,在之前使用springmvc+spring+mybatis時代的時候要寫不少配置文件,其中一個就是web.xml,要在裏面添加上。經過通配符*讓全部請求都走的是DispatcherServlet。

<servlet>
	<servlet-name>springMVC</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>springMVC</servlet-name>
	<url-pattern>*</url-pattern>
</servlet-mapping>
複製代碼

不過咱們無需這樣作,爲了致敬spring-boot,咱們會在下一節實現內嵌Tomcat,並經過啓動器啓動。

缺陷

可能這一節的代碼讓你們看起來不是很舒服,這是由於目前這個代碼雖說功能已是實現了,可是代碼結構還須要優化。

首先DispatcherServlet是一個請求分發器,這裏面不該該有處理Http的邏輯代碼的

其次咱們把MVC步驟的三、四、5的時候都放在了一個類裏,這樣也很差,原本這裏每一步驟的功能就很繁雜,還將這幾步驟都放在一個類中,這樣不利於後期更改對應步驟的功能。

還有目前也沒實現異常的處理,不能返回異常頁面給用戶。

這些優化工做會在後期的章節完成的。


源碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架(七)--實現MVC

相關文章
相關標籤/搜索