天天看點

1.寫一個Mvc架構_超輕量級MVC架構的設計和實作

  超輕量級MVC架構的設計和實作  2009-11-17 16:01:15

分類: Java

1.設計背景

   前段時間準備做一個小網站,但是又不想用Spring/Struts/WebWork這樣的大塊頭,是以決定自己寫一個MVC架構。花了3天左右時間完成,目前運作良好,整個MVC架構僅21KB,感興趣的朋友可以從http://code.google.com/p/lightweight-mvc/downloads/list下載下傳完整的源代碼和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;

    public ModelAndView(String view) {

        this.view = view;

        this.model = Collections.emptyMap();

    }

    public ModelAndView(String view, Map model) {

        this.view = view;

        this.model = model;

    }

    public String getView() {

        return view;

    }

    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;

    public static ActionContext getActionContext() {

        return contextThreadLocal.get();

    }

    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);

    }

    static void remove() {

        contextThreadLocal.remove();

    }

    public HttpServletRequest getRequest() {

        return request;

    }

    void setRequest(HttpServletRequest request) {

        this.request = request;

    }

    public HttpServletResponse getResponse() {

        return response;

    }

    void setResponse(HttpServletResponse response) {

        this.response = response;

    }

    public HttpSession getSession() {

        return session;

    }

    void setSession(HttpSession session) {

        this.session = session;

    }

    public ServletContext getServletContext() {

        return context;

    }

    void setServletContext(ServletContext context) {

        this.context = context;

    }

}

4.4接口Interceptor

   定義類似Filter功能的Interceptor接口:

package com.javaeedev.lightweight.mvc;

public interface Interceptor {

    void intercept(Action action, InterceptorChain chain) throws Exception;

}

   InterceptorChain對象和FilterChain是一樣的,它允許一個攔截器是否将請求繼續交給下一攔截器處理,還是中斷目前請求的處理:

package com.javaeedev.lightweight.mvc;

public interface InterceptorChain {

    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;

public interface ViewResolver {

     void init(ServletContext context) throws ServletException;

     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;

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<key<? style="word-wrap: break-word;">> findKeysByType(Injector inj, Class type) {

    Map<key<? style="word-wrap: break-word;">, Binding> map = inj.getBindings();

    List<key<? style="word-wrap: break-word;">> keyList = new ArrayList<key<? style="word-wrap: break-word;">>();

    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<key<? style="word-wrap: break-word;">> 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;

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;

public class JspViewResolver implements ViewResolver {

    public void init(ServletContext context) throws ServletException {

    }

    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架構的核心已經完成。