最近给学生讲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);
}
}
}
}
项目的其他部分,我就不在这里赘述了,运行效果如下图所示。
查看和创建班级页面。
点击班级名称查看学生信息。
点击下一页可以查看下一页的学生信息。
点击修改按钮编辑学生信息。
点击删除按钮删除班级或学生信息(删除班级时如果班级中有学生则无法删除)。