标签:
希望你看了此小随 可以实现自己的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); } } }
这工具类中封装了四个方法,通过这个工具类可给对象指定字段赋值,也可获取对象指定字段值和类型,对对象某个字段又是一个对象的情况,上面的工具类能够很好的处理,如person对象关联了car对象,car对象关联了producer对象,producer对象有name属性,可用ReflectionUtil.get(person, "car.producer.name")获取name属性的值。有这个工具类,可继续写前端控制器了,代码如下
package cn.sm.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 cn.sm.action.Action; import cn.sm.action.ActionResult; import cn.sm.action.ResultContent; import cn.sm.util.CommonUtil; import cn.sm.util.ReflectionUtil; /** * 前端控制器(门面模式[提供用户请求的门面]) * @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(); 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 cn.sm.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 cn.sm.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 cn.sm.action.Action; import cn.sm.action.ActionResult; import cn.sm.action.ResultContent; import cn.sm.action.ResultType; import cn.sm.action.Uploadable; import cn.sm.util.CommonUtil; import cn.sm.util.ReflectionUtil; /** * 前端控制器(门面模式[提供用户请求的门面]) * @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(); 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框架做一应用“Student管理系统”。由于要进行数据库操作,可对操作数据库的JDBC代码进行简单的封装并引入DAO(数据访问对象)模式。DAO(Data Access Object)是为数据库或其他持久化机制提供了抽象接口的对象,在不显露底层持久化方案实现细节的前提,提供各种数据访问操作。实际开发中,应将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程设语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),一个是要解决如何访问数据的问题,二个要解决的是如何用对象封装数据。
数据库资源管理器的代码如下。
package cn.sm.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 = "cn.mysql.jdbc.Driver"; private static final String JDBC_URL = "jdbc:mysql://localhost:3306/sm"; private static final String JDBC_UID = "sm"; 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 cn.sm.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 cn.sm.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 cn.sm.util; import java.io.Serializable; /** * 数据库操作的结果 * @author 微冷的风 * */ 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 cn.sm.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 cn.sm.biz; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import cn.sm.exception.DbSessionException; import cn.sm.util.DbSession; import cn.sm.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实现类对象的创建也该交给工厂完成
业务逻辑对象的工厂类,仍是采用约定优于配置的方式,代码如下。
package cn.sm.biz; import java.util.HashMap; import java.util.Map; import cn.sm.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); } } } }
未完待续··· ··· 这里微冷的风丶 じ 深夜撸码的我丶 べ 深夜念你的我丶
——— 听风讲我与你丶
听风讲MVC丶 —— 一言不合就撸码 (未完待续······)
标签:
原文地址:http://www.cnblogs.com/smbk/p/5828151.html