自己寫一個java的mvc框架吧(四)
自己寫一個mvc框架吧(四)
寫一個請求的入口,以及初始化框架
上一章寫了獲取方法的入參,並根據入參的引數型別進行資料轉換。這時候,我們已經具備了通過反射呼叫方法的一切必要條件。現在我們缺少一個http請求的入口,就是一個servlet。現在我們開始寫吧~
在這一章我們要做的事情有
-
定義一個配置檔案,用來描述什麼樣的請求對映到哪一個class的哪一個方法上面。
-
在servlet初始化後,根據上面定義的配置檔案載入mvc框架。
-
在一個http請求進入後,根據其請求路徑,找到相應的方法,獲取引數,使用反射執行該方法。
-
得到方法的執行結果後,先以json的形式在瀏覽器顯示出來。
這一步是檢視層的功能,先這樣寫,之後在寫各種檢視控制器 。
現在開始寫吧
定義配置檔案
這裡的配置不一定 就必須是一個xml ,json ,yaml ... 之類的檔案,也可以是註解 的形式。區別就只是在載入框架的時候根據不同的形式進行解析就好了。這裡為了寫起來方便,就先定義一個json 的配置檔案(因為json的檔案用起來比較方便)。
著這個配置檔案中我們需要定義一些引數,這些引數需要滿足我們將一個http請求對映到一個方法上的需求。我是這樣定義的:
{ "annotationSupport": false, "mapping": [ { "url": "/index", "requestType": [ "get" ], "method": "index", "objectClass": "com.hebaibai.demo.web.IndexController", "paramTypes": [ "java.lang.String" ] } ] }
下面說一下各個屬性是幹啥用的:
1:annotationSupport :用來描述有沒有開啟註解的支援,現在還沒有寫,就給了一個false。
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:applicationUrlMapping :url描述 與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 來返回出來,這個不太好,最起碼要能返回個頁面啥的。
下一章開始寫檢視控制器
拜拜~