1.设计背景 前段时间准备做一个小网站,但是又不想用Spring/Struts/WebWork这样的大块头,因此决定自己写一个MVC框架。花了3天左右时间完成,目前运行良好,整个MVC框架仅21KB,感兴趣的朋友可以从下载完整的源代码和jar包。
2.设计目标 一个最简单最小巧的MVC框架,花哨的功能一个不要,越简洁越好,并且不使用XML配置文件,而是完全用Java 5注解配置。
3.功能列表 .组件必须用IoC配置
.处理HTTP请求的Action,类似WebWork每个请求都生成一个新实例,并自动填充属性
.类似Filter的Interceptor机制,但是在IoC容器中配置
.统一的异常处理
.多视图支持
由于组件需要用IoC容器配置,因此,第一步就是寻找小巧的IoC容器,Google Guice是一个很不错的选择,并且完全用Java 5注解配置组件。这个MVC框架唯一依赖的也就是Guice和Commons Logging两个jar包,如果使用Velocity作为视图则还需要Velocity的jar包。
4.设计各主要功能类4.1Action接口 负责处理Http请求的Action类必须实现的Action接口
package com.javaeedev.lightweight.mvc;
public interface Action {
ModelAndView execute() throws Exception;
}
从WebWork抄过来,不过返回值由String改成了ModelAndView(从Spring抄过来的),好处是不必再次根据String查找视图的绝对路径,直接在ModelAndView中包含了。用Spring的MVC其实可以发现,ModelAndView同时包含一个Model(本质是一个 Map)和View的路径,减少了Struts和WebWork需要的一个XML映射文件,而维护XML配置文件是一件相当令人头疼的问题,往往改了代码还要改配置,索性写死在代码中得了,视图路径又不会经常改变,没必要为了额外的灵活性给自己搞一堆XML配置文件。
4.2Action返回模型ModelAndView Action返回的ModelAndView:
package com.javaeedev.lightweight.mvc;
public final class ModelAndView {
private String view;
private Map
model;
/**
* Construct a View with empty model.
* @param view View's logic name.
*/
public ModelAndView(String view) {
this.view = view;
this.model = Collections.emptyMap();
}
/**
* Construct a View with model.
* @param view View's logic name.
* @param model Model as a Map.
*/
public ModelAndView(String view, Map model) {
this.view = view;
this.model = model;
}
/**
* Return View.
* @return View's logic name.
*/
public String getView() {
return view;
}
/**
* Return model.
* @return Model as a Map.
*/
public Map getModel() {
return model;
}
}
这个完全是从Spring MVC抄过来的,Map改成了泛型,View路径可以以"redirect:"开头表示重定向,这个和Spring MVC一致。虽然直接调用HttpServletResponse也可以重定向,但是遇到事务处理起来会很麻烦,还是让MVC框架自己来处理会好一些。
4.3类ActionContext
WebWork的Action设计的好处是大大简化了参数的绑定,不过很多时候也需要在Action中访问HttpSession等对象,因此还需要设计一个ActionContext类,通过ThreadLocal让Action对象能轻易地访问到这些对象:
package com.javaeedev.lightweight.mvc;
public final class ActionContext {
private static ThreadLocal contextThreadLocal = new ThreadLocal();
private HttpServletRequest request;
private HttpServletResponse response;
private HttpSession session;
private ServletContext context;
/**
* Get current ActionContext.
* @return ActionContext.
*/
public static ActionContext getActionContext() {
return contextThreadLocal.get();
}
/**
* Initiate all servlet objects as thread local.
* @param request HttpServletRequest object.
* @param response HttpServletResponse object.
* @param session HttpSession object.
* @param context ServletContext object.
*/
static void setActionContext(HttpServletRequest request, HttpServletResponse response, HttpSession session, ServletContext context) {
ActionContext actionContext = new ActionContext();
actionContext.setRequest(request);
actionContext.setResponse(response);
actionContext.setSession(session);
actionContext.setServletContext(context);
contextThreadLocal.set(actionContext);
}
/**
* Remove all servlet objects from thread local.
*/
static void remove() {
contextThreadLocal.remove();
}
/**
* Get HttpServletRequest object.
*
* @return HttpServletRequest object.
*/
public HttpServletRequest getRequest() {
return request;
}
/**
* Set HttpServletRequest object.
*
* @param request HttpServletRequest object.
*/
void setRequest(HttpServletRequest request) {
this.request = request;
}
/**
* Get HttpServletResponse object.
*
* @return HttpServletResponse object.
*/
public HttpServletResponse getResponse() {
return response;
}
/**
* Set HttpServletResponse object.
*
* @param response HttpServletResponse object.
*/
void setResponse(HttpServletResponse response) {
this.response = response;
}
/**
* Get HttpSession object.
*
* @return HttpSession object.
*/
public HttpSession getSession() {
return session;
}
/**
* Set HttpSession object.
*
* @param session HttpSession object.
*/
void setSession(HttpSession session) {
this.session = session;
}
/**
* Get ServletContext object.
*
* @return ServletContext object.
*/
public ServletContext getServletContext() {
return context;
}
/**
* Set ServletContext object.
*
* @param context ServletContext object.
*/
void setServletContext(ServletContext context) {
this.context = context;
}
}
4.4接口Interceptor
定义类似Filter功能的Interceptor接口:
package com.javaeedev.lightweight.mvc;
/**
* Intercept action's execution like servlet Filter, but interceptors are
* configured and managed by IoC container. Another difference from Filter
* is that Interceptor is executed around Action's execution, but before
* rendering view.
*
* @author Xuefeng
*/
public interface Interceptor {
/**
* Do intercept and invoke chain.doInterceptor() to process next interceptor.
* NOTE that process will not continue if chain.doInterceptor() method is not
* invoked.
*
* @param action Action instance to handle http request.
* @param chain Interceptor chain.
* @throws Exception If any exception is thrown, process will not continued.
*/
void intercept(Action action, InterceptorChain chain) throws Exception;
}
InterceptorChain对象和FilterChain是一样的,它允许一个拦截器是否将请求继续交给下一拦截器处理,还是中断当前请求的处理:
package com.javaeedev.lightweight.mvc;
/**
* Holds all interceptors as a chain.
*
* @author Xuefeng
*/
public interface InterceptorChain {
/**
* Apply next interceptor around the execution of Action.
*
* @param action Target Action to execute.
* @throws Exception Any exception if error occured.
*/
void doInterceptor(Action action) throws Exception;
}
4.5类ViewResolver
最后是支持多种View的ViewResolver,这个也抄自Spring MVC:
package com.javaeedev.lightweight.mvc;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* To resolve and render a view.
*
* @author Xuefeng
*/
public interface ViewResolver {
/**
* Init this ViewResolver.
*
* @param context ServletContext object that holds information of current
* web application.
* @throws ServletException If init failed.
*/
void init(ServletContext context) throws ServletException;
/**
* To resolve view's name and render view if necessary.
*
* @param view View's logic name.
* @param model Model represent as a generic Map.
* @param request HttpServletRequest object.
* @param response HttpServletResponse object.
* @throws ServletException If any ServletException occur.
* @throws IOException If any IOException occur.
*/
void resolveView(String view, Map model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
第一个版本支持JSP和Velocity两种View,其实支持其他的View完全是可扩展的,只需要参考现有的两种ViewResolver的实现再写一个实现即可,例如支持FreeMarker的ViewResolver。
到此为止,提供给客户端的API准备完毕。下一步是如何实现这些API。虽然概念和结构都来自WebWork和Spring,但是其具体实现却没有参考他们的源代码,因为读大块头的源码本身就是一件非常费力的事情,还不如自己身体力行,写代码往往比读懂代码更快。
5.开发MVC具体实现类
MVC框架的核心是一个DispatcherServlet,用于接收所有的HTTP请求,并根据URL选择合适的Action对其进行处理。在这里,和Struts不同的是,所有的组件均被IoC容器管理,因此,DispatcherServlet需要实例化并持有Guice IoC容器,此外,DispatcherServlet还需要保存URL映射和Action的对应关系,一个Interceptor拦截器链,一个 ExceptionResolver处理异常。
5.1类DispatcherServlet
package com.javaeedev.lightweight.mvc;
/**
* Core dispatcher servlet.
*
* @author Xuefeng
*/
public class DispatcherServlet extends HttpServlet {
private Log log = LogFactory.getLog(getClass());
private Map actionMap;
private Interceptor[] interceptors = null;
private ExceptionResolver exceptionResolver = null;
private ViewResolver viewResolver = null;
private Injector injector = null; // Guice IoC容器
...
}
Guice 的配置完全由Java 5注解完成,而在DispatcherServlet中,我们需要主动从容器中查找某种类型的Bean,相对于客户端被动地使用IoC容器(客户端甚至不能感觉到IoC容器的存在),DispatcherServlet需要使用ServiceLocator模式主动查找Bean,写一个通用方法:
private List> findKeysByType(Injector inj, Class> type) {
Map, Binding>> map = inj.getBindings();
List> keyList = new ArrayList>();
for(Key> key : map.keySet()) {
Type t = key.getTypeLiteral().getType();
if(t instanceof Class>) {
Class> clazz = (Class>) t;
if(type==null || type.isAssignableFrom(clazz)) {
keyList.add(key);
}
}
}
return keyList;
}
DispatcherServlet初始化时就要首先初始化Guice IoC容器:
public void init(ServletConfig config) throws ServletException {
String moduleClass = config.getInitParameter("module");
if(moduleClass==null || moduleClass.trim().equals(""))
throw new ServletException("Cannot find init parameter in web.xml: "
+ "?"
+ getClass().getName()
+ "module"
+ "put-your-config-module-full-class-name-here");
ServletContext context = config.getServletContext();
// init guice:
injector = Guice.createInjector(Stage.PRODUCTION, getConfigModule(moduleClass.trim(), context));
...
}
然后,从IoC容器中查找Action和URL的映射关系:
private Map getUrlMapping(List> actionKeys) {
Map urlMapping = new HashMap();
for(Key> key : actionKeys) {
Object obj = safeInstantiate(key);
if(obj==null)
continue;
Class actionClass = (Class) obj.getClass();
Annotation ann = key.getAnnotation();
if(ann instanceof Named) {
Named named = (Named) ann;
String url = named.value();
if(url!=null)
url = url.trim();
if(!"".equals(url)) {
log.info("Bind action [" + actionClass.getName() + "] to URL: " + url);
// link url with this action:
urlMapping.put(url, new ActionAndMethod(key, actionClass));
}
else {
log.warn("Cannot bind action [" + actionClass.getName() + "] to *EMPTY* URL.");
}
}
else {
log.warn("Cannot bind action [" + actionClass.getName() + "] because no @Named annotation found in config module. Using: binder.bind(MyAction.class).annotatedWith(Names.named(\"/url\"));");
}
}
return urlMapping;
}
我们假定客户端是以如下方式配置Action和URL映射的:
public class MyModule implements Module {
public void configure(Binder binder) {
// bind actions:
binder.bind(Action.class)
.annotatedWith(Names.named("/start.do"))
.to(StartAction.class);
binder.bind(Action.class)
.annotatedWith(Names.named("/register.do"))
.to(RegisterAction.class);
binder.bind(Action.class)
.annotatedWith(Names.named("/signon.do"))
.to(SignonAction.class);
...
}
}
即通过Guice提供的一个注解Names.named()指定URL。当然还可以用其他方法,比如标注一个@Url注解可能更方便,下一个版本会加上。
Interceptor,ExceptionResolver和ViewResolver也是通过查找获得的。
下面讨论DispatcherServlet如何真正处理用户请求。第一步是根据URL查找对应的Action:
String contextPath = request.getContextPath();
String url = request.getRequestURI().substring(contextPath.length());
if(log.isDebugEnabled())
log.debug("Handle for URL: " + url);
ActionAndMethod am = actionMap.get(url);
if(am==null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
return;
}
没找到Action就直接给个404 Not Found,找到了进行下一步,实例化一个Action并填充参数:
// init ActionContext:
HttpSession session = request.getSession();
ServletContext context = session.getServletContext();
ActionContext.setActionContext(request, response, session, context);
// 每次创建一个新的Action实例:
Action action = (Action) injector.getInstance(am.getKey());
// 把HttpServletRequest的参数自动绑定到Action的属性中:
List props = am.getProperties();
for(String prop : props) {
String value = request.getParameter(prop);
if(value!=null) {
am.invokeSetter(action, prop, value);
}
}
注意,为了提高速度,所有的set方法已经预先缓存了,因此避免每次请求都用反射重复查找Action的set方法。
然后要应用所有的Interceptor以便拦截Action:
InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
chains.doInterceptor(action);
ModelAndView mv = chains.getModelAndView();
实现InterceptorChain看上去复杂,其实就是一个简单的递归,大家看InterceptorChainImpl代码就知道了:
package com.javaeedev.lightweight.mvc;
/**
* Used for holds an interceptor chain.
*
* @author Xuefeng
*/
class InterceptorChainImpl implements InterceptorChain {
private final Interceptor[] interceptors;
private int index = 0;
private ModelAndView mv = null;
InterceptorChainImpl(Interceptor[] interceptors) {
this.interceptors = interceptors;
}
ModelAndView getModelAndView() {
return mv;
}
public void doInterceptor(Action action) throws Exception {
if(index==interceptors.length)
// 所有的Interceptor都执行完毕:
mv = action.execute();
else {
// 必须先更新index,再调用interceptors[index-1],否则是一个无限递归:
index++;
interceptors[index-1].intercept(action, this);
}
}
}
把上面的代码用try ... catch包起来,就可以应用ExceptionResolver了。
如果得到了ModelAndView,最后一步就是渲染View了,这个过程极其简单:
// render view:
private void render(ModelAndView mv, HttpServletRequest reqest, HttpServletResponse response) throws ServletException, IOException {
String view = mv.getView();
if(view.startsWith("redirect:")) {
// 重定向:
String redirect = view.substring("redirect:".length());
response.sendRedirect(redirect);
return;
}
Map model = mv.getModel();
if(viewResolver!=null)
viewResolver.resolveView(view, model, reqest, response);
}
最简单的JspViewResolver的实现如下:
package com.javaeedev.lightweight.mvc.view;
/**
* Let JSP render the model returned by Action.
*
* @author Xuefeng
*/
public class JspViewResolver implements ViewResolver {
/**
* Init JspViewResolver.
*/
public void init(ServletContext context) throws ServletException {
}
/**
* Render view using JSP.
*/
public void resolveView(String view, Map model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if(model!=null) {
Set keys = model.keySet();
for(String key : keys) {
request.setAttribute(key, model.get(key));
}
}
request.getRequestDispatcher(view).forward(request, response);
}
}
至此,MVC框架的核心已经完成。
阅读(5719) | 评论(0) | 转发(0) |