標題是‘從零開始實現一個簡易的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-api
和jsp-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。相似的包還有Gson
和Jackson
等,這裏就不具體比較了,能夠挑選一個本身喜歡的。github
首先咱們要了解到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
, 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.jsonThe
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.api
這段大體意思就是:springmvc經過中心Servlet(DispatcherServlet)來實現對控制controller的操做。這個Servlet
要經過java配置或者配置在web.xml中,它用於尋找請求的映射(即找到對應的controller),視圖解析(即執行controller的結果),異常處理(即對執行過程的異常統一處理)等等
因此實現MVC的效果就是如下幾點:
DispatcherServlet
來接收全部請求根據上面的步驟,咱們先從步驟二、三、四、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最多見的註解。
爲了可以方便的傳遞參數到前端,建立一個工具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分發器相似於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
變量中的功能。大概講解一些這個類的功能流程:
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流程中的步驟三、四、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()
實例化調用方法要傳入的參數值resultResolver()
解析方法的返回值,選擇返回頁面或者json經過這幾個步驟算是凝聚了MVC核心步驟了,不過因爲篇幅問題,幾乎每一步驟得功能都有所精簡,如
雖然有缺陷,可是一個MVC流程是完成了。接下來就要把這些功能組裝一下了。
終於到實現開頭說的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步驟的三、四、5的時候都放在了一個類裏,這樣也很差,原本這裏每一步驟的功能就很繁雜,還將這幾步驟都放在一個類中,這樣不利於後期更改對應步驟的功能。
還有目前也沒實現異常的處理,不能返回異常頁面給用戶。
這些優化工做會在後期的章節完成的。
- 從零開始實現一個簡易的Java MVC框架(一)--前言
- 從零開始實現一個簡易的Java MVC框架(二)--實現Bean容器
- 從零開始實現一個簡易的Java MVC框架(三)--實現IOC
- 從零開始實現一個簡易的Java MVC框架(四)--實現AOP
- 從零開始實現一個簡易的Java MVC框架(五)--引入aspectj實現AOP切點
- 從零開始實現一個簡易的Java MVC框架(六)--增強AOP功能
- 從零開始實現一個簡易的Java MVC框架(七)--實現MVC
- 從零開始實現一個簡易的Java MVC框架(八)--製做Starter
- 從零開始實現一個簡易的Java MVC框架(九)--優化MVC代碼
源碼地址:doodle