基于Spring的MVC框架设计与实现

来源:互联网 发布:金融数据来源有哪些 编辑:程序博客网 时间:2024/04/29 15:46

说明:这篇博客中并非讲SpringMVC,而是讲述我曾经实现过的一个MVC框架,它利用Spring框架作为基础框架,并吸收了部分其它框架中的思想,结合了自己的一些想法。此框架的代码算不上复杂,也不如常用MVC框架那么强大,它只是作为我重新发明轮子的一个成果。设计这个框架起源于在老师那里做过的一个项目,Turbine的设计给了我很大的启发,实现后,给它取了个名叫WebSide,并作为毕业设计中的一部分,下文是从毕业设计中选出,标题与图表都没有经过修改,且其中非常少量内容并非原创,请读者原谅。

2.1 MVC模式WebSide框架基本特性

MVC 是一种使用 MVCModel View Controller 模型-视图-控制器)设计创建交互式应用程序的模式,MVC模式在GUI程序中有很广泛的应用

Model(模型)表示应用程序核心(比如数据库记录列表)。是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据,程序的业务逻辑等。

View(视图)显示数据(数据库记录)。是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的。

Controller(控制器)处理输入(写入数据库记录)。是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

MVC 分层同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。传统MVC模式的示意图如图2.1。


在传统MVC模式中,控制器作为用户与系统功能之间的桥梁,控制器用于处理用户输入,如键盘,按钮的点击等,而模型与视图之间则使用了观擦者模式,当模型中的状态发生变化时,会通知给所有与模型关联的视图,以修改视图的显示。

Web应用中,MVC模式有所变化,由于Web应用无状态的特点,模型与视图之间不需要使用观擦者模式。Web MVC模式的示意图如图2.2所示。


FrontController又叫中心控制器,它的作用主要是请求的分发。在Web应用中,事件是http请求,而不是用户的键盘或者鼠标输入,应用在接收到请求后,中心控制器可以做请求的通用处理,比如做一些验证,将参数封装成对象等,然后利用一定的规则将请求委派给Controller做进一步处理。

Web MVC框架的最核心的功能是请求分发,WebSide在实现请求分发功能的同时,实现了以下特性: 

1. 对请求的拦截加入分支与try-catch-finally逻辑,使请求的拦截更灵活;

2. 利用Spring的父子容器功能,让每一个模块有一个独立的依赖注入容器,且每个模块的依赖注入容器共用同一个父容器,保证各模块之间的独立性;

3. 可配置的静态数据,如有些数据可以在整个应用中使用,有些数据只能在某一个模块中使用;

4. 视图请求视图,而不是直接请求Controller,实现视图与控制器的间接依赖,但也可在控制器中做跳转;

5. 实现多视图的支持,一个请求可以指定它需要什么样的数据

6. 实现将视图中需要的数据的处理与Action分离,且可能根据请求指定的数据类型自动调用对应的视图处理方法;

7. 对HTTP相关的对象的包装,因为Actionsingleton范围,而HttpServletRequestrequest范围,HttpSession则是sesison范围,对它们进行包装后,使request和sesison以被注入到Action中;

8. 支持多种使用方式,多种使用方式可以同时使用。

2.2 WebSide的使用方式

上面说到了WebSide支持多种使用方式,且它们不是互斥的,而是可同时使用的。

2.2.1 多视图

这是WebSide设计的初衷之一,但它是框架的使用方式中最复杂的一种。抛开pipeline细节,它将ActionController当作是从一个视图跳到另一个视图的过程中的两个不同的拦截器,Action与Screen都是可选。且请求需要严格遵守框架的约定。这种请求方式如图2.3所示。请求所返回的资源与请求的urlevent,type参数相关,event代码了需要执行的Action中的方式,typeevent参数决定Screen中调用的方法,而type决定了返回的数据的类型以及返回数据所在的目录,url最后一级(或者resource参数)表示资源名,resourceevent决定了返回给请求者的文件名。


2.2.2 使用注解简化多视图的使用

由于最初设计的多视图渲染的使用方式过于复杂,对于返回jsonxml样的数据时,有时候可以直接使用jacksonxstream工具将对象序列化成为json或者xstream所以在最初定义的约定的基础上,添加了在Screen中使用注解做视图渲染的使用方式。如图2.4中的代码段所示。


当在Screen中的方法上加上@Xml或者@Json,并给Screen上的方法加上返回值后,不需要再严格按照约定在对应的xml目录和json目录下存放一个资源文件。WebSide支持自定义视图渲染注解,任何时候都可以使用自定义注解处理Screen返回的数据。这种方式与前一种方式差不多。

2.2.3 只使用Screen或者Action

Screen虽然是可选的,但它与请求的资源绑定,如果只使用Screen,可以保证在请求某个资源前,需要的逻辑一定被执行。但只使用Screen在处理请求数据上不太方便。Screen主要用于视图渲染前的数据处理。

Action也是可选的,但Action的执行需要用户指定,使用Action处理请求与业务逻辑的调用比使用Screen要灵活,在不需要使用多视图渲染的情况下可以使用。

2.2.4 使用注解与Action

在不需要支持多视图渲染的情况下,使用Action与注解是比较简单的一种使用方式。能在Screen上使用的注解同样也可以使用在Action上。这种情况下,pipeline不会再执行Screen中的方法。目前支持的注解有@ToString、@Json@Xml@Forward@Redirect@Stream,任何时候都可以很方便的给它添加其它注解。

2.3 设计理念

Web MVC框架需要实现的功能主要是请求的分发以及请求在到达Action在Pipeline中的处理过程。常见的MVC框架Struts2和SpringMVC中,页面请求一个Action或者Controller,在Action中返回数据来确定跳转在哪一个视图。WebSide框架的实现过程中,页面请求的不是Action,而是一个资源或者视图,这里的ActionStruts2中的Action有较多的不同,同时,将Struts2中的Action拆分成了两个部分(ActionScreen,其中Action负责处理请求,调用或执行业务逻辑,而Screen则是与资源绑定,负责不同的资源类型的渲染前的数据处理,如在Web上,登陆成功后直接返回主页,而在移动APP上可能需要返回json或者xmlScreen中通过Action执行返回的数据可以在不同的方法中分别对jsonxmlvm/html/jsp数据的处理,这里的资源表示一切可返回给客户端的视图),当然,在Action/Screen中也可以做视图的跳转或者重定向。例如功能设置头像,用户在上传头像后需要跳转到index.vm个资源上,如果使用Struts2,则处理流程是页面先请求头像设置Action,然后Action中处理上传的图片文件并调用业务逻辑完成头像的设置,最后返回一个字符串,代表要跳转的资源,最终用户将看到index.vm个资源,如果在开发的过程中,页面请求的Action还不存,则在开发前端的时候,点击设置头像后会出错,无法看到index.vm需要将Action正确编写后才能完成index.vm显示。如果页面不是直接请求Action,而是请求index.vm个资源,但将Action作为参数指定,WebSide框架在处理请求分发的时候,如果Action不存在,则不执行这个Action那么只要index.vm个资源存在,则不管处理头像的后端逻辑是否存在,都可以看到最后的index.vm资源,这样就可以将前端的开发与后端完全分离,相互不受影响。而Screen,它作为视图渲染前的处理,比如可能需要返回xmljson种格式的数据,如果使用Struts2或者SpringMVC,则需要在Action或者Controller中做判断,处理xml或者json生成,如果把处理视图相关的代码放在Screen中,不同的视图可以用不同的方法中做处理,由框架决定Screen的调用,避免了在Action中加入过多与不同的视图的处理相关的代码。Screen虽然是与资源绑定在一起,但也不是必须的,浏览器在请求一个资源时,可以指定一个ActionScreen也可以看作是与Action之间的一种约定,这一点体现在Action中的方法在调用结束后需要返回一个什么样的对象,也就是说Action的执行结果,Screen将通过Action的返回对象来决定视图渲染相关的数据。

Pipeline的设计仿照的Turbine,但没有那么复杂,它更像是一种变形了的Struts2的拦截器栈,只是多了一些逻辑判断,可以根据请求的后缀来执行不同的子管道,拥有try-catch-finally管道等。

WebSide框架中,MVC模式的示意图如图2.5.

2.3.1 框架的本质

应用框架让人联想到建筑的框架。

•建筑框架确定了整个建筑的结构;应用框架确定了应用的结构。

•建筑框架允许你在不改变结构的基础上,自由改变其内容。例如,你可以用墙体随意分隔房间。应用框架允许你在不改变整体结构的基础上,自由扩展功能。

可以这样说,框架的本质就是“扩展”,一个软件框架必须符合如下要素(维基百科):

反转控制(IoC

应用的流程不是由应用控制的,而是由框架控制的

默认行为

框架会定义一系列默认的行为

扩展性

应用可以扩展框架的功能,也可以修改框架的默认行为

框架本身不可更改

框架在被扩展时,自身的代码无须被改变

 

2.3.2 层次化

设计良好的模块,应该是层次化的。

模块B扩展了模块A,同时被模块C扩展。这样就形成了ABC三个层次。

层次之间有如下的关系:

•下层定义规则,上层定义细节;(上层、下层也可称为外层、内层)

•下层是抽象的,上层是具体的

•越下层,越稳定(越少改变);越上层,越易变

•依赖倒转(Dependency Inversion)。上层(具体)依赖下层(抽象),而不是下层依赖上层

•上层扩展下层时,不需要修改到下层的任何代码和配置。即符合开闭原则

•每一层均可被替换

2.4 层次化的设计

切分功能。每个组件专心做一件事。

分析哪些会改变,哪些不会改变。不变部分固化在组件中,可能会改变的部分抽象成接口,以便扩展。

考虑默认值和默认扩展。默认值和默认扩展应该是最安全、最常用的选择。对于默认值和默认扩展,用户在使用时可使用默认配置,不需要额外的配置。

2.5设计思路

2.5.1 框架中的术语

2.1 框架中的术语

术语

含义

是否必须

Action

处理HTTP请求的单元,类似于Struts2中的Action。应用中的逻辑应该在Action中执行或者调用。Action类需要配置在它所在的模块对应的mvc.xml文件中。

否,一次请求可以没有Action

Screen

视图渲染器,命名来自于Turbine框架,但在程序运行时与Turbine中有所不同,一个Screen与一个资源对应。Screen用于处理与返回数据相关的动作。如果需要返回json,则可在Screen中处理json的生成中,如果需要返回XML,也可在Screen中处理XML的生成,如果在其它类型的视图(jsp,模板文件等),则可在Screen中设置视图上需要的变量等。Screen中执行的方法与返回的数据类型对应,如果请求一个Action可能需要返回多种数据类型,则每种返回类型对应一个渲染方法,如果没有找到返回类型对应的方法,则调用默认方法(execute)进行处理。可以在渲染方法中设置数据类型中的变量,将数据模板写在WebRoot中对应的文件里,然后可通过模板引擎或者JSP将变量应用到视图上。不需要配置,但需要遵守约定。在调用Screen上的方法时,先调用名为${event}${type}的方法,如果该方法调用失败(方法不存在)则调用名为${type}的方法,如果调用失败则调用execute方法。

否,一次请求可以没有Screen

视图

返回的结果,如JSPHTMLVMJSONXML等。

需要,如果跳转到视图失败,框架则会将控制权返还给Servlet容器,让容器决定请求的资源,以便执行Servlet或者返回其它静态资源。

Valve

阀门,类似于Struts2的拦截器。

需要,可使用默认配置,也可修改配置以执行不同的Valve

Pipeline

管道,功能上类似于Struts2中的拦截器栈,但与Struts2的拦截器栈有很大的不同。

需要,可使用默认配置,也可修改默认配置。

请求上下文

RequestContext,有三种请求上下文,1.Request请求上下文,表示当前请求的运行环境,其中的数据只在一次HTTP请求的过程中有效;2.Module请求上下文,表示请求一个模块时的运行环境,是Request请求上下文的父上下文,每个模块只有一个Module请求上下文;3.Application请求上下文,是Module请求上下文的父上下文,整个应用中只有一个。

RunData服务提供。

返回结果重写

由客户端(浏览器)指定返回的数据类型(json,html等),在服务商将其映射到另一种数据类型。

需要配置

服务

Service,将框架中的核心功能独立,形成服务,服务是对框架核心功能的封装。

需要,可使用默认配置。

服务管理器

ServiceManager,服务的数量有多个,服务管理器用于对服务进行统一管理。

需要,由服务管理器工厂提供。

模块装载服务

服务的一种,用于装载每个模块,初始化每个模块的应用上下文。

需要,在service配置文件中进行配置,可使用默认配置。

管道服务

服务的一种,用于初始化Pipeline

需要,可使用默认配置。

RunData服务

服务的一种,提供请求上下文对象。

需要,可使用默认配置。

返回结果重写服务

result-type-rewrite-service,提供返回结果重写的功能。

需要,可使用默认配置。

普通服务

Generic Service,由应用程序指定

不必须

2.5.2 WebSide执行的基本流程

2.7是个简单的执行流程图,各个部分表示的意义如下:

FrameworkFilter:一个Servlet过虑器的实现,作为中心控制器,主要完成框架的初始化以及请求分发功能。所有需要框架处理的请求都需要经过FrameworkFilter。选择Servlet过虑器而不是Servlet的原因在于过虑器有控制返还能力(如果请求不应该由框架处理,则可将控制权返还能Web容器),而Servlet没有。

Pipeline:管道,每个管道可以拥有子管道。

TryCatchFinallyValve:用于控制程序执行的阀门,正常情况下执行Try对应的子管道完成程序流程,如果发生了异常则执行Catch对应的子管道进行异常处理,运行完后执行Finally对应的子管道进行清理操作。

IfElseIfValveURL后缀条件判断阀门,每一个if分支都是一个子管道,如果请求的URL的后缀符合一个分支,则执行分支对应的子管道。

ActionInvokeValve:用于执行Action的阀门。

其它Valve:其它用途的阀门,如:ActionInvokeVale执行之后可以执行ScreenInvokeValve用于执行Screen,然后执行ForwardValve跳转到相应的视图。

ServiceManager:服务管理器,在框架初始化的时候读取WEB-INF下的service.xml文件初始化服务。


2.5.3 WebSide框架设计的三个层次

如图2.8所示,底层使用Spring框架作为基础框架,Spring的依赖注入功能可以不受影响的使用。

中间层:扩展了Spring配置文件中的标签,主要用于实现管道和各种服务的配置以及初始化。

最外层:基于Servlet API实现。


2.6 WebSide框架的设计细节

2.6.1 Action的设计

1.Action:同一个Action类只有一个实例;

2.Action中需要执行的方法可以有任意多个

3.Screen:同一个Screen类只有一个实例;

4.Screen中调用的是execute方法,带有一个请求上下文(RequestContext)参数;

5.Request讲求上下文通过参数传入ActionScreen的方法调用中;

6.如果Action或者Screen中需要使用HttpServletRequestHttpSession或者HttpServletResponse可使用Spring的依赖注入功能注入到类的属性中;

2.6.2 HTTP对象的包装

因为依赖注入有个限制:只有大范围的对象才能注入到小范围的对象中,反之则不能注入。

Spring管理对象时给对象定义了4种范围:singletonsessionrequestprototypeSingleton范围大于session范围,session范围大于request范围,request范围大于prototype范围,所以prototype范围的对象不能注入到requestsessionsingleton范围的对象中,request范围的对象不能注入到sessionsingleton范围的对象中,session范围的对象不能注入到singleton范围的对象中。

由于ActionScreen是单例,而HttpServletRequestHttpServletResponserequest范围,HttpSession则是session范围的对象,若想实现http对象注入到singleton范围的对象中,可把http对象包装成单例,将包装后的对象注入到singleton范围的对象中,在包装后的对象上调用方法时,将方法的调用代理到真实对象上。

2.6.3 模块与模块装载服务

如果应用使用了WebSide框架,则应用中每个模块拥有一个单独的依赖注入容器,所有的模块共享同一个父依赖注入容器

其中,父容器中的对象可以注入到子容器的对象中,而子容器中的对象无法注入到父容器的对象中。各个子容器相互独立,不同子容器中的对象不可相互注入。

假设一个应用分为两个模块,分别是homeabout,则模块的配置文件结构如图2.10所示:

Mvc-root.xml是父容器的配置,每一个模块对应一个子文件夹,文件夹名与模块名相同,每个模块的配置在web-inf下模块名对应的子文件夹中。

WEB-INF下有需要有一个service.xml配置文件,该配置文件中配置的是所有的服务信息。


2.6.4 PipelinePipeline服务

管道控制着应用的执行流程。PipelineStruts2的拦截器栈类似,但又有很多不同之处,在Struts2中,拦截器栈中的拦截器顺序执行,当一次HTTP请求发生时,默认拦截器栈中的所有拦截器都会执行一次,但Pipeline在流程控制上更强大,Pipeline中的Valve可触发子Pipeline的执行,可在正常情况下执行一部分Valve(类似于一个拦截器栈),在发生异常时执行一部分异常处理Valve(另一个拦截器栈),最后执行指定的清理Valve(另一个拦截器栈)。也可以根据请求的URI的后缀来指定执行的Valve(类似于后缀条件对应一个拦截器栈)。

PipelineService(管道服务)的主要功能是通过pipeline.xml配置文件初始化管道,并提供获取管道的接口。

每一个服务都可以被替换,并且应用可以实现Service接口,并配置自己的服务,在给WebSide增加新功能时可以使用自定义服务。

2.6.5 视图渲染

Action执行后,框架将返回一个视图给浏览器(或者客户端),返回的视图类型由请求指定。返回的数据类型可能包含多种,每种数据类型的格式不同,在返回数据前由请求的视图对应的Screen进行渲染(数据格式的生成或者绑定的变量的数据),Screen中调用的方法名与返回的视图类型一一对应。

这种方式将返回数据的处理与Action分离,在Action中不需要写与视图类型相关的判断代码。

2.6.6 返回类型重写与返回类型重写服务

Screen中调用的渲染方法与返回类型相关,返回类型重写的作用是将请求参数中指定的返回返回类型映射到另一种数据类型(重写),在渲染视图时,在Screen上调用的方法的方法名与重写后的返回类型名相同。

参数中指定的返回类型经过重写后,由返回类型处理器来处理视图的跳转(跳转的视图由重写后的数据类型决定),如:返回HTML则直接跳转;返回模板文件则先由模板引擎进行渲染。

返回类型重写服务在初始化时从result-type-rewrite.xml配置文件中读取返回类型重写与返回类型处理器相关的映射信息,并可为Valve提供返回类型重写功能接口。

2.6.7 服务与服务管理器

服务是WebSide中,每一个组件化的核心功能,每一个服务都是可以替换的。

框架提供的每一个服务都封装了一个核心功能,应用也可以定义服务,每一个服务都需要在service.xml文件中进行配置,

服务管理器的作用是能过service.xml初始化每个服务,并提供获取服务的接口。框架在初始化的过程中,会初始化服务。服务共同组成了框架的基础功能,服务初始化后,在任何地方都可以通过服务管理器获取服务的实例。

2.8 框架的实现

2.8.1 扩展SpringApplicationContext

2.8.1.1 Spring依赖注入容器的基本结构的研究

Spring中,依赖注入容器的实现是BeanFactory的实例,ApplicationContext的实例对BeanFactory进行了功能上的增强。

Spring依赖注入容器的简单类图(只有相对重要的几个接口和类):

BeanFactory接口中定义了依赖注入容器的基本方法,DefaultListableBeanFactoryBeanFactory接口的一个比较重要的实现类,DefaultListableBeanFactory类还实现了SingletonBeanRegistry接口,SingletonBeanRegistry接口的作用是向依赖注入容器内注册单例的Bean,如果想要在代码中注册单例的Bean(如:包装后的HttpServletRequest类),则可以通过此接口完成。AbstractRefreshableConfigApplicationContext类实现了ApplicationContext接口中的多数方法,并封装了依赖注入容器的主要功能。Spring IoC容器的简要结构如图2.16所示。


2.8.1.2 扩展ApplicationContext

SpringApplicationContext接口及其实现类中,大量的使用了模板方法模式,可以在不改变依赖注入容器的程序主要逻辑的情况下,改变依赖注入容器的某些行为。例如:继承类XmlWebApplicationContext类,覆盖其中的getDefaultConfigLocations不改变依赖注入容器的程序主要逻辑的情况下,改变依赖注入容器的某些行为。例如:继承类XmlWebApplicationContext类,覆盖其中的getDefaultConfigLocations方法,可让自己的ApplicationContext接口的实现类在创建时通过指定的配置文件初始化依赖注入容器。

模块装载服务在初始化时将各模块的依赖注入容器与根容器初始化,并进行统一管理。对ApplicationContext的扩展基本类图如图2.17。

2.8.2 扩展Spring配置标签

WebSide中,使用Spring框架管理对象,包括服务,pipeline欲使Spring支持WebSide所需功能,需要对Spring中的标签做扩展。

2.8.2.1 Spring的配置文件解析过程研究

Spring中,配置文件使用XML文件,XML中,每一个标签有它所在的命名空间,Spring先根据命名空间查找到命名空间处理器,然后通过命名空间处理器查找命名空间中每一个标签的标签解析器,调用标签解析器向容器中注册BeanDefinition的实例。

Spring的配置文件的读取由BeanDefinitionDocumentReader接口完成,该接口的实现类利用NamespaceHandlerResolver接口实现了命名空间处理器的定位(命名空间与命名空间处理器存储在HashMap中),每一个命名空间处理器可继承NamespaceHandlerSupport类并覆盖init方法给每一个标签注册标签解析器。NamespaceHandlerSupport类中通过HashMap存储命名空间中标签与标签解析器的映射。

2.18是Spring解析配置文件的过程的简要类图,Spring解析配置的过程虽然复杂,但却达到了非常好的可扩展性。我们只需要利用NamespaceHandlerBeanDefinitionParser即可给Spring定义新的配置,增加新的功能。


2.8.2.2 Spring标签的扩展

Spring的命名空间处理器与标签解析器的基本结构如图2.19所示:


如果需要添加新的标签,则需要先添加新的命名空间,将标签相关的信息在该命名空间的xsd文件中进行描述。然后为命名空间实现命名空间处理器,不需要从头开始实现,可以继承自Spring中的NamespaceHandlerSupport类,覆盖init方法。可以在init方法中注册所有的标签解析器。为每一个需要返回<beans>标签下的子标签写一个标签解析器,此标签解析器需要实现BeanDefinitionParser接口,在该实现类的parse方法中通过XML元素Element对象中的配置信息向ParserContext注册BeanDefinition对象。

本框架中新增了actionservicepipelineresult-type-rewrite四个命名空间,在新增了命名空间与标签后,需要在项目的META-INF文件夹中按照Spring的方式添加3个以键值对存储数据的文件,其中spring.handlers中存储的是命名空间和命名空间处理器的映射关系,spring.schemas中存储的是命名空间与xsd文件的位置的映射关系。Actionserviceresult-type-rewrite命名空间中的标签解析器在注册BeanDefinition对象时,使用了by-Name的方式进行依赖注入,如果需要向Action类,Screen类,Service类或者ResultTypeHandler类中注入属性可以在各自的配置文件中配置与属性名相同的bean进行默认注入。

2.8.3 框架的扩展——对Velocity的支持

扩展默认的中心控制器FrameworkFilter,编写其子类VelocityBasedFrameworkFilter这个类不是必须,可以不使用。VelocityService会在Web容器启动时初始化Velocity

编写ResultTypeHandler接口的实现类(可参照VelocityServlet类),在此实现类中实现利用Velocity渲染模板文件并输出到浏览器;

实现一个新的RequestContext的实现类VelocityBasedRequestContext,将RequestContext中设置<key, value>对的功能利用VelocityContext实现。此实现类还需要实现Velocity中的Context接口,以便使RequestContextVelocity兼容;

实现一个新的RequestContext工厂,可继承自DefaultRequestContextFactory类,在新的类中,获取Request范围的RequestContext时返回VelocityBasedRequestContext的对象。并覆盖VelocityBasedFrameworkFilter类的getRequestContextFactory方法,使之返回新的工厂类DefaultRequestContextFactory的对象。

如果需要结合Velocity到应用中,只需将Web.xml中的Filter改为新的Filter,然后将VelocityResultTypeHandler配置到result-type-rewriter中即可。

Velocity的支持类似于Struts2的插件。如果想要支持其它表示层技术,可以使用相似的方式。

2.8.4 视图跳转

由请求中的参数action,eventreturn指定需要执行的actionaction中的方法和返回数据类型。

2.8.5 其它框架的帮助

1.ScreenpipelineValve的命名来源于ApacheMVC框架Turbine

2.Pipeline的设计与Turbine相似

3.返回类型重写的想法来源于数据库中的SQL重写


0 0