MVC模式
MVC,即模型-视图-控制器,是一种用于用户界面设计/展示层的架构模式,起初出现在Smalltalk。MVC模式的核心是利用控制器作为中介,将视图和模型解耦。
MVC模式中三者的职责分工明确:
- 模型(Model):持有所有的数据、状态和程序逻辑,模型不会感知到视图和控制器(下层组件不依赖于上层)。模型提供了操作和检索状态的接口,并且将状态改变信息推送给观察者(视图),在某些设计中,视图需要的数据被一同推送
- 视图(View):用来呈现模型,视图通常直接从模型中获取它需要显示的状态和数据
- 控制器(Controller):负责获取用户输入,并解读其对于模型的意义。控制器也可能直接要求视图做出改变。控制器不应当包含业务逻辑
注意和传统的模型含义不同,MVC中的模型不仅仅是指数据对象。以播放器为例,播放状态、当前歌曲、播放行为等都属于模型的组成部分。
三者的调用关系图如下:
上图虽然有箭头从Model指向View,但是这是一种松耦合的、基于观察者模式的通知方式。此外,某些设计中把Controller也作为Model的观察者,图中没有标出。
一般MVC中会用到三种设计模式:
- 观察者模式:Model作为事件源,View、Controller作为监听器
- 策略模式:对于View来说,Controller是具体策略,如果需要改变行为,可以更换一个Controller
- 组合模式:View中的GUI控件一般都呈现出树状组合关系
经典的MVC模式适用于单机桌面程序的开发,由于Model、View运行在统一进程中,因而互相之间基于函数调用的通信很方便。在常见的Web应用场景中,Model是无法通知到View的,因为:
- 在使用服务器端脚本(PHP、JSP、ASP等)作为视图组件的情况下,这些视图都是一次性生成的过程,结果是HTML代码,并传递到浏览器端运行。这些视图无法后续接收通知(调用)
- 在使用RIA时,View完全运行在浏览器中,View与服务器端只有数据的交互,同样不能接收Model的通知(调用)
为了解决这一问题,MVC2架构出现了,MVC2将View和Model完全解耦,两者之间的交互完全通过Controller间接完成。MVC2中三者的调用关系图如下:
随着基于JavaScript的UI框架以及WebSocket等协议的兴起,现在很多应用抛弃了服务器端脚本技术。View和Controller之间的通信完全基于数据而不是函数调用,WebSocket支持双向通信,这意味着Model在数据发生变化时即时通知Controller,由后者推送给View成为可能。这样的应用场景导致MVC架构又有了新的变化:
JavaEE中的Servlet与JSP技术可以用来构建MVC模式,其中Servlet充当Controller、JSP充当View、后端的Bean则充当Model:
Struts 是早期的Java Web框架,其内置了MVC模式的支持。Struts的核心接口是Action,它是一个HTTP请求与相应业务逻辑之间的适配器,作为Controller的RequestProcessor负责把请求分发给合适的Action;Struts没有对View和Model建模,但是在视图方面提供了多种标签库,简化开发。在Struts,ActionForm被用来作为数据传输对象,可以在Model和View之间传递信息。Struts的核心类图如下:
Struts2源于Webwork框架,它朝向零配置的方向发展,原先需要通过XML进行的配置,现在都可以使用Annotation完成。在Struts2中Controller的角色由FilterDispatcher承担,与Struts1中的ActionServlet不同,该类是一个Servlet过滤器;Model角色部分由Action类承担;View角色则部分由Result接口承担。
Struts2最大的不同是,Action没有接口约束,它的角色(该角色实质是由它的一个方法承担)也变成Model,框架通过反射调用之。Action类上可以通过Annotation附加大量元数据信息,例如Action负责哪些请求的处理,请求处理完毕后转向什么页面,下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import org.apache.struts2.convention.annotation.Action; import org.apache.struts2.convention.annotation.ExceptionMapping; import org.apache.struts2.convention.annotation.ExceptionMappings; import org.apache.struts2.convention.annotation.Namespace; import org.apache.struts2.convention.annotation.ParentPackage; import org.apache.struts2.convention.annotation.Result; import org.apache.struts2.convention.annotation.Results; import com.opensymphony.xwork2.ActionSupport; @ParentPackage ( "struts-default" ) @Namespace ( "/web-manager" ) @Results ( { @Result ( name = "success", location = "/main.jsp" ), @Result ( name = "error", location = "/error.jsp" ) } ) @ExceptionMappings ( { @ExceptionMapping ( exception = "java.lang.RuntimeException", result = "error" ) } ) public class LoginAction extends ActionSupport { private static final long serialVersionUID = 1L; @Action ( "login" ) public String login() throws Exception { return SUCCESS; } @Action ( value = "add", results = { @Result ( name = "success", location = "/index.jsp" ) } ) public String add() throws Exception { return SUCCESS; } } |
Struts2的类图如下(使用带标签的序号说明了某个场景下的调用顺序):
可以看到,Struts2处理HTTP请求的大致流程如下:
- FilterDispatcher拦截到来自Servlet容器的HTTP请求,创建ActionContext,该对象是Action执行的上下文,并依据HttpRequest和ConfigurationManager获取ActionMapping,然后调用Dispatcher.serviceAction(),进行分发处理
- ActionMapping知道哪个类的方法负责处理当前请求,Dispatcher根据ActionMapping创建extraContext,这是一个简单的Map,然后根据extraContext创建ActionProxy,后者是代理模式的实现。Struts2使用的代理类是StrutsActionProxy。Dispatcher调用ActionProxy.execute(),将请求的处理权转给StrutsActionProxy
- StrutsActionProxy没有任何状态,因而不需要为每次请求重新创建它,该代理会在请求处理前后设置/恢复线程本地的ActionContext信息,这样在整个处理期间,ActionContext都不需要通过方法参数传递了。代理通过调用ActionInvocation.invoke(),转移请求处理器
- ActionInvocation是Structs2请求处理的核心,主要干三件事情:
- 调用Interceptor链条,对请求进行处理,这里是职责链模式的应用
- 通过反射调用Action的方法,Action是用户程序的主要扩展点
- 调用Result,处理视图
Spring的MVC框架和Struct2的思想很相似,都是倾向于走向零配置、大量使用注解。Spring MVC中的Controller类与Struct2的Action在功能上是相当的,但是Spring MVC倾向于将其划分到C而不是M;DispatcherServlet是Spring MVC的(核心)控制器角色;ModelAndView类对V、M进行了通用建模,不过很明显了,Spring MVC把Model的概念弱化了,在它这里,M仅仅是DTO。
SpringMVC的处理流程比Struts2清晰简明,逻辑一直把握在DispatcherServlet手上:
- DispatcherServlet使用模板方式模式的变体来组织整个请求处理过程,它定义了整体处理算法:请求预处理⇨设置属性⇨调用拦截器预处理⇨调用Handler⇨调用拦截器后处理⇨处理渲染⇨调用拦截器完成处理。这些具体步骤中,很大一部分交由用户提供的拦截器、Handler完成。Handler可以是任意Java类,只需要将其标记@Controller注解并扫描。
- DispatcherServlet能够接受到Spring事件通知,监听器是它父类的一个内部类ContextRefreshListener,该内部类转调了DispatcherServlet的onApplicationEvent
- DispatcherServlet设置当前HttpServletRequest对象的若干属性,例如它关联的ApplicationContext
- DispatcherServlet执行拦截器链条,这是一个职责链模式的变体,具有3条职责路径(预处理、后处理、完成后处理),首先执行preHandle()进行预处理
- DispatcherServlet调用HandlerAdapter.handle()处理请求,最常用的HandlerAdapter实现是AnnotationMethodHandlerAdapter,基于注解使用Spring MVC时,会自动使用该类。从名字上可以看出,这是一个适配器模式,它基于反射机制,对Handler继续完全的适配:
- Handler的入参数量、类型都是任意的,HandlerAdapter需要把HttpServletRequest、HttpServletResponse等对象适配为这些类型。AnnotationMethodHandlerAdapter依靠反射机制和各种注解完成这一适配
- Handler的返回值类型是任意的,HandlerAdapter需要将其适配为ModelAndView类型
- DispatcherServlet执行拦截器链条的postHandle()进行后处理
- DispatcherServlet调用View.render()执行渲染
- DispatcherServlet执行拦截器链条的afterCompletion()进行最终处理
Spring MVC的主要类图如下:
下面是一个简单的Handler的例子,可以看到和Struts的Action是很类似的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping ( "/web-manager" ) public class LoginController { @RequestMapping ( value = "login/{mode}", method = { RequestMethod.POST } ) @ResponseBody public Object login( @PathVariable String mode, @RequestParam String uname ) { return null; } } |
ExtJS是一个基于JavaScript的框架,它提供了大量基于Web的UI控件,ExtJS还内置了MVC模式,具体可以参考:ExtJS 4的MVC框架
在这里我们可以注意到,理解MVC的概念不能死板。假设我们同时使用Spring MVC和ExtJS MVC,那么这两个框架什么关系呢?岂不是重复了?实际上,对于ExtJS来说,整个服务器端应用程序都属于M的一部分;而对于Spring来说,整个ExtJS和Spring的JsonView都属于V的部分。角度不同,思考也就不同,所谓横看成岭侧成峰嘛。
Leave a Reply