最近給學生講Java Web,希望他們能夠在學完這部分內容後自己實現一個MVC框架。但是突然發現百度上能搜索到的靠譜的資料並不是很多,有些只是原理沒有代碼實現,有些有代碼實現但是對於初學者來說理解起來還是比較困難,於是決定把自己講自定義MVC框架的內容放在這裏分享給大家,不僅僅是代碼,也有原理和探討。內容會比較長,因爲我打算用遞增的方式講解如何寫一個自定義MVC框架,重點是前端控制器的開發。
先說一下什麼是前端控制器(font controller)。Java Web中的前端控制器是應用的門面,簡單的說所有的請求都會經過這個前端控制器,由前端控制器根據請求的內容來決定如何處理並將處理的結果返回給瀏覽器。這就好比很多公司都有一個前臺,那裏通常站着幾位面貌姣好的美女,你要到這家公司處理任何的業務或者約見任何人都可以跟她們說,她們會根據你要做什麼知會相應的部門或個人來處理,這樣做的好處是顯而易見的,公司內部系統運作可能很複雜,但是這些對於外部的客戶來說應該是透明的,通過前臺,客戶可以獲得他們希望該公司爲其提供的服務而不需要了解公司的內部實現。這裏說的前臺就是公司內部系統的一個門面,它簡化了客戶的操作。前端控制器的理念就是GoF設計模式中門面模式(外觀模式)在Web項目中的實際應用。SUN公司爲Java Web開發定義了兩種模型,Model 1和Model 2。Model 2是基於MVC(Model-View-Controller,模型-視圖-控制)架構模式的,通常將小服務(Servlet)或過濾器(Filter)作爲控制器,其作用是接受用戶請求並獲得模型數據然後跳轉到視圖;將JSP頁面作爲視圖,用來顯示用戶操作的結果;模型當然是POJO(Plain Old Java Object),它是區別於EJB(Enterprise JavaBean)的普通Java對象,不實現任何其他框架的接口也不扮演其他的角色,而是負責承載數據,可以作爲VO(Value Object)或DTO(Data Transfer Object)來使用。當然,如果你對這些概念不熟悉,可以用百度或者維基百科查閱一下,想要深入的瞭解這些內容推薦閱讀大師Martin Fowler的《企業應用架構模式》(英文名:Patterns of Enterprise Application Architecture)。
接下來我們就來編寫一個作爲處理用戶各種請求門面的前端控制器。
package com.lovo.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("*.do")
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PACKAGE_NAME = "com.lovo.action.";// 默認的Action類的包名前綴
private static final String DEFAULT_ACTION_NAME = "Action";// 默認的Action類的類名後綴
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 獲得請求的小服務路徑
String servletPath = req.getServletPath();
// 從servletPath中去掉開頭的斜槓和末尾的.do就是要執行的動作(Action)的名字
int start = 1; // 去掉第一個字符斜槓從第二個字符開始
int end = servletPath.lastIndexOf(".do"); // 找到請求路徑的後綴.do的位置
String actionName = end > start ? servletPath.substring(start, end) + DEFAULT_ACTION_NAME : "";
String actionClassName = DEFAULT_PACKAGE_NAME + actionName.substring(0, 1).toUpperCase() + actionName.substring(1);
// 接下來可以通過反射來創建Action對象並調用
System.out.println(actionClassName);
}
}
上面的FrontController類中用@WebServlet註解對該小服務做了映射,只要是後綴爲.do的請求,都會經過這個小服務,所以它是一個典型的前端控制器(當然,你也可以在web.xml中使用<servlet>和<servlet-mapping>標籤對小服務進行映射,使用註解通常是爲了提升開發效率,但需要注意的是註解也是一種耦合,配置文件在解耦合上肯定是更好的選擇,如果要使用註解,最好是像Spring 3那樣可以基於程序配置應用,此外,使用註解配置Servlet需要你的服務器支持Servlet 3規範)。假設使用Tomcat作爲服務器(使用默認設置),項目的部署名稱爲hw,接下來可以瀏覽器地址欄輸入http://localhost:8080/hw/login.do,Tomcat的控制檯會輸出com.lovo.action.LoginAction。
到這裏我們已經將請求對應到一個處理該請求的Action類的名字,不要着急,我們馬上來解釋什麼是Action,怎麼寫Action。我們可以使用不同的Action類來處理用戶不同的請求,那麼如何在前端控制器中根據不同的請求創建出不同的Action對象呢,相信大家都想到了反射,我們剛纔已經得到了Action類的完全限定名(帶包名的類名),接下來就可以用反射來創建對象,但是稍等,每個Action要執行的處理是不一樣的,怎樣才能寫一個通用的前端控制器呢?答案是多態!我們可以先定義一個Action接口並定義一個抽象方法,不同的Action子類會對該方法進行重寫,這樣的話用Action的引用引用不同的Action子類對象,再調用子類重寫過的方法,那麼就可以執行不同的行爲。想到這一層,我們可以繼續編寫我們的前端控制器。
首先,我們需要定義Action類的接口。
package com.lovo.action;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 處理用戶請求的控制器接口
* @author 駱昊
*
*/
public interface Action {
public ActionResult execute(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException;
}
接口中的execute方法是處理用戶請求的方法,所以它的兩個參數分別是HttpServletRequest和HttpServletResponse對象,到時候我們會在前端控制中通過反射創建Action,並調用execute方法,由於不同的Action子類通過重寫對execute方法給出了不同的實現版本,因此該方法是一個多態方法。execute方法的返回值是一個ActionResult對象,它的實現代碼如下所示。
package com.lovo.action;
/**
* Action執行結果
* @author 駱昊
*
*/
public class ActionResult {
private ResultContent resultContent;
private ResultType resultType;
public ActionResult(ResultContent resultContent) {
this(resultContent, ResultType.Forward);
}
public ActionResult(ResultContent resultContent, ResultType type) {
this.resultContent = resultContent;
this.resultType = type;
}
/**
* 獲得執行結果的內容
*/
public ResultContent getResultContent() {
return resultContent;
}
/**
* 獲得執行結果的類型
*/
public ResultType getResultType() {
return resultType;
}
}
ActionResult類中的ResultContent代表了Action對用戶請求進行處理後得到的內容,它可以存儲一個字符串表示要跳轉或重定向到的資源的URL,它也可以存儲一個對象來保存對用戶請求進行處理後得到的數據(模型),爲了支持Ajax操作,我們可以將此對象處理成JSON格式的字符串。
package com.lovo.action;
import com.google.gson.Gson;
/**
* Action執行結束產生的內容
* @author 駱昊
*
*/
public class ResultContent {
private String url;
private Object obj;
public ResultContent(String url) {
this.url = url;
}
public ResultContent(Object obj) {
this.obj = obj;
}
public String getUrl() {
return url;
}
public String getJson() {
return new Gson().toJson(obj);// 這裏使用了Google的JSON工具類gson
}
}
ActionResult類中的ResultType代表了對用戶請求處理後如何向瀏覽器產生響應,它是一個枚舉類型,代碼如下所示。
package com.lovo.action;
/**
* Action執行結果類型
* @author 駱昊
*
*/
public enum ResultType {
/**
* 重定向
*/
Redirect,
/**
* 轉發
*/
Forward,
/**
* 異步請求
*/
Ajax,
/**
* 數據流
*/
Stream,
/**
* 跳轉到向下一個控制器
*/
Chain,
/**
* 重定向到下一個控制器
*/
RedirectChain
}
稍等,我們還需要一個工具類來封裝常用的工具方法。
package com.lovo.util;
import java.awt.Color;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 通用工具類
* @author 駱昊
*
*/
public final class CommonUtil {
private static final List<String> patterns = new ArrayList<>();
private static final List<TypeConverter> converters = new ArrayList<>();
static {
patterns.add("yyyy-MM-dd");
patterns.add("yyyy-MM-dd HH:mm:ss");
}
private CommonUtil() {
throw new AssertionError();
}
/**
* 將字符串的首字母大寫
*/
public static String capitalize(String str) {
StringBuilder sb = new StringBuilder();
if (str != null && str.length() > 0) {
sb.append(str.substring(0, 1).toUpperCase());
if (str.length() > 1) {
sb.append(str.substring(1));
}
return sb.toString();
}
return str;
}
/**
* 生成隨機顏色
*/
public static Color getRandomColor() {
int r = (int) (Math.random() * 256);
int g = (int) (Math.random() * 256);
int b = (int) (Math.random() * 256);
return new Color(r, g, b);
}
/**
* 添加時間日期樣式
* @param pattern 時間日期樣式
*/
public static void registerDateTimePattern(String pattern) {
patterns.add(pattern);
}
/**
* 取消時間日期樣式
* @param pattern 時間日期樣式
*/
public static void unRegisterDateTimePattern(String pattern) {
patterns.remove(pattern);
}
/**
* 添加類型轉換器
* @param converter 類型轉換器對象
*/
public static void registerTypeConverter(TypeConverter converter) {
converters.add(converter);
}
/**
* 取消類型轉換器
* @param converter 類型轉換器對象
*/
public static void unRegisterTypeConverter(TypeConverter converter) {
converters.remove(converter);
}
/**
* 將字符串轉換成時間日期類型
* @param str 時間日期字符串
*/
public static Date convertStringToDateTime(String str) {
if (str != null) {
for (String pattern : patterns) {
Date date = tryConvertStringToDate(str, pattern);
if (date != null) {
return date;
}
}
}
return null;
}
/**
* 按照指定樣式將時間日期轉換成字符串
* @param date 時間日期對象
* @param pattern 樣式字符串
* @return 時間日期的字符串形式
*/
public static String convertDateTimeToString(Date date, String pattern) {
return new SimpleDateFormat(pattern).format(date);
}
private static Date tryConvertStringToDate(String str, String pattern) {
DateFormat dateFormat = new SimpleDateFormat(pattern);
dateFormat.setLenient(false); // 不允許將不符合樣式的字符串轉換成時間日期
try {
return dateFormat.parse(str);
}
catch (ParseException ex) {
}
return null;
}
/**
* 將字符串值按指定的類型轉換成轉換成對象
* @param elemType 類型
* @param value 字符串值
*/
public static Object changeStringToObject(Class<?> elemType, String value) {
Object tempObj = null;
if(elemType == byte.class || elemType == Byte.class) {
tempObj = Byte.parseByte(value);
}
else if(elemType == short.class || elemType == Short.class) {
tempObj = Short.parseShort(value);
}
else if(elemType == int.class || elemType == Integer.class) {
tempObj = Integer.parseInt(value);
}
else if(elemType == long.class || elemType == Long.class) {
tempObj = Long.parseLong(value);
}
else if(elemType == double.class || elemType == Double.class) {
tempObj = Double.parseDouble(value);
}
else if(elemType == float.class || elemType == Float.class) {
tempObj = Float.parseFloat(value);
}
else if(elemType == boolean.class || elemType == Boolean.class) {
tempObj = Boolean.parseBoolean(value);
}
else if(elemType == java.util.Date.class) {
tempObj = convertStringToDateTime(value);
}
else if(elemType == java.lang.String.class) {
tempObj = value;
}
else {
for(TypeConverter converter : converters) {
try {
tempObj = converter.convert(elemType, value);
if(tempObj != null) {
return tempObj;
}
}
catch (Exception e) {
}
}
}
return tempObj;
}
/**
* 獲取文件後綴名
* @param filename 文件名
* @return 文件的後綴名以.開頭
*/
public static String getFileSuffix(String filename) {
int index = filename.lastIndexOf(".");
return index > 0 ? filename.substring(index) : "";
}
}
定義好Action接口及其相關類後,我們可以繼續改寫寫前端控制器的代碼,如下所示。
package com.lovo.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lovo.action.Action;
import com.lovo.action.ActionResult;
import com.lovo.action.ResultContent;
import com.lovo.action.ResultType;
@WebServlet("*.do")
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PACKAGE_NAME = "com.lovo.action."; // 默認的Action類的包名前綴
private static final String DEFAULT_ACTION_NAME = "Action"; // 默認的Action類的類名後綴
private static final String DEFAULT_JSP_PATH = "/WEB-INF/jsp"; // 默認的JSP文件的路徑
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contextPath = req.getContextPath() + "/";
// 獲得請求的小服務路徑
String servletPath = req.getServletPath();
// 從servletPath中去掉開頭的斜槓和末尾的.do就是要執行的動作(Action)的名字
int start = 1; // 去掉第一個字符斜槓從第二個字符開始
int end = servletPath.lastIndexOf(".do"); // 找到請求路徑的後綴.do的位置
String actionName = end > start ? servletPath.substring(start, end) + DEFAULT_ACTION_NAME : "";
String actionClassName = DEFAULT_PACKAGE_NAME + actionName.substring(0, 1).toUpperCase() + actionName.substring(1);
try {
// 通過反射來創建Action對象並調用
Action action = (Action) Class.forName(actionClassName).newInstance();
// 執行多態方法execute得到ActionResult
ActionResult result = action.execute(req, resp);
ResultType resultType = result.getResultType();// 結果類型
ResultContent resultContent = result.getResultContent();// 結果內容
// 根據ResultType決定如何處理
switch (resultType) {
case Forward: // 跳轉
req.getRequestDispatcher(
DEFAULT_JSP_PATH + resultContent.getUrl()).forward(req,
resp);
break;
case Redirect: // 重定向
resp.sendRedirect(resultContent.getUrl());
break;
case Ajax: // Ajax
PrintWriter pw = resp.getWriter();
pw.println(resultContent.getJson());
pw.close();
break;
case Chain:
req.getRequestDispatcher(contextPath + resultContent.getUrl())
.forward(req, resp);
break;
case RedirectChain:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
default:
}
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e);
}
}
}
迄今爲止,我們還沒有編寫任何的配置文件,但是大家可能已經注意到前端控制器中的硬代碼(hard code)了。我們在前端控制器中設置的幾個常量(默認的Action類的包名前綴、默認的Action類的類名後綴以及默認的JSP文件的路徑)都算是硬代碼,但是我們也可以將其視爲一種約定,我們約定好Action類的名字和路徑,JSP頁面的名字和路徑就可以省去很多的配置,甚至可以做到零配置,這種理念並不新鮮,它叫做約定優於配置(CoC,Convenient over Configuration)。當然,對於符合約定的部分我們可以省去配置,對於不合符約定的部分可以用配置文件或者註解加以說明。繼續修改我們的前端控制器,代碼如下所示。
package com.lovo.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lovo.action.Action;
import com.lovo.action.ActionResult;
import com.lovo.action.ResultContent;
import com.lovo.util.CommonUtil;
/**
* 前端控制器(門面模式[提供用戶請求的門面])
* @author 駱昊
*
*/
@WebServlet(urlPatterns = { "*.do" }, loadOnStartup = 0,
initParams = {
@WebInitParam(name = "packagePrefix", value = "com.lovo.action."),
@WebInitParam(name = "jspPrefix", value = "/WEB-INF/jsp/"),
@WebInitParam(name = "actionSuffix", value = "Action")
}
)
@MultipartConfig
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PACKAGE_NAME = "com.lovo.action.";
private static final String DEFAULT_JSP_PATH = "/WEB-INF/content/";
private static final String DEFAULT_ACTION_NAME = "Action";
private String packagePrefix = null; // 包名的前綴
private String jspPrefix = null; // JSP頁面路徑的前綴
private String actionSuffix = null; // Action類名的後綴
@Override
public void init(ServletConfig config) throws ServletException {
String initParam = config.getInitParameter("packagePrefix");
packagePrefix = initParam != null ? initParam : DEFAULT_PACKAGE_NAME;
initParam = config.getInitParameter("jspPrefix");
jspPrefix = initParam != null ? initParam : DEFAULT_JSP_PATH;
initParam = config.getInitParameter("actionSuffix");
actionSuffix = initParam != null ? initParam : DEFAULT_ACTION_NAME;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contextPath = req.getContextPath() + "/";
String servletPath = req.getServletPath();
try {
Action action = (Action) Class.forName(getFullActionName(servletPath)).newInstance();
ActionResult actionResult = action.execute(req, resp);
ResultContent resultContent = actionResult.getResultContent();
switch(actionResult.getResultType()) {
case Redirect:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
case Forward:
req.getRequestDispatcher(getFullJspPath(servletPath) + resultContent.getUrl())
.forward(req, resp);
break;
case Ajax:
PrintWriter pw = resp.getWriter();
pw.println(resultContent.getJson());
pw.close();
break;
case Chain:
req.getRequestDispatcher(contextPath + resultContent.getUrl())
.forward(req, resp);
break;
case RedirectChain:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
default:
}
}
catch (Exception e) {
e.printStackTrace();
resp.sendRedirect("error.html");
}
}
// 根據請求的小服務路徑獲得對應的Action類的名字
private String getFullActionName(String servletPath) {
int start = servletPath.lastIndexOf("/") + 1;
int end = servletPath.lastIndexOf(".do");
return packagePrefix + getSubPackage(servletPath) + CommonUtil.capitalize(servletPath.substring(start, end)) + actionSuffix;
}
// 根據請求的小服務路徑獲得對應的完整的JSP頁面路徑
private String getFullJspPath(String servletPath) {
return jspPrefix + getSubJspPath(servletPath);
}
// 根據請求的小服務路徑獲得子級包名
private String getSubPackage(String servletPath) {
return getSubJspPath(servletPath).replaceAll("\\/", ".");
}
// 根據請求的小服務路徑獲得JSP頁面的子級路徑
private String getSubJspPath(String servletPath) {
int start = 1;
int end = servletPath.lastIndexOf("/");
return end > start ? servletPath.substring(start, end > 0 ? end + 1 : 0) : "";
}
}
這一次,我們讓前端控制器在解析用戶請求的小服務路徑時,將請求路徑和Action類的包以及JSP頁面的路徑對應起來,也就是說,如果用戶請求的小服務路徑是/user/order/save.do,那麼對應的Action類的完全限定名就是com.lovo.action.user.order.SaveAction,如果需要跳轉到ok.jsp頁面,那麼JSP頁面的默認路徑是/WEB-INF/jsp/user/order/ok.jsp。這樣做才能滿足對項目模塊進行劃分的要求,而不是把所有的Action類都放在一個包中,把所有的JSP頁面都放在一個路徑下。
然而,前端控制器的任務到這裏還遠遠沒有完成,如果每個Action都要寫若干的req.getParameter(String)從請求中獲得請求參數再組裝對象而後調用業務邏輯層的代碼,這樣Action實現類中就會有很多重複的樣板代碼,代碼有很多種壞味道,重複是最壞的一種!解決這一問題的方案仍然是反射,通過反射我們可以將Action需要的參數注入到Action類中。需要注意的是,反射雖然可以幫助我們寫出通用性很強的代碼,但是反射的開銷也是不可忽視的,我們的自定義MVC框架還有很多可以優化的地方,不過先放放,先解決請求參數的注入問題。
先封裝一個反射的工具類,代碼如下所示。
package com.lovo.util;
public interface TypeConverter {
public Object convert(Class<?> elemType, String value) throws Exception;
}
package com.lovo.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
/**
* 反射工具類
* @author 駱昊
*
*/
public class ReflectionUtil {
private ReflectionUtil() {
throw new AssertionError();
}
/**
* 根據字段名查找字段的類型
* @param target 目標對象
* @param fieldName 字段名
* @return 字段的類型
*/
public static Class<?> getFieldType(Object target, String fieldName) {
Class<?> clazz = target.getClass();
String[] fs = fieldName.split("\\.");
try {
for(int i = 0; i < fs.length - 1; i++) {
Field f = clazz.getDeclaredField(fs[i]);
target = f.getType().newInstance();
clazz = target.getClass();
}
return clazz.getDeclaredField(fs[fs.length - 1]).getType();
}
catch(Exception e) {
// throw new RuntimeException(e);
}
return null;
}
/**
* 獲取對象所有字段的名字
* @param obj 目標對象
* @return 字段名字的數組
*/
public static String[] getFieldNames(Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
List<String> fieldNames = new ArrayList<>();
for(int i = 0; i < fields.length; i++) {
if((fields[i].getModifiers() & Modifier.STATIC) == 0) {
fieldNames.add(fields[i].getName());
}
}
return fieldNames.toArray(new String[fieldNames.size()]);
}
/**
* 通過反射取對象指定字段(屬性)的值
* @param target 目標對象
* @param fieldName 字段的名字
* @throws 如果取不到對象指定字段的值則拋出異常
* @return 字段的值
*/
public static Object getValue(Object target, String fieldName) {
Class<?> clazz = target.getClass();
String[] fs = fieldName.split("\\.");
try {
for(int i = 0; i < fs.length - 1; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(true);
target = f.get(target);
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length - 1]);
f.setAccessible(true);
return f.get(target);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 通過反射給對象的指定字段賦值
* @param target 目標對象
* @param fieldName 字段的名稱
* @param value 值
*/
public static void setValue(Object target, String fieldName, Object value) {
Class<?> clazz = target.getClass();
String[] fs = fieldName.split("\\.");
try {
for(int i = 0; i < fs.length - 1; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(true);
Object val = f.get(target);
if(val == null) {
Constructor<?> c = f.getType().getDeclaredConstructor();
c.setAccessible(true);
val = c.newInstance();
f.set(target, val);
}
target = val;
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length - 1]);
f.setAccessible(true);
f.set(target, value);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
這個工具類中封裝了四個方法,通過這個工具類可以給對象的指定字段賦值,也可以獲取對象指定字段的值和類型,對於對象的某個字段又是一個對象的情況,上面的工具類也能夠提供很好的處理,例如person對象關聯了car對象,car對象關聯了producer對象,producer對象有name屬性,可以用ReflectionUtil.get(person, "car.producer.name")來獲取name屬性的值。有了這個工具類,我們可以繼續改寫前端控制器了,代碼如下所示。
package com.lovo.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.Enumeration;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lovo.action.Action;
import com.lovo.action.ActionResult;
import com.lovo.action.ResultContent;
import com.lovo.util.CommonUtil;
import com.lovo.util.ReflectionUtil;
/**
* 前端控制器(門面模式[提供用戶請求的門面])
* @author 駱昊
*
*/
@WebServlet(urlPatterns = { "*.do" }, loadOnStartup = 0,
initParams = {
@WebInitParam(name = "packagePrefix", value = "com.lovo.action."),
@WebInitParam(name = "jspPrefix", value = "/WEB-INF/jsp/"),
@WebInitParam(name = "actionSuffix", value = "Action")
}
)
@MultipartConfig
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PACKAGE_NAME = "com.lovo.action.";
private static final String DEFAULT_JSP_PATH = "/WEB-INF/content/";
private static final String DEFAULT_ACTION_NAME = "Action";
private String packagePrefix = null; // 包名的前綴
private String jspPrefix = null; // JSP頁面路徑的前綴
private String actionSuffix = null; // Action類名的後綴
@Override
public void init(ServletConfig config) throws ServletException {
String initParam = config.getInitParameter("packagePrefix");
packagePrefix = initParam != null ? initParam : DEFAULT_PACKAGE_NAME;
initParam = config.getInitParameter("jspPrefix");
jspPrefix = initParam != null ? initParam : DEFAULT_JSP_PATH;
initParam = config.getInitParameter("actionSuffix");
actionSuffix = initParam != null ? initParam : DEFAULT_ACTION_NAME;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contextPath = req.getContextPath() + "/";
String servletPath = req.getServletPath();
try {
Action action = (Action) Class.forName(getFullActionName(servletPath)).newInstance();
injectProperties(action, req);// 向Action對象中注入請求參數
ActionResult actionResult = action.execute(req, resp);
ResultContent resultContent = actionResult.getResultContent();
switch (actionResult.getResultType()) {
case Redirect:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
case Forward:
req.getRequestDispatcher(
getFullJspPath(servletPath) + resultContent.getUrl())
.forward(req, resp);
break;
case Ajax:
PrintWriter pw = resp.getWriter();
pw.println(resultContent.getJson());
pw.close();
break;
case Chain:
req.getRequestDispatcher(contextPath + resultContent.getUrl())
.forward(req, resp);
break;
case RedirectChain:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
default:
}
}
catch (Exception e) {
e.printStackTrace();
resp.sendRedirect("error.html");
}
}
// 根據請求的小服務路徑獲得對應的Action類的名字
private String getFullActionName(String servletPath) {
int start = servletPath.lastIndexOf("/") + 1;
int end = servletPath.lastIndexOf(".do");
return packagePrefix + getSubPackage(servletPath) + CommonUtil.capitalize(servletPath.substring(start, end)) + actionSuffix;
}
// 根據請求的小服務路徑獲得對應的完整的JSP頁面路徑
private String getFullJspPath(String servletPath) {
return jspPrefix + getSubJspPath(servletPath);
}
// 根據請求的小服務路徑獲得子級包名
private String getSubPackage(String servletPath) {
return getSubJspPath(servletPath).replaceAll("\\/", ".");
}
// 根據請求的小服務路徑獲得JSP頁面的子級路徑
private String getSubJspPath(String servletPath) {
int start = 1;
int end = servletPath.lastIndexOf("/");
return end > start ? servletPath.substring(start, end > 0 ? end + 1 : 0) : "";
}
// 向Action對象中注入屬性
private void injectProperties(Action action, HttpServletRequest req) throws Exception {
Enumeration<String> paramNamesEnum = req.getParameterNames();
while(paramNamesEnum.hasMoreElements()) {
String paramName = paramNamesEnum.nextElement();
Class<?> fieldType = ReflectionUtil.getFieldType(action, paramName.replaceAll("\\[|\\]", ""));
if(fieldType != null) {
Object paramValue = null;
if(fieldType.isArray()) { // 如果屬性是數組類型
Class<?> elemType = fieldType.getComponentType(); // 獲得數組元素類型
String[] values = req.getParameterValues(paramName);
paramValue = Array.newInstance(elemType, values.length); // 通過反射創建數組對象
for(int i = 0; i < values.length; i++) {
Object tempObj = CommonUtil.changeStringToObject(elemType, values[i]);
Array.set(paramValue, i, tempObj);
}
}
else { // 非數組類型的屬性
paramValue = CommonUtil.changeStringToObject(fieldType, req.getParameter(paramName));
}
ReflectionUtil.setValue(action, paramName.replaceAll("\\[|\\]", ""), paramValue);
}
}
}
}
到這裏,我們的前端控制器還不能夠支持文件上傳。Java Web應用的文件上傳在Servlet 3.0規範以前一直是個讓人鬧心的東西,需要自己編寫代碼在Servlet中通過解析輸入流來找到上傳文件的數據,雖然有第三方工具(如commons-fileupload)經封裝了這些操作,但是一個Web規範中居然沒有文件上傳的API難道不是很搞笑嗎?好在Servlet 3.0中有了@MultiConfig註解可以爲Servlet提供文件上傳的支持,而且通過請求對象的getPart或getParts方法可以獲得上傳的數據,這樣處理文件上傳就相當方便了。
我們先定義一個接口來讓Action支持文件上傳,凡是要處理文件上傳的Action類都要實現這個接口,然後我們通過接口注入的方式,將上傳文件的數據以及上傳文件的文件名注入到Action類中,這樣Action類中就可以直接處理上傳的文件了。
支持文件上傳的接口代碼如下所示。
package com.lovo.action;
import javax.servlet.http.Part;
/**
* 支持文件上傳的接口
* @author 駱昊
*
*/
public interface Uploadable {
/**
* 設置文件名
* @param filenames 文件名的數組
*/
public void setFilenames(String[] filenames);
/**
* 設置上傳的附件
* @param parts 附件的數組
*/
public void setParts(Part[] parts);
}
修改後的前端控制器
package com.lovo.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import com.lovo.action.Action;
import com.lovo.action.ActionResult;
import com.lovo.action.ResultContent;
import com.lovo.action.ResultType;
import com.lovo.action.Uploadable;
import com.lovo.util.CommonUtil;
import com.lovo.util.ReflectionUtil;
/**
* 前端控制器(門面模式[提供用戶請求的門面])
* @author 駱昊
*
*/
@WebServlet(urlPatterns = { "*.do" }, loadOnStartup = 0,
initParams = {
@WebInitParam(name = "packagePrefix", value = "com.lovo.action."),
@WebInitParam(name = "jspPrefix", value = "/WEB-INF/jsp/"),
@WebInitParam(name = "actionSuffix", value = "Action")
}
)
@MultipartConfig
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PACKAGE_NAME = "com.lovo.action.";
private static final String DEFAULT_JSP_PATH = "/WEB-INF/content/";
private static final String DEFAULT_ACTION_NAME = "Action";
private String packagePrefix = null; // 包名的前綴
private String jspPrefix = null; // JSP頁面路徑的前綴
private String actionSuffix = null; // Action類名的後綴
@Override
public void init(ServletConfig config) throws ServletException {
String initParam = config.getInitParameter("packagePrefix");
packagePrefix = initParam != null ? initParam : DEFAULT_PACKAGE_NAME;
initParam = config.getInitParameter("jspPrefix");
jspPrefix = initParam != null ? initParam : DEFAULT_JSP_PATH;
initParam = config.getInitParameter("actionSuffix");
actionSuffix = initParam != null ? initParam : DEFAULT_ACTION_NAME;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contextPath = req.getContextPath() + "/";
String servletPath = req.getServletPath();
try {
Action action = (Action) Class.forName(getFullActionName(servletPath)).newInstance();
try {
injectProperties(action, req);
} catch (Exception e) {
}
if(action instanceof Uploadable) { // 通過接口向實現了接口的類注入屬性(接口注入)
List<Part> fileparts = new ArrayList<>();
List<String> filenames = new ArrayList<>();
for(Part part : req.getParts()) {
String cd = part.getHeader("Content-Disposition");
if(cd.indexOf("filename") >= 0) {
fileparts.add(part);
filenames.add(cd.substring(cd.lastIndexOf("=") + 1).replaceAll("\\\"", ""));
}
}
((Uploadable) action).setParts(fileparts.toArray(new Part[fileparts.size()]));
((Uploadable) action).setFilenames(filenames.toArray(new String[filenames.size()]));
}
ActionResult actionResult = action.execute(req, resp);
if(actionResult != null) {
ResultContent resultContent = actionResult.getResultContent();
ResultType resultType = actionResult.getResultType();
switch(resultType) {
case Redirect:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
case Forward:
req.getRequestDispatcher(getFullJspPath(servletPath) + resultContent.getUrl()).forward(req, resp);
break;
case Ajax:
PrintWriter pw = resp.getWriter();
pw.println(resultContent.getJson());
pw.close();
break;
case Chain:
req.getRequestDispatcher(contextPath + resultContent.getUrl()).forward(req, resp);
break;
case RedirectChain:
resp.sendRedirect(contextPath + resultContent.getUrl());
break;
default:
}
}
}
catch (Exception e) {
e.printStackTrace();
resp.sendRedirect("error.html");
}
}
// 根據請求的小服務路徑獲得對應的Action類的名字
private String getFullActionName(String servletPath) {
int start = servletPath.lastIndexOf("/") + 1;
int end = servletPath.lastIndexOf(".do");
return packagePrefix + getSubPackage(servletPath) + CommonUtil.capitalize(servletPath.substring(start, end)) + actionSuffix;
}
// 根據請求的小服務路徑獲得對應的完整的JSP頁面路徑
private String getFullJspPath(String servletPath) {
return jspPrefix + getSubJspPath(servletPath);
}
// 根據請求的小服務路徑獲得子級包名
private String getSubPackage(String servletPath) {
return getSubJspPath(servletPath).replaceAll("\\/", ".");
}
// 根據請求的小服務路徑獲得JSP頁面的子級路徑
private String getSubJspPath(String servletPath) {
int start = 1;
int end = servletPath.lastIndexOf("/");
return end > start ? servletPath.substring(start, end > 0 ? end + 1 : 0) : "";
}
// 向Action對象中注入屬性
private void injectProperties(Action action, HttpServletRequest req) throws Exception {
Enumeration<String> paramNamesEnum = req.getParameterNames();
while(paramNamesEnum.hasMoreElements()) {
String paramName = paramNamesEnum.nextElement();
Class<?> fieldType = ReflectionUtil.getFieldType(action, paramName.replaceAll("\\[|\\]", ""));
if(fieldType != null) {
Object paramValue = null;
if(fieldType.isArray()) { // 如果屬性是數組類型
Class<?> elemType = fieldType.getComponentType(); // 獲得數組元素類型
String[] values = req.getParameterValues(paramName);
paramValue = Array.newInstance(elemType, values.length); // 通過反射創建數組對象
for(int i = 0; i < values.length; i++) {
Object tempObj = CommonUtil.changeStringToObject(elemType, values[i]);
Array.set(paramValue, i, tempObj);
}
}
else { // 非數組類型的屬性
paramValue = CommonUtil.changeStringToObject(fieldType, req.getParameter(paramName));
}
ReflectionUtil.setValue(action, paramName.replaceAll("\\[|\\]", ""), paramValue);
}
}
}
}
到這裏,我們的前端控制器已經基本可用了,接下來用我們自定義的MVC框架做一個小應用“班級學生管理系統”。由於要進行數據庫操作,我們可以對操作數據庫的JDBC代碼進行一個簡單的封裝並引入DAO(數據訪問對象)模式。DAO(Data Access Object)顧名思義是一個爲數據庫或其他持久化機制提供了抽象接口的對象,在不暴露底層持久化方案實現細節的前提下提供了各種數據訪問操作。在實際的開發中,應該將所有對數據源的訪問操作進行抽象化後封裝在一個公共API中。用程序設計語言來說,就是建立一個接口,接口中定義了此應用程序中將會用到的所有事務方法。在這個應用程序中,當需要和數據源進行交互的時候則使用這個接口,並且編寫一個單獨的類來實現這個接口,在邏輯上該類對應一個特定的數據存儲。DAO模式實際上包含了兩個模式,一是Data Accessor(數據訪問器),二是Data Object(數據對象),前者要解決如何訪問數據的問題,而後者要解決的是如何用對象封裝數據。
數據庫資源管理器的代碼如下所示。
package com.lovo.util;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* 數據庫資源管理器
* @author 駱昊
*
*/
public class DbResourceManager {
// 最好的做法是將配置保存到配置文件中(可以用properteis文件或XML文件)
private static final String JDBC_DRV = "com.mysql.jdbc.Driver";
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/hw";
private static final String JDBC_UID = "root";
private static final String JDBC_PWD = "123456";
private static Driver driver = null;
private static Properties info = new Properties();
private DbResourceManager() {
throw new AssertionError();
}
static {
try {
loadDriver(); // 通過靜態代碼塊加載數據庫驅動
info.setProperty("user", JDBC_UID);
info.setProperty("password", JDBC_PWD);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void setDriver(Driver _driver) {
driver = _driver;
}
// 加載驅動程序
private static void loadDriver() throws Exception {
driver = (Driver) Class.forName(JDBC_DRV).newInstance();
DriverManager.registerDriver(driver);
}
/**
* 打開連接
* @return 連接對象
* @throws Exception 無法加載驅動或無法建立連接時將拋出異常
*/
public static Connection getConnection() throws Exception {
if(driver == null) {
loadDriver();
}
return driver.connect(JDBC_URL, info);
}
/**
* 關閉遊標
*/
public static void close(ResultSet rs) {
try {
if(rs != null && !rs.isClosed()) {
rs.close();
}
}
catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 關閉語句
*/
public static void close(Statement stmt) throws SQLException {
try {
if(stmt != null && !stmt.isClosed()) {
stmt.close();
}
}
catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 關閉連接
*/
public static void close(Connection con) {
try {
if(con != null && !con.isClosed()) {
con.close();
}
}
catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 註銷驅動
* @throws SQLException
*/
public static void unloadDriver() throws SQLException {
if(driver != null) {
DriverManager.deregisterDriver(driver);
driver = null;
}
}
}
數據庫會話的代碼如下所示,封裝了執行查詢和執行增刪改的方法以減少重複代碼。
package com.lovo.util;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.io.Serializable;
import com.lovo.exception.DbSessionException;
/**
* 數據庫會話(尚未提供批處理操作)
* @author 駱昊
*
*/
public class DbSession {
private Connection con = null;
private PreparedStatement stmt = null;
private ResultSet rs = null;
/**
* 開啓數據庫會話
*/
public void open() {
if(con == null) {
try {
con = DbResourceManager.getConnection();
}
catch (Exception e) {
throw new DbSessionException("創建會話失敗", e);
}
}
}
/**
* 獲得與數據庫會話綁定的連接
*/
public Connection getConnection() {
return con;
}
/**
* 關閉數據庫會話
*/
public void close() {
try {
DbResourceManager.close(rs);
rs = null;
DbResourceManager.close(stmt);
stmt = null;
DbResourceManager.close(con);
con = null;
}
catch (SQLException e) {
throw new DbSessionException("關閉會話失敗", e);
}
}
/**
* 開啓事務
* @throws 無法開啓事務時將拋出異常
*/
public void beginTx() {
try {
if(con != null && !con.isClosed()) {
con.setAutoCommit(false);
}
}
catch (SQLException e) {
throw new RuntimeException("開啓事務失敗", e);
}
}
/**
* 提交事務
* @throws 無法提交事務時將拋出異常
*/
public void commitTx() {
try {
if(con != null && !con.isClosed()) {
con.commit();
}
}
catch (SQLException e) {
throw new DbSessionException("提交事務失敗", e);
}
}
/**
* 回滾事務
* @throws 無法回滾事務時將拋出異常
*/
public void rollbackTx() {
try {
if(con != null && !con.isClosed()) {
con.rollback();
}
}
catch (SQLException e) {
throw new DbSessionException("回滾事務失敗", e);
}
}
/**
* 執行更新語句
* @param sql SQL語句
* @param params 替換SQL語句中佔位符的參數
* @return 多少行受影響
*/
public DbResult executeUpdate(String sql, Object... params) {
try {
boolean isInsert = sql.trim().startsWith("insert");
if(isInsert) {
stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
}
else {
stmt = con.prepareStatement(sql);
}
for(int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
int affectedRows = stmt.executeUpdate();
Serializable generatedKey = null;
if(isInsert) {
rs = stmt.getGeneratedKeys();
generatedKey = rs.next()? (Serializable) rs.getObject(1) : generatedKey;
}
return new DbResult(affectedRows, generatedKey);
}
catch (SQLException e) {
throw new DbSessionException(e);
}
}
/**
* 執行查詢語句
* @param sql SQL語句
* @param params 替換SQL語句中佔位符的參數
* @return 結果集(遊標)
*/
public ResultSet executeQuery(String sql, Object... params) {
try {
stmt = con.prepareStatement(sql);
for(int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
rs = stmt.executeQuery();
}
catch (SQLException e) {
throw new DbSessionException(e);
}
return rs;
}
}
package com.lovo.util;
import java.io.Serializable;
/**
* 數據庫操作的結果
* @author Hao
*
*/
public class DbResult {
private int affectedRows; // 受影響的行數
private Serializable generatedKey; // 生成的主鍵
public DbResult(int affectedRows, Serializable generatedKey) {
this.affectedRows = affectedRows;
this.generatedKey = generatedKey;
}
public int getAffectedRows() {
return affectedRows;
}
public Serializable getGeneratedKey() {
return generatedKey;
}
}
數據庫會話工廠的代碼如下所示,使用ThreadLocal將數據庫會話和線程綁定。
package com.lovo.util;
/**
* 數據庫會話工廠
* @author 駱昊
*
*/
public class DbSessionFactory {
private static final ThreadLocal<DbSession> threadLocal = new ThreadLocal<DbSession>();
private DbSessionFactory() {
throw new AssertionError();
}
/**
* 打開會話
* @return DbSession對象
*/
public static DbSession openSession() {
DbSession session = threadLocal.get();
if(session == null) {
session = new DbSession();
threadLocal.set(session);
}
session.open();
return session;
}
/**
* 關閉會話
*/
public static void closeSession() {
DbSession session = threadLocal.get();
threadLocal.set(null);
if(session != null) {
session.close();
}
}
}
如果使用基於事務腳本模式的分層開發,可以在業務邏輯層設置事務的邊界,但是這會導致所有的業務邏輯方法中都要處理事務,爲此可以使用代理模式爲業務邏輯對象生成代理,如果業務邏輯層有設計接口,那麼可以使用Java中的動態代理來完成業務邏輯代理對象的創建,代碼如下所示。
package com.lovo.biz;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.lovo.exception.DbSessionException;
import com.lovo.util.DbSession;
import com.lovo.util.DbSessionFactory;
/**
* 業務邏輯代理對象(對非get開頭的方法都啓用事務)
* @author 駱昊
*
*/
public class ServiceProxy implements InvocationHandler {
private Object target;
public ServiceProxy(Object target) {
this.target = target;
}
public static Object getProxyInstance(Object target) {
Class<?> clazz = target.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), new ServiceProxy(target));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object retValue = null;
DbSession session = DbSessionFactory.openSession();
boolean isTxNeeded = !method.getName().startsWith("get");
try {
if(isTxNeeded) session.beginTx();
retValue = method.invoke(target, args);
if(isTxNeeded) session.commitTx();
}
catch(DbSessionException ex) {
ex.printStackTrace();
if(isTxNeeded) session.rollbackTx();
}
finally {
DbSessionFactory.closeSession();
}
return retValue;
}
}
可以使用工廠類來創建業務邏輯對象,其實DAO實現類對象的創建也應該交給工廠來完成,當然,對於那些熟練使用Spring框架的Java開發者來說,這些東西Spring都幫你做好了,你只需要做出一些配置即可,Spring的理念是“不重複發明輪子”。我們上面的很多代碼都是在重複的發明輪子,但是作爲一個案例,這個例子卻充分運用了多態、反射、接口回調、接口注入、代理模式、工廠模式、單例模式、ThreadLocal等諸多知識點。如果你已經對Java有了一定程度的瞭解和認識,想驗證自己的水平,真的可以嘗試自己寫一個MVC框架。
業務邏輯對象的工廠類,仍然是採用約定優於配置的方式,代碼如下所示。
package com.lovo.biz;
import java.util.HashMap;
import java.util.Map;
import com.lovo.util.CommonUtil;
/**
* 創建業務邏輯代理對象的工廠 (登記式單例模式)
* @author 駱昊
*
*/
public class ServiceFactory {
private static final String DEFAULT_IMPL_PACKAGE_NAME = "impl";
private static Map<Class<?>, Object> map = new HashMap<>();
/**
* 工廠方法
* @param type 業務邏輯對象的類型
* @return 業務邏輯對象的代理對象
*/
public static synchronized Object factory(Class<?> type) {
if(map.containsKey(type)) {
return map.get(type);
}
else {
try {
Object serviceObj = Class.forName(
type.getPackage().getName() + "." + DEFAULT_IMPL_PACKAGE_NAME + "."
+ type.getSimpleName() + CommonUtil.capitalize(DEFAULT_IMPL_PACKAGE_NAME)).newInstance();
map.put(type, ServiceProxy.getProxyInstance(serviceObj));
return serviceObj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
項目的其他部分,我就不在這裏贅述了,運行效果如下圖所示。
查看和創建班級頁面。
點擊班級名稱查看學生信息。
點擊下一頁可以查看下一頁的學生信息。
點擊修改按鈕編輯學生信息。
點擊刪除按鈕刪除班級或學生信息(刪除班級時如果班級中有學生則無法刪除)。