本身寫一個java的mvc框架吧(四)

本身寫一個mvc框架吧(四)

寫一個請求的入口,以及初始化框架

上一章寫了獲取方法的入參,並根據入參的參數類型進行數據轉換。這時候,咱們已經具有了經過反射調用方法的一切必要條件。如今咱們缺乏一個http請求的入口,就是一個servlet。如今咱們開始寫吧~java

在這一章咱們要作的事情有

  1. 定義一個配置文件,用來描述什麼樣的請求映射到哪個class的哪個方法上面。web

  2. 在servlet初始化後,根據上面定義的配置文件加載mvc框架。json

  3. 在一個http請求進入後,根據其請求路徑,找到相應的方法,獲取參數,使用反射執行該方法。數組

  4. 獲得方法的執行結果後,先以json的形式在瀏覽器顯示出來。瀏覽器

    這一步是視圖層的功能,先這樣寫,以後在寫各類視圖控制器緩存

如今開始寫吧

定義配置文件

這裏的配置不必定就必須是一個xmljsonyaml... 之類的文件,也能夠是註解的形式。區別就只是在加載框架的時候根據不一樣的形式進行解析就行了。這裏爲了寫起來方便,就先定義一個json的配置文件(由於json的文件用起來比較方便)。mvc

着這個配置文件中咱們須要定義一些參數,這些參數須要知足咱們將一個http請求映射到一個方法上的需求。我是這樣定義的:app

{
  "annotationSupport": false,
  "mapping": [
    {
      "url": "/index",
      "requestType": [
        "get"
      ],
      "method": "index",
      "objectClass": "com.hebaibai.demo.web.IndexController",
      "paramTypes": [
        "java.lang.String"
      ]
    }
  ]
}

下面說一下各個屬性是幹啥用的:框架

1:annotationSupport:用來描述有沒有開啓註解的支持,如今尚未寫,就給了一個false。ide

2:mapping:用來描述映射關係的數據,是一個數組的類型。一個對象表示一個映射關係。

3:url:http請求的地址,表示這個映射關係對應的是哪個請求地址。

4:requestType:這個映射支持的請求類型,數組的形式。說明一個方法支持多種請求方式。

5:objectClass:這個映射必定的是哪個java對象。

6:method:這個映射關係對應的objectClass中的方法名稱。

7:paramTypes:方法的入參類型,這裏是一個數組,順序要和定義的方法中的入參順序相一致。定義這個參數是由於在經過反射找到一個一個Method的時候須要有兩個參數,一是方法名稱,另外一個就是入參類型。因此這兩個是必不可少的。

這裏的配置說實話看起來有點複雜,用起來也不是很方便。好比在修改一個方法入參的時候,若是修改了參數類型,就要修改對應的配置。這裏之後能夠作一些簡化處理,好比使用註解的形式,這樣就會方便不少。可是如今是在設計並實現的階段,能夠把全部的配置按照最複雜的形式來作,完成功能以後再進行優化,能夠添加一些全局的默認配置,這樣就能夠減小配置文件的編寫。

上面的配置文件寫完了,開始寫怎樣加載這個配置文件,並初始化這個mvc框架。

根據約定獲取配置文件名稱

由於請求的入口我用的是servlet,每個servlet都須要配置 一個servlet-name,因此咱們能夠約定配置文件的名稱就是就是servlet-name的名稱後加上」.json「。例如我定義一個servlet:

<servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class>com.hebaibai.amvc.MvcServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

這時,配置文件的名稱就是mvc.json。那麼怎麼作呢? 咱們這麼寫:

//先定義一個servlet
public class MvcServlet extends HttpServlet {

    //重寫其中的方法
    @Override
    public void init(ServletConfig config) {
        //執行父類的init方法
        super.init(config);
        //獲取servlet的名稱
        String servletName = config.getServletName();
        //接下來,就能夠寫別的東西了
    }
}

在上面的代碼中,我只取到了servlet-name,尚未開始讀取配置文件。由於我認爲讀取配置和加載咱們的框架這件事請不該該寫在一個servlet中,因此我定義了一個類Application.java。在這個類裏面用來處理讀取配置文件,加載各類配置以及緩存http映射以及別的一些我還沒想到的事情。這個Application.java有一個帶參數的構造函數,參數是應用名稱,就是servlet-name,這樣每個類的功能就能夠分開了。接下來咱們寫這個類裏應該有什麼東西。

讀取配置文件並完成框架加載

先把代碼貼出來:

package com.hebaibai.amvc;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.hebaibai.amvc.namegetter.AsmParamNameGetter;
import com.hebaibai.amvc.objectfactory.AlwaysNewObjectFactory;
import com.hebaibai.amvc.objectfactory.ObjectFactory;
import com.hebaibai.amvc.utils.Assert;
import com.hebaibai.amvc.utils.ClassUtils;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.java.Log;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * aMac
 *
 * @author hjx
 */
@Log
public class Application {

    private static final String NOT_FIND = "缺乏配置!";
    //urlMapping節點名稱
    private static final String MAPPING_NODE = "mapping";
    //是否支持註解
    private static final String ANNOTATION_SUPPORT_NODE = "annotationSupport";

    /**
     * 映射的工廠類
     */
    private UrlMethodMappingFactory urlMethodMappingFactory = new UrlMethodMappingFactory();

    /**
     * 生成對象的工廠
     */
    private ObjectFactory objectFactory;

    /**
     * 應用的名稱
     */
    private String applicationName;

    /**
     * 應用中的全部urlMapping
     */
    private Map<String, UrlMethodMapping> applicationUrlMapping = new ConcurrentHashMap<>();


    /**
     * 構造函數,經過servletName加載配置
     *
     * @param applicationName
     */
    public Application(String applicationName) {
        this.applicationName = applicationName;
        init();
    }

    /**
     * 初始化配置
     */
    @SneakyThrows(IOException.class)
    protected void init() {
        String configFileName = applicationName + ".json";
        InputStream inputStream = ClassUtils.getClassLoader().getResourceAsStream(configFileName);
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes);
        String config = new String(bytes, "utf-8");
        //應用配置
        JSONObject configJson = JSONObject.parseObject(config);
        boolean annotationSupport = configJson.getBoolean(ANNOTATION_SUPPORT_NODE);

        //TODO:是否開啓註解,註解支持以後寫
        Assert.isTrue(!annotationSupport, "如今不支持此功能!");
        urlMethodMappingFactory.setParamNameGetter(new AsmParamNameGetter());

        //TODO:生成對象的工廠類(當先默認爲每次都new一個新的對象)
        this.objectFactory = new AlwaysNewObjectFactory();

        JSONArray jsonArray = configJson.getJSONArray(MAPPING_NODE);
        Assert.notNull(jsonArray, MAPPING_NODE + NOT_FIND);
        for (int i = 0; i < jsonArray.size(); i++) {
            UrlMethodMapping mapping = urlMethodMappingFactory.getUrlMethodMappingByJson(jsonArray.getJSONObject(i));
            addApplicationUrlMapping(mapping);
        }
    }

    /**
     * 將映射映射添加進應用
     *
     * @param urlMethodMapping
     */
    protected void addApplicationUrlMapping(@NonNull UrlMethodMapping urlMethodMapping) {
        RequestType[] requestTypes = urlMethodMapping.getRequestTypes();
        String url = urlMethodMapping.getUrl();
        for (RequestType requestType : requestTypes) {
            String urlDescribe = getUrlDescribe(requestType, url);
            if (applicationUrlMapping.containsKey(urlDescribe)) {
                throw new UnsupportedOperationException(urlDescribe + "已經存在!");
            }
            Method method = urlMethodMapping.getMethod();
            Class aClass = urlMethodMapping.getClass();
            log.info("mapping url:" + urlDescribe + " to " + aClass.getName() + "." + method.getName());
            applicationUrlMapping.put(urlDescribe, urlMethodMapping);
        }
    }

    /**
     * 獲取Url的描述
     *
     * @param requestType
     * @param url
     * @return
     */
    protected String getUrlDescribe(RequestType requestType, @NonNull String url) {
        return requestType.name() + ":" + url;
    }

    /**
     * 根據url描述獲取 UrlMethodMapping
     *
     * @param urlDescribe
     * @return
     */
    protected UrlMethodMapping getUrlMethodMapping(@NonNull String urlDescribe) {
        UrlMethodMapping urlMethodMapping = applicationUrlMapping.get(urlDescribe);
        return urlMethodMapping;
    }

    /**
     * 生成對象的工廠
     *
     * @return
     */
    protected ObjectFactory getObjectFactory() {
        return this.objectFactory;
    }

}

這個類中我用了一些lombok的註解,你們能夠先不用管它

屬性的說明

1:UrlMethodMappingFactory :用來建立url與Method的映射關係:UrlMethodMapping的工廠類,在 本身寫一個mvc框架吧(二)這一篇中有說到。

2:applicationName :應用的名稱,其實就是servlet的名稱(web.xml中servlet-name節點中的值)

3:applicationUrlMappingurl描述UrlMethodMapping 的一個對應關係。url描述是我本身定義的一個東西,結構基本上是這樣的:請求類型+「:」+請求地址。例子:「 GET:/index 」。

4:objectFactory:對象工廠,用來實例化對象用的,在 本身寫一個mvc框架吧(二)這一篇中有說道。

方法的說明

1:init():用來根據應用名稱,拼接配置文件的名稱,並讀取其中的內容,並作一些校驗。

2:getUrlDescribe(): 獲取前面說道的url描述

3:addApplicationUrlMapping(UrlMethodMapping urlMethodMapping): 將 applicationUrlMapping 填充起來。

4:getUrlMethodMapping(String urlDescribe):根據url描述獲取 urlMethodMapping

5:getObjectFactory():獲取對象工廠,用來在servlet中實例化對象

如今加載框架的代碼寫好了,下面開始寫Servlet。

寫請求的入口:servlet

這個寫起來比較簡單,須要作的事情有以下幾個:

1:在servlet初始化的時候獲取servlet的名稱,而後加載咱們的mvc框架。

2:在獲得一次http請求的時候,根據請求地址、請求方式獲取對應的Method,也就是urlMethodMapping

3:根據urlMethodMapping獲取對應的參數,轉換成相應的類型,並經過反射執行方法。

4:將返回結果轉換爲Json,並在瀏覽器顯示出來。(這一步是暫時的)

由於在前幾章咱們已經將不少代碼寫好了,這裏咱們只須要將以前寫的一些東西拼起來就行了,並不須要寫太多的東西,下面吧代碼貼出來:

import com.alibaba.fastjson.JSONObject;
import com.hebaibai.amvc.objectfactory.ObjectFactory;
import lombok.SneakyThrows;
import lombok.extern.java.Log;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * mvc的入口
 *
 * @author hjx
 */
@Log
public class MvcServlet extends HttpServlet {

    /**
     * 應用
     */
    private Application application;

    /**
     * 請求中的參數獲取器
     */
    private MethodValueGetter methodValueGetter;


    /**
     * 初始化項目
     * 1:獲取Servlet名稱,加載名稱相同的配置文件
     * 2:加載配置文件中的urlMapping
     */
    @Override
    @SneakyThrows(ServletException.class)
    public void init(ServletConfig config) {
        super.init(config);
        String servletName = config.getServletName();
        log.info("aMvc init servletName:" + servletName);
        application = new Application(servletName);
        methodValueGetter = new MethodValueGetter();
    }

    /**
     * 執行請求
     *
     * @param request
     * @param response
     */
    @SneakyThrows({IOException.class})
    private void doInvoke(HttpServletRequest request, HttpServletResponse response) {
        RequestType requestType = getRequestType(request.getMethod());
        String urlDescribe = application.getUrlDescribe(requestType, request.getPathInfo());
        UrlMethodMapping urlMethodMapping = application.getUrlMethodMapping(urlDescribe);
        //沒有找到對應的mapping
        if (urlMethodMapping == null) {
            unsupportedMethod(request, response);
            return;
        }
        //方法執行結果
        Object result = invokeMethod(urlMethodMapping, request);
        //TODO:視圖處理,先以JSON形式返回
        response.setHeader("content-type", "application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write(JSONObject.toJSONString(result));
        writer.close();
    }

    /**
     * 反射執行方法
     *
     * @param urlMethodMapping
     * @param request
     * @return
     */
    @SneakyThrows({IllegalAccessException.class, InvocationTargetException.class})
    private Object invokeMethod(UrlMethodMapping urlMethodMapping, HttpServletRequest request) {
        Object[] methodValue = methodValueGetter.getMethodValue(urlMethodMapping.getParamClasses(), urlMethodMapping.getParamNames(), request);
        Method method = urlMethodMapping.getMethod();
        Class objectClass = urlMethodMapping.getObjectClass();
        //經過對象工廠實例化objectClass
        ObjectFactory objectFactory = application.getObjectFactory();
        Object object = objectFactory.getObject(objectClass);
        return method.invoke(object, methodValue);
    }

    /**
     * 根據http請求方式獲取RequestType
     *
     * @param requestMethod
     * @return
     */
    private RequestType getRequestType(String requestMethod) {
        if (requestMethod.equalsIgnoreCase(RequestType.GET.name())) {
            return RequestType.GET;
        }
        if (requestMethod.equalsIgnoreCase(RequestType.POST.name())) {
            return RequestType.POST;
        }
        if (requestMethod.equalsIgnoreCase(RequestType.PUT.name())) {
            return RequestType.PUT;
        }
        if (requestMethod.equalsIgnoreCase(RequestType.DELETE.name())) {
            return RequestType.DELETE;
        }
        throw new UnsupportedOperationException("請求方式不支持:" + requestMethod);
    }

    /**
     * 不支持的請求方式
     *
     * @param request
     * @param response
     */
    @SneakyThrows(IOException.class)
    private void unsupportedMethod(HttpServletRequest request, HttpServletResponse response) {
        String protocol = request.getProtocol();
        String method = request.getMethod();
        String errorMsg = "不支持的請求方式:" + method + "!";
        if (protocol.endsWith("1.1")) {
            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, errorMsg);
        } else {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, errorMsg);
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        doInvoke(request, response);
    }


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        doInvoke(request, response);
    }

    @Override
    protected void doPut(HttpServletRequest request, HttpServletResponse response) {
        doInvoke(request, response);
    }

    @Override
    protected void doDelete(HttpServletRequest request, HttpServletResponse response) {
        doInvoke(request, response);
    }
}

這裏主要說一下 doInvoke(HttpServletRequest request, HttpServletResponse response)invokeMethod(UrlMethodMapping urlMethodMapping, HttpServletRequest request) 這兩個方法。

doInvoke:處理每次請求的主要方法,負責根據請求的信息獲取對應的Method執行這個Method,在沒有找到對應Method的時候顯示對應的錯誤信息。最後根據配置將其處理成相應的視圖(如今是Json)。

invokeMethod:經過對象工廠獲取實例化對象,並經過反射執行Method,獲取方法的返回值。

如今入口就寫好了,新建一個Web項目測試一下吧

測試一下

首先咱們新建一個web項目,以後在web.xml中添加:

<servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class>com.hebaibai.amvc.MvcServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

而後寫一個IndexController.java做爲controller:

package com.hebaibai.demo.web;

import java.util.HashMap;
import java.util.Map;

/**
 * @author hjx
 */
public class IndexController {

    /**
     * @param name
     * @return
     */
    public Map<String, String> index(String name) {
        Map<String, String> map = new HashMap<>();
        map.put("value", name);
        map.put("msg", "success");
        return map;
    }

}

由於servlet-name的值爲mvc,因此咱們須要在resources目錄下新建文件mvc.json做爲配置文件,so~ 新建文件:

{
  "annotationSupport": false,
  "mapping": [
    {
      "url": "/index",
      "requestType": [
        "get"
      ],
      "method": "index",
      "objectClass": "com.hebaibai.demo.web.IndexController",
      "paramTypes": [
        "java.lang.String"
      ]
    }
  ]
}

如今全部的配製就寫好,能夠測試了~~~

but~~,如今有一個BUG,驚不驚喜 !!!

有一個BUG

這個bug是在 本身寫一個mvc框架吧(二) 這一章的經過asm獲取方法入參名稱的時候出現的,以前的代碼是這樣的:

ClassReader classReader = null;
try {
    classReader = new ClassReader(className);
} catch (IOException e) {
    e.printStackTrace();
}

由於咱們最終寫好的mvc框架是做爲一個jar包出現的,因此在jar中,是沒法經過這種形式解析到依賴這個jar的項目中的class,這裏會出現一個異常,我以爲應該是類加載器在獲取文件路徑時候的問題。怎麼解決呢?

解決bug

咱們看一下classReader = new ClassReader(className) 這個方法的實現代碼:

/**
 * Constructs a new {@link ClassReader} object.
 *
 * @param className the fully qualified name of the class to be read. The ClassFile structure is
 *     retrieved with the current class loader's {@link ClassLoader#getSystemResourceAsStream}.
 * @throws IOException if an exception occurs during reading.
 */
public ClassReader(final String className) throws IOException {
  this(
      readStream(
          ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));
}

他是經過class的包名稱轉換成爲文件路徑以後,經過相對路徑(應該是以項目路徑做爲根路徑)的形式讀取的,這樣就好解決了。咱們使用絕對路徑的形式(以系統中的根路)獲取到這個文件流就行了,這樣寫:

ClassReader getClassReader(Class aClass) {
    Assert.notNull(aClass);
    String className = aClass.getName();
    String path = getClass().getClassLoader().getResource("/").getPath();
    File classFile = new File(path + className.replace('.', '/') + ".class");
    try (InputStream inputStream = new FileInputStream(classFile)) {
        ClassReader classReader = new ClassReader(inputStream);
        return classReader;
    } catch (IOException e) {
    }
    throw new RuntimeException(className + "沒法加載!");
}

先獲取到項目中的根目錄在系統中的那個位置,而後將包名轉換成文文件路徑,最後拼接一下就行了~ 搞定。

如今就能夠測試了,只須要將剛纔的web項目啓動後,訪問一下配置的地址,就行了。我就不寫了~~

最後

還剩視圖控制器沒有寫,如今咱們只是簡單的用Json來返回出來,這個不太好,最起碼要能返回個頁面啥的。

下一章開始寫視圖控制器

拜拜~

相關文章
相關標籤/搜索