1. 程式人生 > >超輕量級MVC框架的設計和實現 (2)

超輕量級MVC框架的設計和實現 (2)

在設計完API後,我們就需要實現這個MVC框架。MVC框架的核心是一個DispatcherServlet,用於接收所有的HTTP請求,並根據URL選擇合適的Action對其進行處理。在這裡,和Struts不同的是,所有的元件均被IoC容器管理,因此,DispatcherServlet需要例項化並持有Guice IoC容器,此外,DispatcherServlet還需要儲存URL對映和Action的對應關係,一個Interceptor攔截器鏈,一個ExceptionResolver處理異常。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框架的核心已經完成。 



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1896767