最近给学生讲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.";
-     private static final String DEFAULT_ACTION_NAME = "Action";
-   
-     @Override  
-     protected void service(HttpServletRequest req, HttpServletResponse resp)  
-             throws ServletException, IOException {  
-         
-         String servletPath = req.getServletPath();  
-         
-         int start = 1;  
-         int end = servletPath.lastIndexOf(".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);  
-         
-         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;  
-   
- 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;  
-   
- 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;  
-   
- 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);
-     }  
- }  
 
ActionResult类中的ResultType代表了对用户请求处理后如何向浏览器产生响应,它是一个枚举类型,代码如下所示。
- package com.lovo.action;  
-   
- public enum ResultType {  
-     
-     Redirect,   
-     
-     Forward,   
-     
-     Ajax,  
-     
-     Stream,  
-     
-     Chain,  
-     
-     RedirectChain  
- }  
 
稍等,我们还需要一个工具类来封装常用的工具方法。
定义好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.";      
-     private static final String DEFAULT_ACTION_NAME = "Action";         
-     private static final String DEFAULT_JSP_PATH = "/WEB-INF/jsp";          
-   
-     @Override  
-     protected void service(HttpServletRequest req, HttpServletResponse resp)  
-             throws ServletException, IOException {  
-         String contextPath = req.getContextPath() + "/";  
-         
-         String servletPath = req.getServletPath();  
-         
-         int start = 1;  
-         int end = servletPath.lastIndexOf(".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) Class.forName(actionClassName).newInstance();  
-             
-             ActionResult result = action.execute(req, resp);  
-             ResultType resultType = result.getResultType();
-             ResultContent resultContent = result.getResultContent();
-             
-             switch (resultType) {  
-             case Forward: 
-                 req.getRequestDispatcher(  
-                         DEFAULT_JSP_PATH + resultContent.getUrl()).forward(req,  
-                         resp);  
-                 break;  
-             case Redirect: 
-                 resp.sendRedirect(resultContent.getUrl());  
-                 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();  
-             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;  
-   
- @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;            
-     private String actionSuffix = null;         
-       
-     @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");  
-         }  
-     }  
-       
-     
-     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;  
-     }  
-       
-     
-     private String getFullJspPath(String servletPath) {  
-         return jspPrefix + getSubJspPath(servletPath);  
-     }  
-       
-     
-     private String getSubPackage(String servletPath) {  
-         return getSubJspPath(servletPath).replaceAll("\\/", ".");  
-     }  
-       
-     
-     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;  
- }  
 
这个工具类中封装了四个方法,通过这个工具类可以给对象的指定字段赋值,也可以获取对
象指定字段的值和类型,对于对象的某个字段又是一个对象的情况,上面的工具类也能够提供很好的处理,例如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;  
-   
- @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;            
-     private String actionSuffix = null;         
-       
-     @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);
-             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");  
-         }  
-     }  
-       
-     
-     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;  
-     }  
-       
-     
-     private String getFullJspPath(String servletPath) {  
-         return jspPrefix + getSubJspPath(servletPath);  
-     }  
-       
-     
-     private String getSubPackage(String servletPath) {  
-         return getSubJspPath(servletPath).replaceAll("\\/", ".");  
-     }  
-       
-     
-     private String getSubJspPath(String servletPath) {  
-         int start = 1;  
-         int end = servletPath.lastIndexOf("/");  
-         return end > start ? servletPath.substring(start, end > 0 ? end + 1 : 0) : "";  
-     }  
-   
-     
-     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;  
-   
- public interface Uploadable {  
-       
-     
-     public void setFilenames(String[] filenames);  
-       
-     
-     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;  
-   
- @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;            
-     private String actionSuffix = null;         
-       
-     @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");  
-         }  
-     }  
-       
-     
-     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;  
-     }  
-       
-     
-     private String getFullJspPath(String servletPath) {  
-         return jspPrefix + getSubJspPath(servletPath);  
-     }  
-       
-     
-     private String getSubPackage(String servletPath) {  
-         return getSubJspPath(servletPath).replaceAll("\\/", ".");  
-     }  
-       
-     
-     private String getSubJspPath(String servletPath) {  
-         int start = 1;  
-         int end = servletPath.lastIndexOf("/");  
-         return end > start ? servletPath.substring(start, end > 0 ? end + 1 : 0) : "";  
-     }  
-   
-     
-     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.PreparedStatement;  
- import java.sql.ResultSet;  
- import java.sql.SQLException;  
- import java.sql.Statement;  
- import java.io.Serializable;  
-   
- import com.lovo.exception.DbSessionException;  
-   
- 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);  
-         }  
-     }  
-       
-     
-     public void beginTx() {  
-         try {  
-             if(con != null && !con.isClosed()) {  
-                 con.setAutoCommit(false);  
-             }  
-         }  
-         catch (SQLException e) {  
-             throw new RuntimeException("开启事务失败", e);  
-         }  
-     }  
-       
-     
-     public void commitTx() {  
-         try {  
-             if(con != null && !con.isClosed()) {  
-                 con.commit();  
-             }  
-         }  
-         catch (SQLException e) {  
-             throw new DbSessionException("提交事务失败", e);  
-         }  
-     }  
-       
-     
-     public void rollbackTx() {  
-         try {  
-             if(con != null && !con.isClosed()) {  
-                 con.rollback();  
-             }  
-         }  
-         catch (SQLException e) {  
-             throw new DbSessionException("回滚事务失败", e);  
-         }  
-     }  
-       
-     
-     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);  
-         }  
-     }  
-       
-     
-     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;  
-   
- 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;  
-   
-   
- public class DbSessionFactory {  
-     private static final ThreadLocal<DbSession> threadLocal = new ThreadLocal<DbSession>();  
-       
-     private DbSessionFactory() {  
-         throw new AssertionError();  
-     }  
-       
-     
-     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;  
-   
- 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;  
-   
- public class ServiceFactory {  
-     private static final String DEFAULT_IMPL_PACKAGE_NAME = "impl";  
-       
-     private static Map<Class<?>, Object> map = new HashMap<>();  
-   
-     
-     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);  
-             }  
-         }  
-     }  
- }  
 
项目的其他部分,我就不在这里赘述了,如果需要完整的代码请点击下面的链接进行下载,其中包
括了Eclipse项目文件和创建数据库的SQL文件,我使用的IDE是Eclipse Java EE IDE for Web Developers
 (Luna Release),数据库使用的是MySQL,页面使用了Bootstrap框架。
下载链接:http://download.csdn.net/detail/jackfrued/8590231
项目的运行效果如下图所示。

点击班级名称可以分页查看班级学生的信息。

一页显示5条学生记录,点击下一页可以查看下一页的学生信息。

可以点击修改按钮修改学生信息。

可以点击删除按钮删除班级或学生,删除班级时如果班级中有学生则无法删除。
