前言
標題是‘從零開始實現一個簡易的Java MVC框架’,結果寫了這麼多才到實現MVC的時候…只能說前戲確實有點多了。不過這些前戲都是必須的,如果只是簡簡單單實現一個MVC的功能那就沒有意思了,要有Bean容器、IOC、AOP和MVC才像是一個`框架`嘛。
實現準備
為了實現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-api
和jsp-api
這兩個包,如果不想用這種嵌入式的tomcat的話,可以去除tomcat-embed-jasper
然後引入這兩個包。 -
jstl
用於解析jsp表示式的,比如在jsp頁面編寫下面這樣c:forEach
語句就需要這個包。<c:forEach items="${list}" var="user"> <tr> <td>${user.id}</td> <td>${user.name}</td> </tr> </c:forEach> 複製程式碼
-
fastjson
是阿里開發的一個json解析包,用於將實體類轉換成json。類似的包還有Gson
和Jackson
等,這裡就不具體比較了,可以挑選一個自己喜歡的。
實現MVC
MVC實現原理
首先我們要了解到MVC的實現原理,在使用spring-boot編寫專案的時候,我們通常都是通過編寫一系列的Controller來實現一個個連結,這是`現代`的寫法。但是在以前springmvc甚至是struts2這類mvc框架都還沒流行的時候,都是通過編寫Servlet
來實現。
每一個請求都會對應一個Servlet
,然後還要在web.xml中配置這個Servlet
,然後對請求的接收和處理啥的都分佈在一大堆的Servlet
中,程式碼十分混雜。
為了讓人們編寫的時候更專注於業務程式碼而減少對請求的處理,springmvc就通過一箇中央的Servlet
,處理這些請求,然後再轉發到對應的Controller中,這樣就只有一個Servlet
統一處理請求了。下面的一段話來自spring的官方文件docs.spring.io/spring/docs…
Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central
Servlet
, theDispatcherServlet
, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.The
DispatcherServlet
, as anyServlet
, needs to be declared and mapped according to the Servlet specification using Java configuration or inweb.xml
. In turn theDispatcherServlet
uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.
這段大致意思就是:springmvc通過中心Servlet(DispatcherServlet)來實現對控制controller的操作。這個Servlet
要通過java配置或者配置在web.xml中,它用於尋找請求的對映(即找到對應的controller),檢視解析(即執行controller的結果),異常處理(即對執行過程的異常統一處理)等等
所以實現MVC的效果就是以下幾點:
- 通過一箇中央sevlet如
DispatcherServlet
來接收所有請求 - 根據請求找到對應的controller
- 執行controller獲取結果
- 對controller的結果解析並轉到對應檢視
- 若有異常則統一處理異常
根據上面的步驟,我們先從步驟2、3、4、5開始,最後再實現1完成mvc。
建立註解
為了方便實現,先在com.zbw.mvc.annotation包下建立三個註解和一個列舉:RequestMapping
、RequestParam
、ResponseBody
、RequestMethod
。
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
變數中的功能。大概講解一些這個類的功能流程:
- 在構造方法中獲取Bean容器
BeanContainer
的單例例項 - 獲取並遍歷
BeanContainer
中存放的被RequestMapping
註解標記的類 - 遍歷這個類中的方法,找出被
RequestMapping
註解標記的方法 - 獲取這個方法的引數名字和引數型別,生成
ControllerInfo
- 根據
RequestMapping
裡的value()
和method()
生成PathInfo
- 將生成的
PathInfo
和ControllerInfo
存到變數pathControllerMap
中 - 其他類通過呼叫
getController()
方法獲取到對應的controller
以上就是這個類的流程,其中有個注意的點:
步驟4的時候,必須規定這個方法的所有引數名字都被RequestParam
註解標註,這是因為在java中,雖然我們編寫程式碼的時候是有引數名的,比如String name
這樣的形式,但是被編譯成class檔案後‘name’這個欄位就會被擦除,所以必須要通過一個RequestParam
來儲存名字。
但是大家在springmvc中並不用必須每個方法都用註解標記的,這是因為spring中藉助了*asm
* ,這種工具可以在編譯之前拿到引數名然後儲存起來。還有一種方法是在java8之後支援了儲存引數名,但是必須修改編譯器的引數來支援。這兩種方法實現起來都比較複雜或者有限制條件,這裡就不實現了,大家可以查詢資料自己實現
實現結果執行器
接下來實現結果執行器,這個類中實現剛才mvc流程中的步驟3、4、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中的方法並根據結果解析對應的頁面。主要流程為:
- 呼叫
getRequestParams()
獲取HttpServletRequest中引數 - 呼叫
instantiateMethodArgs()
例項化呼叫方法要傳入的引數值 - 通過反射呼叫目標controller的目標方法
- 呼叫
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);
}
}
複製程式碼
在這個類裡呼叫了ControllerHandler
和ResultRender
兩個類,先根據請求的方法和路徑獲取對應的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步驟的3、4、5的時候都放在了一個類裡,這樣也不好,本來這裡每一步驟的功能就很繁雜,還將這幾步驟都放在一個類中,這樣不利於後期更改對應步驟的功能。
還有目前也沒實現異常的處理,不能返回異常頁面給使用者。
這些優化工作會在後期的章節完成的。
原始碼地址:doodle