MVC架构探究及其源码实现
(1)-理论基础
MVC本来是存在于Desktop程序中的,M是指数据模型,V是指用户界面,C则是控制器。使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据你可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。
模型-视图-控制器(MVC)是Xerox PARC在八十年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已经被广泛使用。
我们先来看下经典桌面MVC的模型,为了实现显示和数据的分离,我们在视图和模型之间加入一个控制层,视图只能通过控制器来操作模型,也就是数据层,一旦数据有更新,模型就会通知视图更新自己。在桌面程序中,用户可以直接和视图进行交互,通过对事件的操作,可以触发视图的各种事件,通过控制器,以达到更新模型或数据的目的。
由于Web应用的复杂程度的日益增加,功能也日益庞大,表示层与数据层的分离也显得日益重要。于是MVC这种架构模式被移植到WEB开发中来也是很自然的事情,然而,Web程序和Desktop程序是有很大区别的,大家都知道,HTTP协议是无连接的,当用户从服务器拿到一个页面之后,整个交互过程也就完成了,用户无法知道服务器端状态的更新,除非用户再次发送请求。而且,在Web程序中,是没有事件模型支持的,用户的每个动作都必须转化为对服务器请求。以往的经验,我们经常把视图和控制器组合起来,一个页面既包含程序的业务逻辑,又包含页面的显示信息。然而,视图是经常变化的,业务逻辑确实相对比较稳定的。为了解决这个问题,比较流行的做法是让控制器执行业务逻辑,从数据层(模型)中抓取显示相关的数据,而视图仅仅是一段显示代码,没有业务逻辑。由于请求多种多样,而且在控制器到视图的数据转发部分含有很多相同的逻辑,而且为了方便扩展和管理,于是就有人提出了前端控制器的概念,也就是请求分发器。分发器的作用主要工作就是将一个request分发到一个合适的处理器上,并将处理返回的包含特定信息的视图返回给客户端。下图展现了现在常用的Web MVC 的标准模型。
然而,这不是唯一的模型,在ASP.net中,有一种叫做页面控制器的模型。在这种MVC中,并不是令分发器去寻找一个控制器并执行之,而是直接到达视图并且在继续生成视图之前调用相应的控制器。与传统的MVC模式中的前端控制器对应,这种模式称为页面控制器。页面控制器和前端控制器实现实现之间的区别在于页面控制器描述的往往是同一个页面中(如类似于控制面板那样的页面)的处理逻辑,不能用于跨多个页面对处理过程进行控制或协调。它是一种Poll的模型。
对应的,前端控制器是很典型的一种Push的模型,对同一个请求的几个不同的动作,根据业务逻辑处理后的结果分别被压入到response的各个不同部分。
在下文中,我们仅讨论前端控制器这种方式的具体实现。
(2)-核心组件定义
上文中,我们讨论了MVC的架构的基本原理,这里,我们就要开始着手实现一个简单的WEB MVC前端控制器模型。为了实现这个架构的原型,我们必须引入几个新的概念。
- DispatcherServlet:前端控制器,也是整个架构的核心,负责处理和分发请求。
- HandlerMapping:处理器映射,他主要包含的是控制器的列表,对于特定的请求,根据HandlerMapping的映射关系,可以找到特定的控制器。最简单的便是url到控制器的映射。
- HandlerAdapter:对于不同类型的控制器,该类负责把Handler请求处理的结果统一转换成ModelAndView。
- ModelAndView:包含数据和视图的信息,一般包含视图名,和这个视图需要用的数据,这里的Model大家不要误会为模型的概念,它只不过同时包含视图信息及这个视图需要显示的相关信息而已。
- ViewResolver:它View名称解析成View对象。
- View:定义response显示的详细内容。
HandlerMapping:
- package com.google.mvc.web.servlet;
- import javax.servlet.http.HttpServletRequest;
- public interface HandlerMapping {
- String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
- Object getHandler(HttpServletRequest request) throws Exception;
- }
- package com.google.mvc.web.servlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public interface HandlerAdapter {
- boolean supports(Object handler);
- ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
- long getLastModified(HttpServletRequest request, Object handler);
- }
- package com.google.mvc.web.servlet;
- public interface ViewResolver {
- View resolveViewName(String viewName) throws Exception;
- }
- package com.google.mvc.web.servlet;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public interface View {
- String getContentType();
- void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
- }
- package com.google.mvc.web.servlet;
- import java.util.HashMap;
- import java.util.Map;
- public class ModelAndView {
- private Object view;
- private Map<String, Object> model;
- private boolean cleared;
- public ModelAndView() {
- }
- public ModelAndView(String viewName) {
- this.view = viewName;
- }
- public ModelAndView(View view) {
- this.view = view;
- }
- public ModelAndView(String viewName, Map<String, Object> model) {
- this.view = viewName;
- if (model != null) {
- addAllObjects(model);
- }
- }
- public ModelAndView(View view, Map<String, Object> model) {
- this.view = view;
- if (model != null) {
- addAllObjects(model);
- }
- }
- public ModelAndView(String viewName, String modelName, Object modelObject) {
- this.view = viewName;
- addObject(modelName, modelObject);
- }
- public ModelAndView(View view, String modelName, Object modelObject) {
- this.view = view;
- addObject(modelName, modelObject);
- }
- public void setViewName(String viewName) {
- this.view = viewName;
- }
- public String getViewName() {
- return (this.view instanceof String ? (String) this.view : null);
- }
- public void setView(View view) {
- this.view = view;
- }
- public View getView() {
- return (this.view instanceof View ? (View) this.view : null);
- }
- public boolean hasView() {
- return (this.view != null);
- }
- public boolean isReference() {
- return (this.view instanceof String);
- }
- protected Map<String, Object> getModelInternal() {
- return this.model;
- }
- public Map<String, Object> getModelMap() {
- if (this.model == null) {
- this.model = new HashMap<String, Object>();
- }
- return this.model;
- }
- public Map<String, Object> getModel() {
- return getModelMap();
- }
- public ModelAndView addObject(String attributeName, Object attributeValue) {
- getModelMap().put(attributeName, attributeValue);
- return this;
- }
- public ModelAndView addObject(Object attributeValue) {
- getModelMap().put(attributeValue.toString(), attributeValue);
- return this;
- }
- public ModelAndView addAllObjects(Map<String, Object> modelMap) {
- getModelMap().putAll(modelMap);
- return this;
- }
- public void clear() {
- this.view = null;
- this.model = null;
- this.cleared = true;
- }
- public boolean isEmpty() {
- return (this.view == null && this.model == null);
- }
- public boolean wasCleared() {
- return (this.cleared && isEmpty());
- }
- public String toString() {
- StringBuffer buf = new StringBuffer("ModelAndView: ");
- if (isReference()) {
- buf.append("reference to view with name '").append(this.view).append("'");
- } else {
- buf.append("materialized View is [").append(this.view).append(']');
- }
- buf.append("; model is ").append(this.model);
- return buf.toString();
- }
- }
这几个类由DispatcherServlet管理和控制,以下是它们的序列图:
这些对象需要怎么注入到系统中呢?这里当然少不了配置文件的支持,现在比较流行的MVC框架大多可以使用Spring作为其IOC容器,为了简单起见,我们自己决定模拟Spring简单地实现一个配置管理容器,用于管理我们的各种对象资源。
(3)-WebApplicationContext
直接利用web.xml去配置和定义我们的对象组件显然是不灵活和不方便扩展的,由于我们系统中将会需要配置很多个不同的对象资源,比如控制器,View对象,HandlerMapping对象等等,如何对它们进行管理,如何能让我们的前端控制器访问和利用到到它们便是我们不得不面对的问题。还好,现在有了Spring,现在很多流行的MVC框架都支持使用Spring对自己容器里的对象资源进行管理。尽管Spring千好万好,我们这里还是决定不使用它,而是自己来写一个对象容器来管理我们的相关资源,这样我们不仅可以了解对象资源配置管理的细节,还可以顺带学习一下Spring等IOC容器的实现原理。当然,我们这里的实现方案将会尽可能的简单。如下便是我们的WebApplicationContext类的实现,它能够自动查找WEB-INF路径下所有以config结尾的xml文件,并把其中定义的对象抽取出来,放到Application作用域中,由于我们这里只是个Sample,不需要考虑太多的并发的情况,所有对象的类型,我们都是用Singleton,也就是定义的每个对象都为所有的请求和Servlet共享。 WebApplicationContext中还定义了一个很有用的方法,beansOfType(Class<T>),该方法用于查找出系统中定义的所有的属于当前类型的所有对象资源。
- package com.google.mvc.web.context;
- import java.io.InputStream;
- import java.lang.reflect.Method;
- import java.util.HashMap;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.ConcurrentHashMap;
- import javax.servlet.ServletContext;
- import javax.xml.parsers.DocumentBuilder;
- import javax.xml.parsers.DocumentBuilderFactory;
- import javax.xml.parsers.ParserConfigurationException;
- import org.apache.log4j.Logger;
- import org.w3c.dom.Document;
- import org.w3c.dom.Element;
- import org.w3c.dom.NodeList;
- public class WebApplicationContext {
- private static final Logger LOGGER = Logger.getLogger(WebApplicationContext.class);
- private static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".CONTEXT";
- private Map<String, Object> cacheMap;
- private ServletContext servletContext;
- private DocumentBuilder builder;
- public WebApplicationContext(ServletContext servletContext) {
- this.servletContext = servletContext;
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- try {
- builder = factory.newDocumentBuilder();
- } catch (ParserConfigurationException e) {
- LOGGER.error("Can't load dom builder", e);
- }
- }
- public void init() {
- ServletContext context = getServletContext();
- Set<?> set = context.getResourcePaths("/WEB-INF");
- Object map = servletContext.getAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE);
- if (map != null) {
- cacheMap = (Map<String, Object>) map;
- } else {
- cacheMap = new ConcurrentHashMap<String, Object>();
- servletContext.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, cacheMap);
- for (Object o : set) {
- String path = (String) o;
- if (path.endsWith("config.xml")) {
- try {
- loadResource(servletContext.getResourceAsStream(path));
- } catch (Exception ex) {
- LOGGER.error("Can't load resource " + path);
- }
- }
- }
- }
- }
- private void loadResource(InputStream resource) throws Exception{
- Document doc = builder.parse(resource);
- Element root = doc.getDocumentElement();
- NodeList nodeList = root.getElementsByTagName("bean");
- for(int i = 0; i < nodeList.getLength(); i++){
- Element el = (Element)nodeList.item(i);
- String id = el.getAttribute("id");
- String className = el.getAttribute("class");
- Class<?> clazz = this.getClass().getClassLoader().loadClass(className);
- Object o = createBean(id, clazz);
- NodeList propertyList = el.getElementsByTagName("property");
- for(int j = 0; j < propertyList.getLength(); j++){
- Element prop = (Element)propertyList.item(j);
- String methodName = getMethodName(prop.getAttribute("name"));
- Method m = clazz.getMethod(methodName, String.class);
- String property = prop.getAttribute("value");
- Object dependObject = cacheMap.get(property);
- if(dependObject != null){
- m.invoke(o, dependObject);
- } else {
- m.invoke(o, property);
- }
- }
- cacheMap.put(id, o);
- }
- }
- protected String getMethodName(String methodName){
- StringBuilder sb = new StringBuilder();
- sb.append("set");
- sb.append(methodName.substring(0, 1).toUpperCase(Locale.US));
- sb.append(methodName.substring(1));
- return sb.toString();
- }
- public Object createBean(Class<?> clazz) throws Exception{
- return createBean(clazz.getCanonicalName(), clazz);
- }
- public Object createBean(String name, Class<?> clazz) throws Exception{
- Object o = cacheMap.get(name);
- if(o == null){
- o = clazz.newInstance();
- if(o instanceof WebApplicationContextAware){
- ((WebApplicationContextAware)o).setWebApplicationContext(this);
- }
- cacheMap.put(name, o);
- }
- LOGGER.info(name + "=" + clazz.getCanonicalName());
- return o;
- }
- public Object getBean(String beanName){
- return servletContext.getAttribute(beanName);
- }
- public ServletContext getServletContext() {
- return servletContext;
- }
- public void setServletContext(ServletContext servletContext) {
- this.servletContext = servletContext;
- }
- public <T> Map<String, T> beansOfType(Class<T> clazz){
- Map<String, T> map = new HashMap<String, T>();
- for(String key : cacheMap.keySet()){
- Object o = cacheMap.get(key);
- if(clazz.isAssignableFrom(o.getClass())){
- map.put(key, (T)o);
- }
- }
- return map;
- }
- }
- <?xml version="1.0" encoding="UTF-8"?>
- <beans>
- <bean id="ControllerAdapter" class="com.google.mvc.web.servlet.mvc.ControllerHandlerAdapter" />
- <bean id="HttpRequestAdapter" class="com.google.mvc.web.servlet.mvc.HttpRequestHandlerAdapter" />
- <bean id="ViewResolver" class="com.google.mvc.web.servlet.mvc.DefaultViewResolver">
- <property name="viewClass" value="com.google.mvc.web.servlet.mvc.InternalResourceView"/>
- <property name="prefix" value="/WEB-INF/"/>
- <property name="suffix" value=".jsp"/>
- </bean>
- <bean id="login.do" class="com.google.mvc.web.sample.LoginController" />
- <bean id="hello.do" class="com.google.mvc.web.sample.HelloController" />
- <bean id="404" class="com.google.mvc.web.servlet.mvc.HandlerFor404" />
- </beans>
- package com.google.mvc.web.context;
- public interface WebApplicationContextAware {
- void setWebApplicationContext(WebApplicationContext wac);
- }
(4)-前端控制器
前端控制器是整个MVC框架中最为核心的一块,它主要用来拦截符合要求的外部请求,并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端。前端控制器既可以使用Filter实现(Struts2采用这种方式),也可以使用Servlet来实现。这里我们就采用后一种方式来实现我们的MVC框架。
1.配置web.xml,使得我们的前端控制器可以拦截所有符合要求的用户请求,这里我们的前端控制器能处理所有以.do结尾的用户请求。
- <?xml version="1.0" encoding="ISO-8859-1"?>
- <web-app xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- version="2.5">
- <description>MVC Sample</description>
- <display-name>MVC</display-name>
- <servlet>
- <servlet-name>DispatcherServlet</servlet-name>
- <servlet-class>com.google.mvc.web.servlet.DispatcherServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>DispatcherServlet</servlet-name>
- <url-pattern>*.do</url-pattern>
- </servlet-mapping>
- </web-app>
FrameworkServlet是DispatcherServlet的直接父类,继承自HttpServlet,主要用来初始话WebApplicationContext,把不同的Http请求操作委托给同一个方法去处理。
- package com.google.mvc.web.servlet;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.context.WebApplicationContext;
- public abstract class FrameworkServlet extends HttpServlet {
- private static final long serialVersionUID = 1L;
- private static final Logger LOGGER = Logger.getLogger(FrameworkServlet.class);
- private WebApplicationContext webApplicationContext;
- @Override
- public void init() throws ServletException {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("----------Initializing servlet '" + getServletName() + "'----------");
- }
- this.webApplicationContext = initWebApplicationContext();
- initServletBean();
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("----------Servlet '" + getServletName() + "' configured successfully----------\n\n");
- }
- }
- private WebApplicationContext initWebApplicationContext() {
- WebApplicationContext wac = new WebApplicationContext(getServletContext());
- wac.init();
- onRefresh(wac);
- return wac;
- }
- protected void onRefresh(WebApplicationContext context) {
- // For subclasses: do nothing by default.
- }
- protected void initServletBean(){
- }
- protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
- protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- long startTime = System.currentTimeMillis();
- Throwable failureCause = null;
- try {
- doService(request, response);
- } catch (ServletException ex) {
- failureCause = ex;
- throw ex;
- } catch (IOException ex) {
- failureCause = ex;
- throw ex;
- } catch (Throwable ex) {
- failureCause = ex;
- throw new NestedServletException("Request processing failed", ex);
- } finally {
- if (failureCause != null) {
- LOGGER.error("Could not complete request", failureCause);
- } else {
- long processingTime = System.currentTimeMillis() - startTime;
- if (LOGGER.isDebugEnabled()) {
- LOGGER.info("Successfully completed request, cost " + processingTime + " ms\n");
- }
- }
- }
- }
- @Override
- protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IOException {
- processRequest(request, response);
- }
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- processRequest(request, response);
- }
- @Override
- protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IOException {
- processRequest(request, response);
- }
- @Override
- protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IOException {
- processRequest(request, response);
- }
- @Override
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IOException {
- processRequest(request, response);
- }
- @Override
- protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- processRequest(request, response);
- }
- @Override
- protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IOException {
- processRequest(request, response);
- }
- @Override
- public void destroy() {
- if(LOGGER.isDebugEnabled()){
- LOGGER.info("Servlet destory");
- }
- super.destroy();
- }
- public WebApplicationContext getWebApplicationContext() {
- return webApplicationContext;
- }
- }
- package com.google.mvc.web.servlet;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.Properties;
- import java.util.StringTokenizer;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.context.WebApplicationContext;
- public class DispatcherServlet extends FrameworkServlet {
- private static final long serialVersionUID = 1L;
- private static final Logger LOGGER = Logger.getLogger(DispatcherServlet.class);
- private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
- private static final Properties defaultStrategies = new Properties();
- private List<HandlerMapping> handlerMappings;
- private List<HandlerAdapter> handlerAdapters;
- private List<ViewResolver> viewResolvers;
- static {
- try {
- defaultStrategies.load(DispatcherServlet.class.getResourceAsStream(DEFAULT_STRATEGIES_PATH));
- } catch (IOException ex) {
- throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
- }
- }
- @Override
- protected void onRefresh(WebApplicationContext wac) {
- initHandlerMappings(wac);
- initHandlerAdapters(wac);
- initViewResolvers(wac);
- }
- private void initHandlerMappings(WebApplicationContext wac) {
- Map<String, HandlerMapping> map = wac.beansOfType(HandlerMapping.class);
- if (!map.isEmpty()) {
- this.handlerMappings = new ArrayList<HandlerMapping>(map.values());
- }
- if (this.handlerMappings == null) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
- }
- this.handlerMappings = getDefaultStrategies(wac, HandlerMapping.class);
- }
- }
- private void initHandlerAdapters(WebApplicationContext wac) {
- Map<String, HandlerAdapter> map = wac.beansOfType(HandlerAdapter.class);
- if (!map.isEmpty()) {
- this.handlerAdapters = new ArrayList<HandlerAdapter>(map.values());
- }
- if (this.handlerAdapters == null) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default");
- }
- this.handlerAdapters = getDefaultStrategies(wac, HandlerAdapter.class);
- }
- }
- private void initViewResolvers(WebApplicationContext wac) {
- Map<String, ViewResolver> map = wac.beansOfType(ViewResolver.class);
- if (!map.isEmpty()) {
- this.viewResolvers = new ArrayList<ViewResolver>(map.values());
- }
- if (this.viewResolvers == null) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default");
- }
- this.viewResolvers = getDefaultStrategies(wac, ViewResolver.class);
- }
- }
- @Override
- protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("DispatcherServlet with name '" + getServletName() + "' received request for ["
- + request.getRequestURI() + "]");
- }
- doDispatch(request, response);
- }
- protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Bound request context to thread: " + request);
- }
- Object handler = getHandler(request);
- HandlerAdapter ha = getHandlerAdapter(handler);
- ModelAndView mv = ha.handle(request, response, handler);
- // Do we need view name translation?
- if (mv != null && !mv.hasView()) {
- mv.setViewName(getDefaultViewName(request));
- }
- // Did the handler return a view to render?
- if (mv != null && !mv.wasCleared()) {
- render(mv, request, response);
- }
- }
- protected <T> List<T> getDefaultStrategies(WebApplicationContext wac, Class<T> strategyInterface) {
- String key = strategyInterface.getName();
- List<T> strategies = new ArrayList<T>();
- String value = defaultStrategies.getProperty(key);
- if (value != null) {
- StringTokenizer token = new StringTokenizer(value, ",");
- while (token.hasMoreTokens()) {
- String className = token.nextToken();
- try {
- Class<?> clazz = this.getClass().getClassLoader().loadClass(className);
- strategies.add((T) wac.createBean(clazz));
- } catch (Exception e) {
- LOGGER.error("Can't load class " + className + "", e);
- }
- }
- } else {
- strategies = Collections.emptyList();
- }
- return strategies;
- }
- protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
- View view = null;
- if (mv.isReference()) {
- // We need to resolve the view name.
- view = resolveViewName(mv.getViewName(), mv.getModelInternal(), request);
- if (view == null) {
- throw new ServletException("Could not resolve view with name '" + mv.getViewName()
- + "' in servlet with name '" + getServletName() + "'");
- }
- } else {
- // No need to lookup: the ModelAndView object contains the actual
- // View object.
- view = mv.getView();
- if (view == null) {
- throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a "
- + "View object in servlet with name '" + getServletName() + "'");
- }
- }
- // Delegate to the View object for rendering.
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
- }
- view.render(mv.getModelInternal(), request, response);
- }
- protected View resolveViewName(String viewName, Map<String, Object> model, HttpServletRequest request)
- throws Exception {
- for (Iterator<ViewResolver> it = this.viewResolvers.iterator(); it.hasNext();) {
- ViewResolver viewResolver = it.next();
- View view = viewResolver.resolveViewName(viewName);
- if (view != null) {
- return view;
- }
- }
- return null;
- }
- protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
- Iterator<HandlerAdapter> it = this.handlerAdapters.iterator();
- while (it.hasNext()) {
- HandlerAdapter ha = it.next();
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Testing handler adapter [" + ha + "]");
- }
- if (ha.supports(handler)) {
- return ha;
- }
- }
- throw new ServletException("No adapter for handler [" + handler
- + "]: Does your handler implement a supported interface like Controller?");
- }
- protected Object getHandler(HttpServletRequest request) throws Exception {
- Iterator<HandlerMapping> it = this.handlerMappings.iterator();
- while (it.hasNext()) {
- HandlerMapping hm = it.next();
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName()
- + "'");
- }
- return hm.getHandler(request);
- }
- return null;
- }
- private String getDefaultViewName(HttpServletRequest request) {
- String url = request.getServletPath();
- url = url.replaceAll("/", "");
- url = url.replaceAll(".html", "");
- url = url.replaceAll(".htm", "");
- url = "WEB-INF/" + url + ".jsp";
- return url;
- }
- }
初始化操作.
- 检查系统中是否已经定义HandlerMapping。如果没有定义,则使用默认配置。
- 检查系统中是否已经定义HandlerAdapter。如果没有定义,则使用默认配置。
- 检查系统中是否已经定义ViewResolover。如果没有定义,则使用默认配置。
请求处理.
- 根据特定的请求,使用HandlerMapping找到相应的控制器Handler。
- 找到支持此种handler的HandlerAdapter,handler处理完响应业务后,HandlerAdapter把它转化为ModelAndView对象。
- 利用ViewResolver对ModelAndView进行分析,生成相应的View对象。
- 生成响应。
默认配置
- com.google.mvc.web.servlet.HandlerMapping=com.google.mvc.web.servlet.handler.URLHandlerMapping
- com.google.mvc.web.servlet.HandlerAdapter=com.google.mvc.web.servlet.mvc.HttpRequestHandlerAdapter,\
- com.google.mvc.web.servlet.mvc.ControllerHandlerAdapter
- com.google.mvc.web.servlet.ViewResolver=com.google.mvc.web.servlet.mvc.DefaultViewResolver
(5)-相关组件实现
本文将讨论HandlerMapping,HandlerAdapter,ViewResolver组件类的具体实现。
URLHandlerMapping,利用request中包含的url信息,找到对应Handler对象,URLHandlerMapping是最典型的映射方式。
- package com.google.mvc.web.servlet.handler;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.atomic.AtomicBoolean;
- import javax.servlet.http.HttpServletRequest;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.context.WebApplicationContext;
- import com.google.mvc.web.context.WebApplicationContextAware;
- import com.google.mvc.web.servlet.HandlerMapping;
- import com.google.mvc.web.servlet.mvc.Controller;
- import com.google.mvc.web.servlet.mvc.HttpRequestHandler;
- public class URLHandlerMapping implements HandlerMapping, WebApplicationContextAware{
- private static final Logger LOGGER = Logger.getLogger(URLHandlerMapping.class);
- private WebApplicationContext wac;
- private Map<String, Object> handlerMap = new ConcurrentHashMap<String, Object>();
- private AtomicBoolean initialize = new AtomicBoolean(false);
- @Override
- public void setWebApplicationContext(WebApplicationContext wac) {
- this.wac = wac;
- }
- @Override
- public Object getHandler(HttpServletRequest request) throws Exception {
- if(LOGGER.isDebugEnabled()){
- LOGGER.debug("Find handler for request " + request.getServletPath());
- }
- if(initialize.compareAndSet(false, true)){
- Map<String, HttpRequestHandler> map1 = wac.beansOfType(HttpRequestHandler.class);
- for(String key : map1.keySet()){
- handlerMap.put(key, map1.get(key));
- }
- Map<String, Controller> map2 = wac.beansOfType(Controller.class);
- for(String key : map2.keySet()){
- handlerMap.put(key, map2.get(key));
- }
- }
- Object handler = handlerMap.get(getHandlerName(request));
- if(handler == null){
- handler = handlerMap.get("404");
- }
- return handler;
- }
- protected String getHandlerName(HttpServletRequest request){
- String path = request.getServletPath();
- int index = path.lastIndexOf('/');
- String handleName = path.substring(index + 1, path.length());
- return handleName;
- }
- }
HandlerAdapter用于把不同的Handler对象处理的结果封装成一个统一的对象ModelAndView,以达到逻辑上的一致处理。在这里,我们定义两种Handler类型
- Controller:用于处理用户的业务逻辑,并返回具体ModelAndView对象,具体接口定义如下
- package com.google.mvc.web.servlet.mvc;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.google.mvc.web.servlet.ModelAndView;
- public interface Controller {
- ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
- }
- HttpRequestHandler:用于处理简单的HTTP请求。接口定义如下
- package com.google.mvc.web.servlet.mvc;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public interface HttpRequestHandler {
- void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
- }
- package com.google.mvc.web.servlet.mvc;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class HandlerFor404 implements HttpRequestHandler {
- @Override
- public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
- response.sendRedirect("404.html");
- }
- }
ControllerHandlerAdapter
- package com.google.mvc.web.servlet.mvc;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.google.mvc.web.servlet.HandlerAdapter;
- import com.google.mvc.web.servlet.ModelAndView;
- public class ControllerHandlerAdapter implements HandlerAdapter{
- public boolean supports(Object handler) {
- return (handler instanceof Controller);
- }
- public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws Exception {
- return ((Controller) handler).handleRequest(request, response);
- }
- public long getLastModified(HttpServletRequest request, Object handler) {
- return -1L;
- }
- }
- package com.google.mvc.web.servlet.mvc;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.google.mvc.web.servlet.HandlerAdapter;
- import com.google.mvc.web.servlet.ModelAndView;
- public class HttpRequestHandlerAdapter implements HandlerAdapter {
- @Override
- public long getLastModified(HttpServletRequest request, Object handler) {
- return 0;
- }
- @Override
- public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws Exception {
- ((HttpRequestHandler) handler).handleRequest(request, response);
- return null;
- }
- @Override
- public boolean supports(Object handler) {
- return (handler instanceof HttpRequestHandler);
- }
- }
ViewResolver用于指定View的生成方式,我们先来看下AbstractView的定义
- package com.google.mvc.web.servlet.mvc;
- import java.util.Iterator;
- import java.util.Map;
- import java.util.Map.Entry;
- import javax.servlet.ServletRequest;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.servlet.View;
- public abstract class AbstractView implements View {
- private static final Logger LOGGER = Logger.getLogger(AbstractView.class);
- public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
- public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
- public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
- public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
- public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
- public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
- public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";
- private String contentType = DEFAULT_CONTENT_TYPE;
- private boolean alwaysInclude = false;
- @Override
- public String getContentType() {
- return contentType;
- }
- protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request){
- Iterator<Entry<String, Object>> it = model.entrySet().iterator();
- while (it.hasNext()) {
- Entry<String, Object> entry = it.next();
- String modelName = entry.getKey();
- Object modelValue = entry.getValue();
- if (modelValue != null) {
- request.setAttribute(modelName, modelValue);
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() +
- "] to request in view with name '" + this + "'");
- }
- } else {
- request.removeAttribute(modelName);
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Removed model object '" + modelName + "' from request in view with name '"
- + this + "'");
- }
- }
- }
- }
- protected boolean useInclude(HttpServletRequest request, HttpServletResponse response) {
- return (this.alwaysInclude || request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null || response
- .isCommitted());
- }
- protected void exposeForwardRequestAttributes(HttpServletRequest request) {
- exposeRequestAttributeIfNotPresent(request, FORWARD_REQUEST_URI_ATTRIBUTE, request.getRequestURI());
- exposeRequestAttributeIfNotPresent(request, FORWARD_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
- exposeRequestAttributeIfNotPresent(request, FORWARD_SERVLET_PATH_ATTRIBUTE, request.getServletPath());
- exposeRequestAttributeIfNotPresent(request, FORWARD_PATH_INFO_ATTRIBUTE, request.getPathInfo());
- exposeRequestAttributeIfNotPresent(request, FORWARD_QUERY_STRING_ATTRIBUTE, request.getQueryString());
- }
- private void exposeRequestAttributeIfNotPresent(ServletRequest request, String name, Object value) {
- if (request.getAttribute(name) == null) {
- request.setAttribute(name, value);
- }
- }
- public void setContentType(String contentType) {
- this.contentType = contentType;
- }
- public boolean isAlwaysInclude() {
- return alwaysInclude;
- }
- public void setAlwaysInclude(boolean alwaysInclude) {
- this.alwaysInclude = alwaysInclude;
- }
- }
- package com.google.mvc.web.servlet.mvc;
- import java.io.IOException;
- import java.util.Map;
- import javax.servlet.RequestDispatcher;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- public class InternalResourceView extends AbstractView {
- private static final Logger LOGGER = Logger.getLogger(InternalResourceView.class);
- private String url;
- @Override
- public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Rendering view with name '" + this + "' with model " + model);
- }
- if(model != null){
- exposeModelAsRequestAttributes(model, request);
- }
- RequestDispatcher rd = request.getRequestDispatcher(url);
- if (rd == null) {
- throw new ServletException("Could not get RequestDispatcher for ["
- + getUrl() + "]: check that this file exists within your WAR");
- }
- if (useInclude(request, response)) {
- response.setContentType(getContentType());
- rd.include(request, response);
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Included resource [" + getUrl() + "] in InternalResourceView '" + url + "'");
- }
- }else {
- exposeForwardRequestAttributes(request);
- rd.forward(request, response);
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Forwarded to resource [" + getUrl() + "] in InternalResourceView '" + url + "'");
- }
- }
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public String toString(){
- return this.url;
- }
- }
- package com.google.mvc.web.servlet.mvc;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.servlet.View;
- import com.google.mvc.web.servlet.ViewResolver;
- public class DefaultViewResolver implements ViewResolver
- {
- private static final Logger LOGGER = Logger.getLogger(DefaultViewResolver.class);
- private String prefix = "";
- private String suffix = "";
- private Class<View> viewClass;
- @Override
- public View resolveViewName(String viewName) throws Exception {
- View view = viewClass.newInstance();
- if(view instanceof InternalResourceView){
- ((InternalResourceView)view).setUrl(prefix + viewName + suffix);
- }
- return view;
- }
- public void setViewClass(String viewClass){
- try {
- this.viewClass = (Class<View>) this.getClass().getClassLoader().loadClass(viewClass);
- } catch (ClassNotFoundException e) {
- LOGGER.error("Can't load view class " + viewClass, e);
- }
- }
- public String getPrefix() {
- return prefix;
- }
- public void setPrefix(String prefix) {
- this.prefix = prefix;
- }
- public String getSuffix() {
- return suffix;
- }
- public void setSuffix(String suffix) {
- this.suffix = suffix;
- }
- }
到这里,我们在整个MVC架构的源码实现已经完成了,下一篇,我们将介绍一个基于我们这个MVC架构的Demo。
(6)-简单示例
在前一系列的文章中,我们已经完成了MVC架构模式的简单实现,尽管有些粗糙,有些功能还不完善,但是,麻雀虽小,五脏俱全。我们现在就用这个小小的框架,来实现我们的几个简单的应用。
限于篇幅,我们不可能把应用的所有代码都贴上来,我们先来演示一个Hello World的简单应用。
实现控制器HelloController.java
- package com.google.mvc.web.sample;
- import java.util.Date;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.servlet.ModelAndView;
- import com.google.mvc.web.servlet.mvc.Controller;
- public class HelloController implements Controller {
- private static final Logger LOGGER = Logger.getLogger(HelloController.class);
- @Override
- public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
- String now = (new Date()).toString();
- LOGGER.info("Returning hello view with " + now);
- return new ModelAndView("hello", "now", now);
- }
- }
- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
- <bean id="ControllerAdapter" class="com.google.mvc.web.servlet.mvc.ControllerHandlerAdapter" />
- <bean id="HttpRequestAdapter" class="com.google.mvc.web.servlet.mvc.HttpRequestHandlerAdapter" />
- <bean id="ViewResolver" class="com.google.mvc.web.servlet.mvc.DefaultViewResolver">
- <property name="viewClass" value="com.google.mvc.web.servlet.mvc.InternalResourceView"/>
- <property name="prefix" value="/WEB-INF/"/>
- <property name="suffix" value=".jsp"/>
- </bean>
- <bean id="hello.do" class="com.google.mvc.web.sample.HelloController" />
- <bean id="404" class="com.google.mvc.web.servlet.mvc.HandlerFor404" />
- </beans>
- <html>
- <head>
- <title>Hello World</title>
- </head>
- <body>
- <strong>Current time is ${now}</strong>
- </body>
- </html>
到现在为止,我们简单的HelloWorld的应用就已经完成了。部署完成后,我们访问application下的hello.do便可以访问此页面,并且可以看到当前时间的显示。是不是很简单呀。
这个应用确实是太简单了,以至于我们无法看出MVC的任何强大之处。下面我们再看一个稍微复杂一点的例子,用户登录的验证。
首先,我们来实现我们的控制器LoginController.java
- package com.google.mvc.web.sample;
- import java.util.HashMap;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.apache.log4j.Logger;
- import com.google.mvc.web.servlet.ModelAndView;
- import com.google.mvc.web.servlet.mvc.Controller;
- public class LoginController implements Controller
- {
- private static final Logger LOGGER = Logger.getLogger(LoginController.class);
- private static final Map<String, String> MAP = new HashMap<String, String>();
- static{
- MAP.put("james", "123456");
- MAP.put("andy", "123456");
- MAP.put("link", "123456");
- MAP.put("sample", "123456");
- }
- @Override
- public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
- String username = request.getParameter("username");
- String password = request.getParameter("password");
- if(LOGGER.isDebugEnabled()){
- LOGGER.debug("username = " + username);
- LOGGER.debug("password = " + password);
- }
- if(username == null || password == null){
- return new ModelAndView("login");
- }
- username = username.trim();
- password = password.trim();
- String errMessage = null;
- if(username.equals("") || password.equals("")){
- errMessage = "User name or password can't be empty!";
- }else{
- String pass = MAP.get(username);
- if(pass == null){
- errMessage = "User doesn't exist!";
- }else if(!password.equals(pass)){
- errMessage = "Wrong password!";
- }
- }
- if(errMessage == null){
- return new ModelAndView("success", "username", username);
- }else{
- return new ModelAndView("failure", "errMessage", errMessage);
- }
- }
- }
- <bean id="login.do" class="com.google.mvc.web.sample.LoginController" />
1.默认登录页面实现(login.jsp):
- <%@ page contentType="text/html; charset=gb2312" language="java" import="java.sql.*" errorPage="" %>
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
- <title>Login Page</title>
- </head>
- <body>
- <table width="400" border="1" align="center">
- <form action="login.do" method="post" enctype="application/x-www-form-urlencoded">
- <tr>
- <td>User Name:</td>
- <td><input name="username" type="text" /></td>
- </tr>
- <tr>
- <td>Password:</td>
- <td><input name="password" type="password" /></td>
- </tr>
- <tr>
- <td colspan="2"><button type="submit">Submit</button></td>
- </tr>
- </form>
- </table>
- </body>
- </html>
- <b>Welcome, ${username}</b>
- <%@ page contentType="text/html; charset=gb2312" language="java" import="java.sql.*" errorPage="" %>
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
- <title>Login Page</title>
- </head>
- <body>
- <form action="login.do" method="post" enctype="application/x-www-form-urlencoded">
- <table width="400" border="1" align="center">
- <tr>
- <td>User Name:</td>
- <td><input name="username" type="text" /></td>
- </tr>
- <tr>
- <td>Password:</td>
- <td><input name="password" type="password" /></td>
- </tr>
- <tr>
- <td colspan="2"><button type="submit">Submit</button></td>
- </tr>
- <tr>
- <td colspan="2" style="color:#ff3300" mce_style="color:#ff3300">${errMessage}</td>
- </tr>
- </table>
- </form>
- </body>
- </html>
尽管我们的MVC框架功能比较简单,但是扩展能力还是很强的,因为我们的目地是为了通过源码实现去更好地掌握MVC框架的实现原理,所以我们将不对此框架进行优化,毕竟,现在成熟而优秀的MVC框架已经有很多了。