SpringMVC源碼手寫實現
作者:田超凡
時間:2019-08-23
開始編碼前的準備工作:
Maven依賴新增javax.servlet依賴,用於編寫的MVC核心控制器繼承自HttpServlet並重寫對應方法,調用對應ServletAPI方法實現
Maven依賴新增fastjson依賴處理json序列化和反序列化。
pom.xml配置如下:
<!-- TODO TCF MVC請求映射Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- TODO TCF Json序列化和反序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
主要實現原理簡述:
(1).首先,定義模型封裝類
Request封裝請求方式和請求路徑(注意,此處存放的請求路徑是請求URL相對項目根目錄的路徑,調用request.getServletPath()獲取)
Handle定義處理每次請求的控制器和方法,Request和Handle是一對一的關聯
Param封裝請求參數
View封裝控制器處理方法執行完畢之後的視圖解析結果,包括視圖路徑和綁定的模型參數
Data封裝控制器處理方法執行完畢之後直接返回的響應數據,最後通過json序列化成json字符串然後寫入輸出流返回給前臺。
(2).其次,定義ControllerHelper控制器助手類用於加載所有視圖層的控制器並建立Request和Handle的關聯映射
(3).最後,編寫核心控制器並在web.xml中註冊servlet(或者直接通過@WebServlet註解註冊)
KidDispatcherServlet是整個MVC框架最核心的管理和控制器,作用等效於Spring MVC的DispatcherServlet核心控制器
對於靜態資源攔截和過濾,可以通過配置默認的servlet並定義URL攔截規則實現。可在web.xml中直接配置default servlet,也可以在KidDispatcherServlet初始化方法中定義ServletRegistration並addMapping,兩種方式均可用來過濾靜態資源請求。
1.定義模型封裝類
Request.java 封裝請求方式和請求路徑
package com.tcf.kid.smart.framework.mvc;
/***
* TODO TCF 請求信息類
* TODO TCF 封裝請求方式和請求路徑
* @author 71485
*
*/
public class Request {
//TODO TCF 請求方式
private String requestMethod;
//TODO TCF 請求路徑
private String requestPath;
public String getRequestMethod() {
return requestMethod;
}
public String getRequestPath() {
return requestPath;
}
//TODO TCF 構造注入請求方式和請求路徑
public Request(String requestMethod,String requestPath)
{
this.requestMethod=requestMethod;
this.requestPath=requestPath;
}
//TODO TCF 默認無參構造
public Request()
{
}
}
Handle.java 封裝處理請求的控制器和處理方法
package com.tcf.kid.smart.framework.mvc;
import java.lang.reflect.Method;
/**
* TODO TCF 請求處理類
* TODO TCF 封裝處理請求的控制器和處理方法
* @author 71485
*
*/
public class Handle {
//TODO TCF 處理請求控制器
private Class<?> handleController;
//TODO TCF 處理請求方法
private Method handleMethod;
public Class<?> getHandleController() {
return handleController;
}
public Method getHandleMethod() {
return handleMethod;
}
//TODO TCF 構造注入處理請求的控制器和處理方法
public Handle(Class<?> handleController,Method handleMethod)
{
this.handleController=handleController;
this.handleMethod=handleMethod;
}
//TODO TCF 默認無參構造
public Handle()
{
}
}
Param.java 封裝請求參數
package com.tcf.kid.smart.framework.mvc;
import java.util.Map;
/**
* TODO TCF 封裝請求參數
* @author 71485
*
*/
public class Param {
//TODO TCF 請求參數 ParameterName:ParameterValue
private Map<String,Object> paramMap;
public Map<String, Object> getParamMap() {
return paramMap;
}
//TODO TCF 構造注入請求參數
public Param(Map<String,Object> paramMap)
{
this.paramMap=paramMap;
}
//TODO TCF 默認無參構造
public Param()
{
}
}
View.java 封裝視圖解析器
package com.tcf.kid.smart.framework.mvc;
import java.util.Map;
/***
* TODO TCF 視圖解析器類
* TODO TCF 定義視圖名稱和綁定的模型參數
* @author 71485
*
*/
public class View {
//TODO TCF 視圖名稱
private String viewName;
//TODO TCF 綁定的模型參數
private Map<String,Object> modelParams;
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModelParams() {
return modelParams;
}
public void setModelParams(Map<String, Object> modelParams) {
this.modelParams = modelParams;
}
}
Data.java 封裝響應結果
package com.tcf.kid.smart.framework.mvc;
/***
* TODO TCF 輸出響應數據類
* TODO TCF 控制器處理方法直接返回的相應數據,適用於ajax或直接寫入輸出流的情況
* @author 71485
*
*/
public class Data {
//TODO TCF 輸出數據
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
//TODO TCF 構造注入輸出數據
public Data(Object data)
{
this.data=data;
}
//TODO TCF 默認無參構造
public Data()
{
}
}
2.定義視圖層相關注解
@KidController 標註和註冊控制器
package com.tcf.kid.smart.framework.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* TODO TCF 註冊控制器組件
* @author 71485
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface KidController {
}
@KidAction 標註和匹配映射請求的控制器處理方法
package com.tcf.kid.smart.framework.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***
* TODO TCF 標註控制器處理方法
* @author 71485
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface KidAction {
String mapping();
String method();
}
3.定義控制器助手類ControllerHelper,用於建立和管理Request -> Handle的請求->處理映射關係。
package com.tcf.kid.smart.framework.helper;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import com.tcf.kid.smart.framework.annotation.KidAction;
import com.tcf.kid.smart.framework.annotation.KidController;
import com.tcf.kid.smart.framework.mvc.Handle;
import com.tcf.kid.smart.framework.mvc.Request;
/***
* TODO TCF 控制器助手類
* TODO TCF 掃描所有視圖層控制器和處理方法,建立請求和處理的映射關係
* TODO TCF 初始化加載請求和處理的映射Map
* @author 71485
*
*/
public class ControllerHelper {
//TODO TCF 請求和處理的映射Map
public static Map<Request,Handle> MAPPING_MAP=new HashMap<Request,Handle>();
static
{
//TODO TCF 建立請求和處理之間的映射關係
initHandleMapping();
}
//TODO TCF 掃描控制器和處理方法並建立和請求的映射關係,初始化請求和處理之間的映射Map
public static void initHandleMapping()
{
try
{
//TODO TCF 加載控制器註解標註的控制器
Set<Class<?>> controllerClassList=ClassHelper.loadClassByAnnotation(KidController.class);
if(controllerClassList!=null && controllerClassList.size()>0)
{
for(Class<?> controllerClass:controllerClassList)
{
//TODO TCF 控制器中定義的所有方法
Method[] methods=controllerClass.getDeclaredMethods();
if(methods!=null && methods.length>0)
{
for(Method method:methods)
{
//TODO TCF 默認只有使用KidAction註解標註的類纔是控制器處理方法
if(method.isAnnotationPresent(KidAction.class))
{
KidAction action=method.getAnnotation(KidAction.class);
if(action!=null)
{
String actionUrl=action.mapping();
String actionMethod=action.method();
if(StringUtils.isNotEmpty(actionUrl) && StringUtils.isNotEmpty(actionMethod))
{
//TODO TCF 請求路徑
String requestPath=actionUrl;
//TODO TCF 請求方式
String requestMethod=actionMethod.toLowerCase();
Request request=new Request(requestMethod,requestPath);
Handle handle=new Handle(controllerClass,method);
MAPPING_MAP.put(request,handle);
}
}
}
}
}
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
//TODO TCF 根據請求方式和請求路徑獲取處理請求信息
public static Handle getHandleByRequest(String requestMethod,String requestPath)
{
Request request=new Request(requestMethod,requestPath);
Handle handle=new Handle();
for(Map.Entry<Request,Handle> mappingEntry:MAPPING_MAP.entrySet())
{
Request entryRequest=mappingEntry.getKey();
if(entryRequest!=null)
{
if(requestMethod.equals(entryRequest.getRequestMethod())
&& requestPath.equals(entryRequest.getRequestPath()))
{
handle=mappingEntry.getValue();
break;
}
}
}
return handle;
}
}
4.定義處理請求控制器KidDispatcherServlet
package com.tcf.kid.smart.framework.core;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.tcf.kid.smart.framework.common.Const;
import com.tcf.kid.smart.framework.helper.BeanHelper;
import com.tcf.kid.smart.framework.helper.ClassHelper;
import com.tcf.kid.smart.framework.helper.ControllerHelper;
import com.tcf.kid.smart.framework.mvc.Data;
import com.tcf.kid.smart.framework.mvc.Handle;
import com.tcf.kid.smart.framework.mvc.Param;
import com.tcf.kid.smart.framework.mvc.View;
import com.tcf.kid.smart.framework.util.CodecUtil;
import com.tcf.kid.smart.framework.util.JsonUtil;
import com.tcf.kid.smart.framework.util.PropertiesUtil;
import com.tcf.kid.smart.framework.util.ReflectUtil;
import com.tcf.kid.smart.framework.util.StreamUtil;
/***
* TODO TCF MVC核心處理請求控制器
* @author 71485
*
*/
@WebServlet
public class KidDispatcherServlet extends HttpServlet{
//TODO TCF 初始化
@Override
public void init(ServletConfig servletConfig) throws ServletException
{
//TODO TCF 啓動IOC/AOP容器,掃包
ClassHelper.loadCoreClass();
//TODO TCF 註冊Servlet資源服務(各類視圖/其他靜態資源)
ServletContext servletContext=servletConfig.getServletContext();
//TODO TCF HTML視圖資源
ServletRegistration viewRegistration=servletContext.getServletRegistration(Const.STATIC_RESOURCE_TYPE.JSP);
viewRegistration.addMapping(PropertiesUtil.loadPropertiesFile(Const.PROPERTIES_FILES.BASE_PROPERTIES).getProperty(Const.BASE_PROPERTIES_KEYS.VIEW_RESOURCE));
//TODO TCF ...其他視圖資源
//TODO TCF 其他靜態資源
ServletRegistration staticRegistration=servletContext.getServletRegistration(Const.STATIC_RESOURCE_TYPE.DEFAULT);
staticRegistration.addMapping(PropertiesUtil.loadPropertiesFile(Const.PROPERTIES_FILES.BASE_PROPERTIES).getProperty(Const.BASE_PROPERTIES_KEYS.STATIC_RESOURCE));
staticRegistration.addMapping("*.js");
}
//TODO TCF 服務
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try
{
//TODO TCF 請求方式
String requestMethod=request.getMethod().toLowerCase();
//TODO TCF 請求路徑
//TODO TCF request.getServletPath()獲取的是相對項目的純請求路徑,如/show.do
String requestPath=request.getServletPath();
if(StringUtils.isNotEmpty(requestMethod) && StringUtils.isNotEmpty(requestPath))
{
//TODO TCF 根據請求信息獲取處理信息
Handle handle=ControllerHelper.getHandleByRequest(requestMethod,requestPath);
if(handle!=null)
{
//TODO TCF 處理請求的控制器
Class<?> handleController=handle.getHandleController();
//TODO TCF 處理請求的方法
Method handleMethod=handle.getHandleMethod();
//TODO TCF 處理請求的控制器實例
Object handleControllerInstance=BeanHelper.getBeanInstance(handleController);
//TODO TCF 請求參數名
Enumeration<String> parameterNames=request.getParameterNames();
if(parameterNames!=null)
{
//TODO TCF 請求參數Map
Map<String,Object> paramMap=new HashMap<String,Object>();
while(parameterNames.hasMoreElements())
{
//TODO TCF 請求參數名
String parameterName=parameterNames.nextElement();
//TODO TCF 根據請求參數名獲取請求參數
String parameterValue=request.getParameter(parameterName);
paramMap.put(parameterName,parameterValue);
}
//TODO TCF 封裝請求參數
Param param=new Param(paramMap);
//TODO TCF 請求屬性
String requestUrl=CodecUtil.encodeStr(StreamUtil.readInputStream(request.getInputStream()));
if(StringUtils.isNotEmpty(requestUrl))
{
String[] parameters=requestUrl.split("&");
if(parameters!=null && parameters.length>0)
{
for(String parameter:parameters)
{
//TODO TCF 解析參數名和參數值
String[] params=parameter.split("=");
if(params!=null && params.length==2)
{
//TODO TCF 參數名
String paramName=params[0];
//TODO TCF 參數值
String paramValue=params[1];
//TODO TCF 存入請求作用域
request.setAttribute(paramName,paramValue);
}
}
}
}
if(handleController!=null && handleMethod!=null)
{
//TODO TCF 調用對應的處理請求控制器的處理方法
Object invokeResult=ReflectUtil.invokeMethod(handleControllerInstance,handleMethod,param);
//TODO TCF 根據控制器處理方法執行之後的返回結果進行下一步處理
if(invokeResult!=null)
{
if(invokeResult instanceof View)
{
//TODO TCF 返回視圖
View view=(View)invokeResult;
if(view!=null)
{
//TODO TCF 返回視圖名
String viewName=view.getViewName();
if(StringUtils.isNotEmpty(viewName))
{
if(viewName.startsWith("/"))
{
//TODO TCF 響應方式:重定向
response.sendRedirect(PropertiesUtil.loadPropertiesFile(Const.PROPERTIES_FILES.BASE_PROPERTIES).getProperty(Const.BASE_PROPERTIES_KEYS.VIEW_RESOURCE)+viewName);
}
else
{
//TODO TCF 響應方式:轉發,解析模型參數
//TODO TCF 返回視圖並傳遞的渲染視圖的模型參數
Map<String,Object> modelParams=view.getModelParams();
if(modelParams!=null)
{
for(Map.Entry<String,Object> paramEntry:modelParams.entrySet())
{
//TODO TCF 需要綁定的模型參數名
String modelParamName=paramEntry.getKey();
//TODO TCF 需要綁定的模型參數值
Object modelParamValue=paramEntry.getValue();
//TODO TCF 存入request請求作用域
request.setAttribute(modelParamName,modelParamValue);
}
}
//TODO TCF 轉發
request.getRequestDispatcher(
PropertiesUtil.loadPropertiesFile(Const.PROPERTIES_FILES.BASE_PROPERTIES)
.getProperty(Const.BASE_PROPERTIES_KEYS.VIEW_RESOURCE)
+viewName)
.forward(request,response);
}
}
}
}
else if (invokeResult instanceof Data)
{
//TODO TCF 直接返回響應數據(如ajax等直接將響應數據寫入輸出流)
Data data=(Data)invokeResult;
if(data!=null)
{
//TODO TCF JSON序列化
String jsonString=JsonUtil.pojoToJson(data.getData());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter writer=response.getWriter();
writer.write(jsonString);
//TODO TCF 清空緩衝區,輸出響應結果
writer.flush();
writer.close();
}
}
}
}
}
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
5.配置web.xml註冊核心控制器KidDispatcherServlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>KidDispatcherServlet</servlet-name>
<servlet-class>com.tcf.kid.smart.framework.core.KidDispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>KidDispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
(6).編寫測試類控制器,測試返回結果是視圖解析器的清空和直接返回響應數據的情況(如Ajax異步)
package com.tcf.kid.smart.framework.demo;
import java.util.HashMap;
import java.util.Map;
import com.tcf.kid.smart.framework.annotation.KidAction;
import com.tcf.kid.smart.framework.annotation.KidController;
import com.tcf.kid.smart.framework.mvc.Data;
import com.tcf.kid.smart.framework.mvc.Param;
import com.tcf.kid.smart.framework.mvc.View;
/***
* TODO TCF 處理請求的控制器
* @author 71485
*
*/
@KidController
public class TestController {
//TODO TCF EXAMPLE 1 直接返回視圖解析器解析模型參數
@KidAction(mapping="/toPage.do",method="GET")
public View toPage(Param param)
{
View view=new View();
view.setViewName("example.jsp");
return view;
}
//TODO TCF EXAMPLE 2 直接返回響應結果
@KidAction(mapping="/show.do",method="GET")
public Data showProxyResult(Param param)
{
System.out.println("====代理的目標方法執行====");
System.out.println(param.getParamMap().get("message")!=null?param.getParamMap().get("message").toString():"");
Map<String,Object> paramMap=new HashMap<String,Object>();
paramMap.put("name","zhangsan");
return new Data(paramMap);
}
}
(7).前臺測試頁面index.jsp和跳轉後的example.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Example頁面</title>
<script type="text/javascript" src="static/js/jquery-1.4.4.min.js"></script>
</head>
<body>
<h1>Hello,Welcome to Use KidFramework!</h1>
<h2>Author:田超凡</h2>
<h3>測試後臺URL:/toPage.do</h3>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Example頁面</title>
<script type="text/javascript" src="static/js/jquery-1.4.4.min.js"></script>
</head>
<body>
<h1 id="showResponseName">返回響應數據=><span id="showData"></span></h1>
<script type="text/javascript">
$(document).ready(function(){
$.ajax({
url:"/kid-framework-web/show.do",
type:"GET",
data:{
"message":"***前臺傳遞消息***"
},
dataType:"json",
success:function(data){
$("#showData").html(data.name);
}
});
});
</script>
</body>
</html>
運行結果圖:
輸入toPage.do
後臺日誌: