仿照SpringMVC,實現一個輕量級MVC框架,知識涉及到了反射機制、注解的使用和一些第三方工具包的使用
思路
主要的總體流程如下圖所示
和之前一樣,我們定義了一個DispatchServlet,用於攔截請求(這里一般攔截.do結尾的url請求);
之后,DispatchServlet會根據url,找到Controller中對應的方法並執行,返回一個結果。
我們根據返回的結果,來DispatchServlet執行不同的操作(請求轉發、頁面重定向、返回json數據)
看完這里,總體的思路應該很明確了,問題來了:
- 如何實現讓DispatchServlet根據url去找到對應的方法?
- 如何根據返回的結果,讓DispatchServlet執行不同的操作?
對於第一個問題,我們可以使用注解來實現
- 定義一個
Controller
注解,用來標記Controller類 - 定義一個
RequestMapping
注解,用來標記url匹配的方法
對於第二個問題,我們可以設置這樣的一套規則:
- 返回的結果是String,且以
redirect:
開頭,則表明DispatchServlet要執行頁面重定向操作 - 返回的結果是String,不以
redirect:
開頭,則表明DispatchServlet要執行請求轉發操作 - 返回的結果是一個bean類,則自動將其轉為json數據
一般我們會讓doGet方法也調用doPost方法,所以我們只需要重寫Servlet中的doPost方法
由上面的思路,我們可以得到細化的流程圖(也就是doPost方法中的流程)
關鍵點實現
處理請求url
//獲得url地址
String servletPath = req.getServletPath();
String requestUrl = StringUtils.substringBefore(servletPath,".do");
使用注解找到對應的方法
之前有說過,我們定義了兩個注解,一個是Controller和RequestMapping
標記一個類和方法
@Controller
public class UserController{
@RequestMapping("/user/login")
public String login(HttpServletRequest request){
return "login.jsp";
}
...
}
之后我們就可以使用Lang3
開源庫去查找類中是否有Controller
標記,然后再去尋找是否被RequestMapping標記的方法,並把RequestMapping注解上的標記的url、Controller全類名和方法名存起來
這里我是用來一個類ClassMapping
來存放全類名和方法名,之后,使用url作為key,ClassMapping
對象作為value,存入一個HashMap中
這里雖然也可以放在doPost方法中,但是會造成資源的浪費,因為doPost方法可能會被執行多次。所以,更好的做法是,可以放在Servlet的初始化方法init()
里面,這樣,只會執行一次。
我封裝了一個配置類Configuration
,專門用來查找url及其對應的方法
判斷類是否包含有Controller注解
controllerClass.isAnnotationPresent(Controller.class)
獲得類中包含有RequestMapping注解的方法列表
Method[] methods = MethodUtils.getMethodsWithAnnotation(controllerClass, RequestMapping.class, true, true);
更多代碼請看下面的代碼部分Configuration類
傳參以及反射調用方法
先獲得方法的參數列表類型,之后,把對象轉入到Object數組中,反射調用方法
避免錯誤,之前還需有個判空操作和是否包含有key,下面的貼出的代碼就省略了
ClassMapping classMapping = classMappingMap.get(requestUrl);
Class<?> controllerClass = classMapping.getControllerClass();
Method method = classMapping.getMethod();
//獲得方法的參數類型列表,之后根據此類型列表,對request傳來的參數進行處理
Class<?>[] parameterTypes = method.getParameterTypes();
//存放着之后需要傳入到方法的參數值
Object[] paramValues = new Object[parameterTypes.length];
//對request傳來的參數進行處理,將參數傳給controller中的對應的方法,調用該方法
try {
//實例化controller類
Object o = controllerClass.newInstance();
for (int i = 0; i < parameterTypes.length; i++) {
//這里我們只考慮了四種情況,所以Controller種的方法參數類型也只有四種
if (ClassUtils.isAssignable(parameterTypes[i], HttpServletRequest.class)) {
paramValues[i] = req;
} else if (ClassUtils.isAssignable(parameterTypes[i], HttpServletResponse.class)) {
paramValues[i] = resp;
} else if (ClassUtils.isAssignable(parameterTypes[i], HttpSession.class)) {
paramValues[i] = req.getSession(true);
} else {
//轉為JavaBean
if (parameterTypes[i] != null) {
//獲得request傳來的參數值,轉為javabean
Map<String, String[]> parameterMap = req.getParameterMap();
//實例化這個JavaBean類型
Object bean = parameterTypes[i].newInstance();
//把數值快速轉為bean
BeanUtils.populate(bean, parameterMap);
paramValues[i] =bean;
}
}
}
//調用方法,獲得返回值,根據返回值,執行頁面跳轉或者是返回json數據等操作
Object returnValue = MethodUtils.invokeMethod(o, true,method.getName(), paramValues);
根據返回結果執行不同操作
這里使用Google的gson框架,用於把實體類或者是List轉為json數據
if (returnValue instanceof String) {
String value = (String) returnValue;
if (value.startsWith("redirect:")) {
//重定向
resp.sendRedirect(req.getContextPath()+ StringUtils.substringAfter(value,"redirect:"));
} else {
//請求轉發
req.getRequestDispatcher(value).forward(req,resp);
}
} else {
//返回一個對象
if (returnValue != null) {
//轉為json,ajax操作
String json = new Gson().toJson(o);
resp.getWriter().print(json);
}
使用注意點
不要忘了配置Servlet,和之前的Servlet一樣,可以使用配置web.xml或者是注解方式進行配置
在方法RequestMapping上的注解上的需要有"/開頭",網頁的請求url不需要"/"開頭,但是需要.do結尾
由於我們只實現了四種類型的封裝,所以Controller類中里面的方法參數只能是四種類型,request、response、session、一個JavaBean類
代碼
DispatchServlet
package mvc;
import com.google.gson.Gson;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author StarsOne
* @date Create in 2019/8/9 0009 10:11
* @description
*/
public class DispatcherServlet extends HttpServlet {
private Map<String, ClassMapping> classMappingMap =null;
private Logger logger = Logger.getLogger(DispatcherServlet.class);
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
//在servlet的初始化獲得map數據
classMappingMap = new Configuration().config();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//獲得url地址
String servletPath = req.getServletPath();
String requestUrl = StringUtils.substringBefore(servletPath,".do");
//根據url地址去和map中的匹配,找到對應的controller類以及方法,之后反射傳參調用
if (classMappingMap != null && classMappingMap.containsKey(requestUrl)) {
ClassMapping classMapping = classMappingMap.get(requestUrl);
Class<?> controllerClass = classMapping.getControllerClass();
Method method = classMapping.getMethod();
//獲得方法的參數類型列表,之后根據此類型列表,對request傳來的參數進行處理
Class<?>[] parameterTypes = method.getParameterTypes();
//存放着之后需要傳入到方法的參數值
Object[] paramValues = new Object[parameterTypes.length];
//對request傳來的參數進行處理,將參數傳給controller中的對應的方法,調用該方法
try {
//實例化controller類
Object o = controllerClass.newInstance();
for (int i = 0; i < parameterTypes.length; i++) {
if (ClassUtils.isAssignable(parameterTypes[i], HttpServletRequest.class)) {
paramValues[i] = req;
} else if (ClassUtils.isAssignable(parameterTypes[i], HttpServletResponse.class)) {
paramValues[i] = resp;
} else if (ClassUtils.isAssignable(parameterTypes[i], HttpSession.class)) {
paramValues[i] = req.getSession(true);
} else {
//轉為JavaBean
if (parameterTypes[i] != null) {
//獲得request傳來的參數值,轉為javabean
Map<String, String[]> parameterMap = req.getParameterMap();
//實例化這個JavaBean類型
Object bean = parameterTypes[i].newInstance();
//把數值快速轉為bean
BeanUtils.populate(bean, parameterMap);
paramValues[i] =bean;
}
}
}
//調用方法,獲得返回值,根據返回值,執行頁面跳轉或者是返回json數據等操作
Object returnValue = MethodUtils.invokeMethod(o, true,method.getName(), paramValues);
if (returnValue instanceof String) {
String value = (String) returnValue;
if (value.startsWith("redirect:")) {
//重定向
resp.sendRedirect(req.getContextPath()+ StringUtils.substringAfter(value,"redirect:"));
} else {
//請求轉發
req.getRequestDispatcher(value).forward(req,resp);
}
} else {
//返回一個對象
if (returnValue != null) {
//轉為json,ajax操作
String json = new Gson().toJson(o);
resp.getWriter().print(json);
}
}
} catch (InstantiationException e) {
logger.error("實例化Controller對象錯誤!");
} catch (IllegalAccessException e) {
logger.error("非法訪問方法!");
} catch (InvocationTargetException e) {
logger.error("invocation target exception");
} catch (NoSuchMethodException e) {
logger.error("調用的方法不存在!");
}
} else {
throw new RuntimeException("url不存在" + requestUrl);
}
}
}
ClassMapping
用來存放全類名和方法名
package mvc;
import java.lang.reflect.Method;
/**
* 類Class和對應的方法Method
* @author StarsOne
* @date Create in 2019/8/8 0008 22:13
* @description
*/
public class ClassMapping {
private Class<?> controllerClass;
private Method method;
public ClassMapping(Class<?> controllerClass, Method method) {
this.controllerClass = controllerClass;
this.method = method;
}
public Class<?> getControllerClass() {
return controllerClass;
}
public void setControllerClass(Class<?> controllerClass) {
this.controllerClass = controllerClass;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return controllerClass.getName() + "." + method.getName();
}
}
Configuration
package mvc;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.log4j.Logger;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import mvc.Annotation.Controller;
import mvc.Annotation.RequestMapping;
/**
* @author StarsOne
* @date Create in 2019/8/8 0008 22:11
* @description
*/
public class Configuration {
Logger logger = Logger.getLogger(Configuration.class);
private String getControllerPackage() {
return ResourceBundle.getBundle("package").getString("packagePath");
}
public Map<String, ClassMapping> config() {
Map<String, ClassMapping> classMappingMap = Collections.synchronizedMap(new HashMap<>());
try {
//根據資源文件中定義的包名,找到控制器的文件夾,得到類名
File file = new File(getClass().getResource("/").toURI());
String controllerPackage = getControllerPackage();
String controllerFullPath = file.getPath() + File.separator +controllerPackage.replaceAll("\\.",File.separator);
//controller類所在的文件夾
file = new File(controllerFullPath);
//過濾文件,只找class文件
String[] classNames = file.list((dir, name) -> name.endsWith(".class"));
for (String className : classNames) {
//拼接全類名
String controllerFullName = controllerPackage + "." + StringUtils.substringBefore(className,".class");
Class controllerClass = ClassUtils.getClass(controllerFullName);
//類是否有controller注解
if (controllerClass.isAnnotationPresent(Controller.class)) {
//找到controller類中標明RequestMapping注解的所有方法
Method[] methods = MethodUtils.getMethodsWithAnnotation(controllerClass, RequestMapping.class, true, true);
for (Method method : methods) {
//獲得注解上的路徑
String path = method.getAnnotation(RequestMapping.class).value();
//路徑為key,保存
classMappingMap.put(path,new ClassMapping(controllerClass,method));
}
}
}
} catch (URISyntaxException | ClassNotFoundException e) {
e.printStackTrace();
}
return classMappingMap;
}
public static void main(String[] args) {
new Configuration().config();
}
}
注解
Controller
package mvc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author stars-one at 2019-08-09 08:50
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
String value() default "";
}
RequestMapping
package mvc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author stars-one at 2019-08-09 08:50
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value() default "";
}