希望你看了此小隨 可以實現自己的MVC框架 也祝所有的程式員身體健康一切安好 ——久伴深海丶默 1.什麼是前端控制器(font controller)。Java Web中的前端控制器是應用的門面,簡單的說所有的請求都會經過這個前端控制器,由前端控制器根據請求的內容來決定如何處理並將處理的結果返回給瀏 ...
希望你看了此小隨 可以實現自己的MVC框架
也祝所有的程式員身體健康一切安好
——久伴深海丶默
1.什麼是前端控制器(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 cn.sm.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 = "cn.sm.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作為伺服器(使用預設設置),項目的部署名稱為sm,接下來可以瀏覽器地址欄輸入http://localhost:8080/sm/login.do,Tomcat的控制台會輸出cn.sm.action.LoginAction。
寫一個通用的前端控制器 用多態 先定義一個Action介面並定義一個抽象方法,不同的Action子類會對該方法進行重寫,用Action的引用引用不同的Action子類對象,調用子類重寫過的方法,執行不同的行為。
定義Action類的介面
package cn.sm.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 cn.sm.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 cn.sm.action; import cn.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 cn.sm.action; /** * Action執行結果類型 * @author 微冷的風 * */ public enum ResultType { // 重定向 Redirect, //轉發 Forward, //非同步請求 Ajax, // 數據流 Stream, // 跳轉到向下一個控制器 Chain, //重定向到下一個控制器 RedirectChain }
再寫一個工具類來封裝常用的工具方法
package cn.sm.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 cn.sm.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 cn.sm.action.Action; import cn.sm.action.ActionResult; import cn.sm.action.ResultContent; import cn.sm.action.ResultType; @WebServlet("*.do") public class FrontController extends HttpServlet { private static final long serialVersionUID = 1L; private static final String DEFAULT_PACKAGE_NAME = "cn.sm.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); } } }
在前端控制器中設置的幾個常量(預設的Action類的包名首碼、預設的Action類的類名尾碼以及預設的JSP文件的路徑)算硬代碼,可以將其看作一種約定,約定好Action類的名字和路徑,JSP頁面的名字和路徑可以省去很多的配置,甚至可以做到零配置,叫做約定優於配置(CoC,Convenient over Configuration)。符合約定的部分可以省去配置,不合符約定的部分用配置文件或者註解加以說明。繼續修改前端控制器,如下。
package cn.sm.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 cn.sm.action.Action; import cn.sm.action.ActionResult; import cn.sm.action.ResultContent; import cn.sm.util.CommonUtil; /** * 前端控制器(門面模式[提供用戶請求的門面]) * @author 微冷的風 * */ @WebServlet(urlPatterns = { "*.do" }, loadOnStartup = 0, initParams = { @WebInitParam(name = "packagePrefix", value = "cn.sm.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 = "cn.sm.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類的完全限定名就是cn.sm.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 cn.sm.util; public interface TypeConverter { public Object convert(Class<?> elemType, String value) throws Exception; }
package cn.sm.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); } } }
這工具類中封裝了四個方法,通過這個工具類可給對象指定欄位賦值,也可獲取對象指定欄位值和類型,