听风讲MVC丶 —— 一言不合就撸码 (未完待续······)
Posted 微冷的風丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了听风讲MVC丶 —— 一言不合就撸码 (未完待续······)相关的知识,希望对你有一定的参考价值。
希望你看了此小随 可以实现自己的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) { 一言不合,CCTV就来IDEADATA了