自己寫一個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來返回出來,這個不太好,最起碼要能返回個頁面啥的。
下一章開始寫視圖控制器
拜拜~