您的位置:首页 > 其它

设计REST风格的MVC框架[转]

2010-01-27 20:10 295 查看
传统的JavaEEMVC框架如Struts等都是基于Action设计的后缀式映射,然而,流行的Web趋势是REST风格的架构。尽管使用Filter或者Apachemod_rewrite能够通过URL重写实现REST风格的URL,为什么不直接设计一个全新的REST风格的MVC框架呢?本文将讲述如何从头设计一个基于REST风格的JavaMVC框架,配合Annotation,最大限度地简化Web应用的开发,您甚至编写一行代码就可以实现“Hello,world”。

Java开发者对MVC框架一定不陌生,从Struts到WebWork,JavaMVC框架层出不穷。我们已经习惯了处理*.do或*.action风格的URL,为每一个URL编写一个控制器,并继承一个Action或者Controller接口。然而,流行的Web趋势是使用更加简单,对用户和搜索引擎更加友好的REST风格的URL。例如,来自豆瓣的一本书的链接是http://www.douban.com/subject/2129650/,而非http://www.douban.com/subject.do?id=2129650。

有经验的JavaWeb开发人员会使用URL重写的方式来实现类似的URL,例如,为前端Apache服务器配置mod_rewrite模块,并依次为每个需要实现URL重写的地址编写负责转换的正则表达式,或者,通过一个自定义的RewriteFilter,使用JavaWeb服务器提供的Filter和请求转发(Forward)功能实现URL重写,不过,仍需要为每个地址编写正则表达式。

既然URL重写如此繁琐,为何不直接设计一个原生支持REST风格的MVC框架呢?

要设计并实现这样一个MVC框架并不困难,下面,我们从零开始,仔细研究如何实现REST风格的URL映射,并与常见的IoC容器如Spring框架集成。这个全新的MVC框架暂命名为WebWind。

术语

MVC:Model-View-Controller,是一种常见的UI架构模式,通过分离Model(模型)、View(视图)和Controller(控制器),可以更容易实现易于扩展的UI。在Web应用程序中,Model指后台返回的数据;View指需要渲染的页面,通常是JSP或者其他模板页面,渲染后的结果通常是HTML;Controller指Web开发人员编写的处理不同URL的控制器(在Struts中被称之为Action),而MVC框架本身还有一个前置控制器,用于接收所有的URL请求,并根据URL地址分发到Web开发人员编写的Controller中。IoC:Invertion-of-Control,控制反转,是目前流行的管理所有组件生命周期和复杂依赖关系的容器,例如Spring容器。

Template:模板,通过渲染,模板中的变量将被Model的实际数据所替换,然后,生成的内容即是用户在浏览器中看到的HTML。模板也能实现判断、循环等简单逻辑。本质上,JSP页面也是一种模板。此外,还有许多第三方模板引擎,如Velocity,FreeMarker等。

设计目标

和传统的Struts等MVC框架完全不同,为了支持REST风格的URL,我们并不把一个URL映射到一个Controller类(或者Struts的Action),而是直接把一个URL映射到一个方法,这样,Web开发人员就可以将多个功能类似的方法放到一个Controller中,并且,Controller没有强制要求必须实现某个接口。一个Controller通常拥有多个方法,每个方法负责处理一个URL。例如,一个管理Blog的Controller定义起来就像清单1所示。

viewsourceprint?

01.
//清单1.管理Blog的Controller定义

02.

03.
public
class
Blog{

04.
@Mapping
(
"/create/$1"
)

05.
Public
void
create(
int
userId){...}

06.

07.
@Mapping
(
"/display/$1/$2"
)

08.
Public
void
display(
int
userId,
int
postId){...}

09.

10.
@Mapping
(
"/edit/$1/$2"
)

11.
Public
void
edit(
int
userId,
int
postId){...}

12.

13.
@Mapping
(
"/delete/$1/$2"
)

14.
PublicStringdelete(
int
userId,
int
postId){...}

15.
}


@Mapping()注解指示了这是一个处理URL映射的方法,URL中的参数$1、$2……则将作为方法参数传入。对于一个“/blog/1234/5678”的URL,对应的方法将自动获得参数userId=1234和postId=5678。同时,也无需任何与URL映射相关的XML配置文件。

使用$1、$2……来定义URL中的可变参数要比正则表达式更简单,我们需要在MVC框架内部将其转化为正则表达式,以便匹配URL。

此外,对于方法返回值,也未作强制要求。

集成IoC

当接收到来自浏览器的请求,并匹配到合适的URL时,应该转发给某个Controller实例的某个标记有@Mapping的方法,这需要持有所有Controller的实例。不过,让一个MVC框架去管理这些组件并不是一个好的设计,这些组件可以很容易地被IoC容器管理,MVC框架需要做的仅仅是向IoC容器请求并获取这些组件的实例。

为了解耦一种特定的IoC容器,我们通过ContainerFactory来获取所有Controller组件的实例,如清单2所示。

viewsourceprint?

01.
//清单2.定义ContainerFactory

02.

03.
public
interface
ContainerFactory{

04.

05.
void
init(Configconfig);

06.

07.
List<Object>findAllBeans();

08.

09.
void
destroy();

10.
}


其中,关键方法findAllBeans()返回IoC容器管理的所有Bean,然后,扫描每一个Bean的所有public方法,并引用那些标记有@Mapping的方法实例。

我们设计目标是支持Spring和Guice这两种容器,对于Spring容器,可以通过ApplicationContext获得所有的Bean引用,代码见清单3。

viewsourceprint?

01.
//清单3.定义SpringContainerFactory

02.

03.
public
class
SpringContainerFactory
implements
ContainerFactory{

04.
private
ApplicationContextappContext;

05.

06.
public
List<Object>findAllBeans(){

07.
String[]beanNames=appContext.getBeanDefinitionNames();

08.
List<Object>beans=
new
ArrayList<Object>(beanNames.length);

09.
for
(
int
i=
0
;i<beanNames.length;i++){

10.
beans.add(appContext.getBean(beanNames[i]));

11.
}

12.
return
beans;

13.
}

14.
...

15.
}


对于Guice容器,通过Injector实例可以返回所有绑定对象的实例,代码见清单4。

viewsourceprint?

01.
//清单4.定义GuiceContainerFactory

02.

03.
public
class
GuiceContainerFactory
implements
ContainerFactory{

04.
private
Injectorinjector;

05.

06.
public
List<Object>findAllBeans(){

07.
Map<Key<?>,Binding<?>>map=injector.getBindings();

08.
Set<Key<?>>keys=map.keySet();

09.
List<Object>list=
new
ArrayList<Object>(keys.size());

10.
for
(Key<?>key:keys){

11.
Objectbean=injector.getInstance(key);

12.
list.add(bean);

13.
}

14.
return
list;

15.
}

16.
...

17.
}


类似的,通过扩展ContainerFactory,就可以支持更多的IoC容器,如PicoContainer。

出于效率的考虑,我们缓存所有来自IoC的Controller实例,无论其在IoC中配置为Singleton还是Prototype类型。当然,也可以修改代码,每次都从IoC容器中重新请求实例。

设计请求转发

和Struts等常见MVC框架一样,我们也需要实现一个前置控制器,通常命名为DispatcherServlet,用于接收所有的请求,并作出合适的转发。在Servlet规范中,有以下几种常见的URL匹配模式:

/abc:精确匹配,通常用于映射自定义的Servlet;

*.do:后缀模式匹配,常见的MVC框架都采用这种模式;

/app/*:前缀模式匹配,这要求URL必须以固定前缀开头;

/:匹配默认的Servlet,当一个URL没有匹配到任何Servlet时,就匹配默认的Servlet。一个Web应用程序如果没有映射默认的Servlet,Web服务器会自动为Web应用程序添加一个默认的Servlet。

REST风格的URL一般不含后缀,我们只能将DispatcherServlet映射到“/”,使之变为一个默认的Servlet,这样,就可以对任意的URL进行处理。

由于无法像Struts等传统的MVC框架根据后缀直接将一个URL映射到一个Controller,我们必须依次匹配每个有能力处理HTTP请求的@Mapping方法。完整的HTTP请求处理流程如下图所示。



当扫描到标记有@Mapping注解的方法时,需要首先检查URL与方法参数是否匹配,UrlMatcher用于将@Mapping中包含$1、$2……的字符串变为正则表达式,进行预编译,并检查参数个数是否符合方法参数,代码见清单5。

viewsourceprint?

01.
//清单5.定义UrlMatcher

02.

03.
final
class
UrlMatcher{

04.
final
Stringurl;

05.
int
[]orders;

06.
Patternpattern;

07.

08.
public
UrlMatcher(Stringurl){

09.
...

10.
}

11.
}


将@Mapping中包含$1、$2……的字符串变为正则表达式的转换规则是,依次将每个$n替换为([^\\/]*),其余部分作精确匹配。例如,“/blog/$1/$2”变化后的正则表达式为:^\\/blog\\/([^\\/]*)\\/([^\\/]*)$

请注意,Java字符串需要两个连续的“\\”表示正则表达式中的转义字符“\”。将“/”排除在变量匹配之外可以避免很多歧义。

调用一个实例方法则由Action类表示,它持有类实例、方法引用和方法参数类型,代码见清单6。

viewsourceprint?

01.
//清单6.定义Action

02.

03.
class
Action{

04.
public
final
Objectinstance;

05.
public
final
Methodmethod;

06.
public
final
Class[]arguments;

07.

08.
public
Action(Objectinstance,Methodmethod){

09.
this
.instance=instance;

10.
this
.method=method;

11.
this
.arguments=method.getParameterTypes();

12.
}

13.
}


负责请求转发的Dispatcher通过关联UrlMatcher与Action,就可以匹配到合适的URL,并转发给相应的Action,代码见清单7。

viewsourceprint?

1.
//清单7.定义Dispatcher

2.

3.
class
Dispatcher{

4.
private
UrlMatcher[]urlMatchers;

5.
private
Map<UrlMatcher,Action>urlMap=
new
HashMap<UrlMatcher,Action>();

6.
....

7.
}


当Dispatcher接收到一个URL请求时,遍历所有的UrlMatcher,找到第一个匹配URL的UrlMatcher,并从URL中提取方法参数,代码见清单8。

viewsourceprint?

01.
//清单8.匹配并从URL中提取参数

02.

03.
final
class
UrlMatcher{

04.
...

05.

06.
/**

07.
*根据正则表达式匹配URL,若匹配成功,返回从URL中提取的参数

08.
*若匹配失败,返回null

09.
*/

10.
public
String[]getMatchedParameters(Stringurl){

11.
Matcherm=pattern.matcher(url);

12.
if
(!m.matches())

13.
return
null
;

14.
if
(orders.length==
0
)

15.
return
EMPTY_STRINGS;

16.
String[]params=
new
String[orders.length];

17.
for
(
int
i=
0
;i<orders.length;i++){

18.
params[orders[i]]=m.group(i+
1
);

19.
}

20.
return
params;

21.
}

22.
}


根据URL找到匹配的Action后,就可以构造一个Execution对象,并根据方法签名将URL中的String转换为合适的方法参数类型,准备好全部参数,代码见清单9。

viewsourceprint?

01.
//清单9.构造Exectuion

02.

03.
public
class
Execution{

04.
public
final
HttpServletRequestrequest;

05.
public
final
HttpServletResponseresponse;

06.
private
final
Actionaction;

07.
private
final
Object[]args;

08.
...

09.

10.
public
Objectexecute()
throws
Exception{

11.
try
{

12.
return
action.method.invoke(action.instance,args);

13.
}

14.
catch
(InvocationTargetExceptione){

15.
Throwablet=e.getCause();

16.
if
(t!=
null
&&t
instanceof
Exception)

17.
throw
(Exception)t;

18.
throw
e;

19.
}

20.
}

21.
}


调用execute()方法就可以执行目标方法,并返回一个结果。请注意,当通过反射调用方法失败时,我们通过查找InvocationTargetException的根异常并将其抛出,这样,客户端就能捕获正确的原始异常。

为了最大限度地增加灵活性,我们并不强制要求URL的处理方法返回某一种类型。我们设计支持以下返回值:

String:当返回一个String时,自动将其作为HTML写入HttpServletResponse;

void:当返回void时,不做任何操作;

Renderer:当返回Renderer对象时,将调用Renderer对象的render方法渲染HTML页面。

最后需要考虑的是,由于我们将DispatcherServlet映射为“/”,即默认的Servlet,则所有的未匹配成功的URL都将由DispatcherServlet处理,包括所有静态文件,因此,当未匹配到任何Controller的@Mapping方法后,DispatcherServlet将试图按URL查找对应的静态文件,我们用StaticFileHandler封装,主要代码见清单10。

viewsourceprint?

01.
//清单10.处理静态文件

02.

03.
class
StaticFileHandler{

04.
...

05.
public
void
handle(HttpServletRequestrequest,HttpServletResponseresponse)

06.
throws
ServletException,IOException{

07.
Stringurl=request.getRequestURI();

08.
Stringpath=request.getServletPath();

09.
url=url.substring(path.length());

10.
if
(url.toUpperCase().startsWith(
"/WEB-INF/"
)){

11.
response.sendError(HttpServletResponse.SC_NOT_FOUND);

12.
return
;

13.
}

14.
int
n=url.indexOf(
'?'
);

15.
if
(n!=(-
1
))

16.
url=url.substring(
0
,n);

17.
n=url.indexOf(
'#'
);

18.
if
(n!=(-
1
))

19.
url=url.substring(
0
,n);

20.
Filef=
new
File(servletContext.getRealPath(url));

21.
if
(!f.isFile()){

22.
response.sendError(HttpServletResponse.SC_NOT_FOUND);

23.
return
;

24.
}

25.
long
ifModifiedSince=request.getDateHeader(
"If-Modified-Since"
);

26.
long
lastModified=f.lastModified();

27.
if
(ifModifiedSince!=(-
1
)&&ifModifiedSince>=lastModified){

28.
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

29.
return
;

30.
}

31.
response.setDateHeader(
"Last-Modified"
,lastModified);

32.
response.setContentLength((
int
)f.length());

33.
response.setContentType(getMimeType(f));

34.
sendFile(f,response.getOutputStream());

35.
}

36.
}


处理静态文件时要过滤/WEB-INF/目录,否则将造成安全漏洞。

集成模板引擎

作为示例,返回一个“<h1>Hello,world!</h1>”作为HTML页面非常容易。然而,实际应用的页面通常是极其复杂的,需要一个模板引擎来渲染出HTML。可以把JSP看作是一种模板,只要不在JSP页面中编写复杂的Java代码。我们的设计目标是实现对JSP和Velocity这两种模板的支持。

和集成IoC框架类似,我们需要解耦MVC与模板系统,因此,TemplateFactory用于初始化模板引擎,并返回Template模板对象。TemplateFactory定义见清单11。

viewsourceprint?

01.
//清单11.定义TemplateFactory

02.

03.
public
abstract
class
TemplateFactory{

04.
private
static
TemplateFactoryinstance;

05.
public
static
TemplateFactorygetTemplateFactory(){

06.
return
instance;

07.
}

08.

09.
public
abstract
TemplateloadTemplate(Stringpath)
throws
Exception;

10.
}


Template接口则实现真正的渲染任务。定义见清单12。

viewsourceprint?

1.
//清单12.定义Template

2.

3.
public
interface
Template{

4.
void
render(HttpServletRequestrequest,HttpServletResponseresponse,

5.
Map<String,Object>model)
throws
Exception;

6.
}


以JSP为例,实现JspTemplateFactory非常容易。代码见清单13。

viewsourceprint?

01.
//清单13.定义JspTemplateFactory

02.

03.
public
class
JspTemplateFactory
extends
TemplateFactory{

04.
private
Loglog=LogFactory.getLog(getClass());

05.

06.
public
TemplateloadTemplate(Stringpath)
throws
Exception{

07.
if
(log.isDebugEnabled())

08.
log.debug(
"LoadJSPtemplate'"
+path+
"'."
);

09.
return
new
JspTemplate(path);

10.
}

11.

12.
public
void
init(Configconfig){

13.
log.info(
"JspTemplateFactoryinitok."
);

14.
}

15.
}


JspTemplate用于渲染页面,只需要传入JSP的路径,将Model绑定到HttpServletRequest,就可以调用Servlet规范的forward方法将请求转发给指定的JSP页面并渲染。代码见清单14。

viewsourceprint?

01.
//清单14.定义JspTemplate

02.

03.
public
class
JspTemplate
implements
Template{

04.
private
Stringpath;

05.

06.
public
JspTemplate(Stringpath){

07.
this
.path=path;

08.
}

09.

10.
public
void
render(HttpServletRequestrequest,HttpServletResponseresponse,

11.
Map<String,Object>model)
throws
Exception{

12.
Set<String>keys=model.keySet();

13.
for
(Stringkey:keys){

14.
request.setAttribute(key,model.get(key));

15.
}

16.
request.getRequestDispatcher(path).forward(request,response);

17.
}

18.
}


另一种比JSP更加简单且灵活的模板引擎是Velocity,它使用更简洁的语法来渲染页面,对页面设计人员更加友好,并且完全阻止了开发人员试图在页面中编写Java代码的可能性。使用Velocity编写的页面示例如清单15所示。

viewsourceprint?

1.
清单15.Velocity模板页面

2.

3.
<
html
>

4.
<
head
><
title
>${title}</
title
></
head
>

5.
<
body
><
h1
>Hello,${name}!</
body
>

6.
</
html
>


通过VelocityTemplateFactory和VelocityTemplate就可以实现对Velocity的集成。不过,从Web开发人员看来,并不需要知道具体使用的模板,客户端仅需要提供模板路径和一个由Map

viewsourceprint?

01.
//清单16.定义TemplateRenderer

02.

03.
public
class
TemplateRenderer
extends
Renderer{

04.
private
Stringpath;

05.
private
Map<String,Object>model;

06.

07.
public
TemplateRenderer(Stringpath,Map<String,Object>model){

08.
this
.path=path;

09.
this
.model=model;

10.
}

11.

12.
@Override

13.
public
void
render(ServletContextcontext,HttpServletRequestrequest,

14.
HttpServletResponseresponse)
throws
Exception{

15.
TemplateFactory.getTemplateFactory()

16.
.loadTemplate(path)

17.
.render(request,response,model);

18.
}

19.
}


TemplateRenderer通过简单地调用render方法就实现了页面渲染。为了指定Jsp或Velocity,需要在web.xml中配置DispatcherServlet的初始参数。配置示例请参考清单17。

viewsourceprint?

01.
清单17.配置Velocity作为模板引擎

02.

03.
<
servlet
>

04.
<
servlet-name
>dispatcher</
servlet-name
>

05.
<
servlet-class
>org.expressme.webwind.DispatcherServlet</
servlet-class
>

06.
<
init-param
>

07.
<
param-name
>template</
param-name
>

08.
<
param-value
>Velocity</
param-value
>

09.
</
init-param
>

10.
</
servlet
>


如果没有该缺省参数,那就使用默认的Jsp。

类似的,通过扩展TemplateFactory和Template,就可以添加更多的模板支持,例如FreeMarker。

设计拦截器

拦截器和Servlet规范中的Filter非常类似,不过Filter的作用范围是整个HttpServletRequest的处理过程,而拦截器仅作用于Controller,不涉及到View的渲染,在大多数情况下,使用拦截器比Filter速度要快,尤其是绑定数据库事务时,拦截器能缩短数据库事务开启的时间。

拦截器接口Interceptor定义如清单18所示。

viewsourceprint?

1.
//清单18.定义Interceptor

2.

3.
public
interface
Interceptor{

4.
void
intercept(Executionexecution,InterceptorChainchain)
throws
Exception;

5.
}


和Filter类似,InterceptorChain代表拦截器链。InterceptorChain定义如清单19所示。

viewsourceprint?

1.
//清单19.定义InterceptorChain

2.

3.
public
interface
InterceptorChain{

4.
void
doInterceptor(Executionexecution)
throws
Exception;

5.
}


实现InterceptorChain要比实现FilterChain简单,因为Filter需要处理Request、Forward、Include和Error这4种请求转发的情况,而Interceptor仅拦截Request。当MVC框架处理一个请求时,先初始化一个拦截器链,然后,依次调用链上的每个拦截器。请参考清单20所示的代码。

viewsourceprint?

01.
//清单20.实现InterceptorChain接口

02.

03.
class
InterceptorChainImpl
implements
InterceptorChain{

04.
private
final
Interceptor[]interceptors;

05.
private
int
index=
0
;

06.
private
Objectresult=
null
;

07.

08.
InterceptorChainImpl(Interceptor[]interceptors){

09.
this
.interceptors=interceptors;

10.
}

11.

12.
ObjectgetResult(){

13.
return
result;

14.
}

15.

16.
public
void
doInterceptor(Executionexecution)
throws
Exception{

17.
if
(index==interceptors.length)

18.
result=execution.execute();

19.
else
{

20.
//mustupdateindexfirst,otherwisewillcausestackoverflow:

21.
index++;

22.
interceptors[index-
1
].intercept(execution,
this
);

23.
}

24.
}

25.
}


成员变量index表示当前链上的第N个拦截器,当最后一个拦截器被调用后,InterceptorChain才真正调用Execution对象的execute()方法,并保存其返回结果,整个请求处理过程结束,进入渲染阶段。清单21演示了如何调用拦截器链的代码。

viewsourceprint?

01.
//清单21.调用拦截器链

02.

03.
class
Dispatcher{

04.
...

05.
private
Interceptor[]interceptors;

06.
void
handleExecution(Executionexecution,HttpServletRequestrequest,

07.
HttpServletResponseresponse)
throws
ServletException,IOException{

08.
InterceptorChainImplchains=
new
InterceptorChainImpl(interceptors);

09.
chains.doInterceptor(execution);

10.
handleResult(request,response,chains.getResult());

11.
}

12.
}


当Controller方法被调用完毕后,handleResult()方法用于处理执行结果。

渲染

由于我们没有强制HTTP处理方法的返回类型,因此,handleResult()方法针对不同的返回值将做不同的处理。代码如清单22所示。

viewsourceprint?

01.
//清单22.处理返回值

02.

03.
class
Dispatcher{

04.
...

05.
void
handleResult(HttpServletRequestrequest,HttpServletResponseresponse,

06.
Objectresult)
throws
Exception{

07.
if
(result==
null
)

08.
return
;

09.
if
(result
instanceof
Renderer){

10.
Rendererr=(Renderer)result;

11.
r.render(
this
.servletContext,request,response);

12.
return
;

13.
}

14.
if
(result
instanceof
String){

15.
Strings=(String)result;

16.
if
(s.startsWith(
"redirect:"
)){

17.
response.sendRedirect(s.substring(
9
));

18.
return
;

19.
}

20.
new
TextRenderer(s).render(servletContext,request,response);

21.
return
;

22.
}

23.
throw
new
ServletException(
"Cannothandleresultwithtype'"

24.
+result.getClass().getName()+
"'."
);

25.
}

26.
}


如果返回null,则认为HTTP请求已处理完成,不做任何处理;如果返回Renderer,则调用Renderer对象的render()方法渲染视图;如果返回String,则根据前缀是否有“redirect:”判断是重定向还是作为HTML返回给浏览器。这样,客户端可以不必访问HttpServletResponse对象就可以非常方便地实现重定向。代码如清单23所示。

viewsourceprint?

01.
//清单23.重定向

02.

03.
@Mapping
(
"/register"
)

04.
Stringregister(){

05.
...

06.
if
(success)

07.
return
"redirect:/reg/success"
;

08.
return
"redirect:/reg/failed"
;

09.
}


扩展Renderer还可以处理更多的格式,例如,向浏览器返回JavaScript代码等。

扩展

以下是对MVC框架核心功能的扩展。

使用Filter转发

对于请求转发,除了使用DispatcherServlet外,还可以使用Filter来拦截所有请求,并直接在Filter内实现请求转发和处理。使用Filter的一个好处是如果URL没有被任何Controller的映射方法匹配到,则可以简单地调用FilterChain.doFilter()将HTTP请求传递给下一个Filter,这样,我们就不必自己处理静态文件,而由Web服务器提供的默认Servlet处理,效率更高。和DispatcherServlet类似,我们编写一个DispatcherFilter作为前置处理器,负责转发请求,代码见清单24。

viewsourceprint?

01.
//清单24.定义DispatcherFilter

02.

03.
public
class
DispatcherFilter
implements
Filter{

04.
...

05.
public
void
doFilter(ServletRequestreq,ServletResponseresp,FilterChainchain)

06.
throws
IOException,ServletException{

07.
HttpServletRequesthttpReq=(HttpServletRequest)req;

08.
HttpServletResponsehttpResp=(HttpServletResponse)resp;

09.
Stringmethod=httpReq.getMethod();

10.
if
(
"GET"
.equals(method)||
"POST"
.equals(method)){

11.
if
(!dispatcher.service(httpReq,httpResp))

12.
chain.doFilter(req,resp);

13.
return
;

14.
}

15.
httpResp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);

16.
}

17.
}


如果用DispatcherFilter代替DispatcherServlet,则我们需要过滤“/*”,在web.xml中添加声明如清单25所示。

viewsourceprint?

01.
清单25.声明DispatcherFilter

02.

03.
<
filter
>

04.
<
filter-name
>dispatcher</
servlet-name
>

05.
<
filter-class
>org.expressme.webwind.DispatcherFilter</
servlet-class
>

06.
</
filter
>

07.
<
filter-mapping
>

08.
<
filter-name
>dispatcher</
servlet-name
>

09.
<
url-pattern
>/*</
url-pattern
>

10.
</
filter-mapping
>


访问Request和Response对象

如何在@Mapping方法中访问Servlet对象?如HttpServletRequest,HttpServletResponse,HttpSession和ServletContext。ThreadLocal是一个最简单有效的解决方案。我们编写一个ActionContext,通过ThreadLocal来封装对Request等对象的访问。代码见清单26。

viewsourceprint?

01.
//清单26.定义ActionContext

02.

03.
public
final
class
ActionContext{

04.
private
static
final
ThreadLocal<ActionContext>actionContextThreadLocal

05.
=
new
ThreadLocal<ActionContext>();

06.

07.
private
ServletContextcontext;

08.
private
HttpServletRequestrequest;

09.
private
HttpServletResponseresponse;

10.

11.
public
ServletContextgetServletContext(){

12.
return
context;

13.
}

14.

15.
public
HttpServletRequestgetHttpServletRequest(){

16.
return
request;

17.
}

18.

19.
public
HttpServletResponsegetHttpServletResponse(){

20.
return
response;

21.
}

22.

23.
public
HttpSessiongetHttpSession(){

24.
return
request.getSession();

25.
}

26.

27.
public
static
ActionContextgetActionContext(){

28.
return
actionContextThreadLocal.get();

29.
}

30.

31.
static
void
setActionContext(ServletContextcontext,

32.
HttpServletRequestrequest,HttpServletResponseresponse){

33.
ActionContextctx=
new
ActionContext();

34.
ctx.context=context;

35.
ctx.request=request;

36.
ctx.response=response;

37.
actionContextThreadLocal.set(ctx);

38.
}

39.

40.
static
void
removeActionContext(){

41.
actionContextThreadLocal.remove();

42.
}

43.
}


在Dispatcher的handleExecution()方法中,初始化ActionContext,并在finally中移除所有已绑定变量,代码见清单27。

viewsourceprint?

01.
//清单27.初始化ActionContext

02.

03.
class
Dispatcher{

04.
...

05.
void
handleExecution(Executionexecution,HttpServletRequestrequest,

06.
HttpServletResponseresponse)
throws
ServletException,IOException{

07.
ActionContext.setActionContext(servletContext,request,response);

08.
try
{

09.
InterceptorChainImplchains=
new
InterceptorChainImpl(interceptors);

10.
chains.doInterceptor(execution);

11.
handleResult(request,response,chains.getResult());

12.
}

13.
catch
(Exceptione){

14.
handleException(request,response,e);

15.
}

16.
finally
{

17.
ActionContext.removeActionContext();

18.
}

19.
}

20.
}


这样,在@Mapping方法内部,可以随时获得需要的Request、Response、Session和ServletContext对象。

处理文件上传

ServletAPI本身并没有提供对文件上传的支持,要处理文件上传,我们需要使用CommonsFileUpload之类的第三方扩展包。考虑到CommonsFileUpload是使用最广泛的文件上传包,我们希望能集成CommonsFileUpload,但是,不要暴露CommonsFileUpload的任何API给MVC的客户端,客户端应该可以直接从一个普通的HttpServletRequest对象中获取上传文件。

要让MVC客户端直接使用HttpServletRequest,我们可以用自定义的MultipartHttpServletRequest替换原始的HttpServletRequest,这样,客户端代码可以通过instanceof判断是否是一个Multipart格式的Request,如果是,就强制转型为MultipartHttpServletRequest,然后,获取上传的文件流。

核心思想是从HttpServletRequestWrapper派生MultipartHttpServletRequest,这样,MultipartHttpServletRequest具有HttpServletRequest接口。MultipartHttpServletRequest的定义如清单28所示。

viewsourceprint?

01.
//清单28.定义MultipartHttpServletRequest

02.

03.
public
class
MultipartHttpServletRequest
extends
HttpServletRequestWrapper{

04.
final
HttpServletRequesttarget;

05.
final
Map<String,List<FileItemStream>>fileItems;

06.
final
Map<String,List<String>>formItems;

07.

08.
public
MultipartHttpServletRequest(HttpServletRequestrequest,
long
maxFileSize)

09.
throws
IOException{

10.
super
(request);

11.
this
.target=request;

12.
this
.fileItems=
new
HashMap<String,List<FileItemStream>>();

13.
this
.formItems=
new
HashMap<String,List<String>>();

14.
ServletFileUploadupload=
new
ServletFileUpload();

15.
upload.setFileSizeMax(maxFileSize);

16.
try
{

17.
...解析Multipart...

18.
}

19.
catch
(FileUploadExceptione){

20.
throw
new
IOException(e);

21.
}

22.
}

23.

24.
public
InputStreamgetFileInputStream(StringfieldName)
throws
IOException{

25.
List<FileItemStream>list=fileItems.get(fieldName);

26.
if
(list==
null
)

27.
throw
new
IOException(
"Nofileitemwithname'"
+fieldName+
"'."
);

28.
return
list.get(
0
).openStream();

29.
};

30.
}


对于正常的Field参数,保存在成员变量Map<String,List<String>>formItems中,通过覆写getParameter()、getParameters()等方法,就可以让客户端把MultipartHttpServletRequest也当作一个普通的Request来操作,代码见清单29。

viewsourceprint?

01.
//清单29.覆写getParameter

02.

03.
public
class
MultipartHttpServletRequest
extends
HttpServletRequestWrapper{

04.
...

05.
@Override

06.
public
StringgetParameter(Stringname){

07.
List<String>list=formItems.get(name);

08.
if
(list==
null
)

09.
return
null
;

10.
return
list.get(
0
);

11.
}

12.

13.
@Override

14.
@SuppressWarnings
(
"unchecked"
)

15.
public
MapgetParameterMap(){

16.
Map<String,String[]>map=
new
HashMap<String,String[]>();

17.
Set<String>keys=formItems.keySet();

18.
for
(Stringkey:keys){

19.
List<String>list=formItems.get(key);

20.
map.put(key,list.toArray(
new
String[list.size()]));

21.
}

22.
return
Collections.unmodifiableMap(map);

23.
}

24.

25.
@Override

26.
@SuppressWarnings
(
"unchecked"
)

27.
public
EnumerationgetParameterNames(){

28.
return
Collections.enumeration(formItems.keySet());

29.
}

30.

31.
@Override

32.
public
String[]getParameterValues(Stringname){

33.
List<String>list=formItems.get(name);

34.
if
(list==
null
)

35.
return
null
;

36.
return
list.toArray(
new
String[list.size()]);

37.
}

38.
}


为了简化配置,在Web应用程序启动的时候,自动检测当前ClassPath下是否有CommonsFileUpload,如果存在,文件上传功能就自动开启,如果不存在,文件上传功能就不可用,这样,客户端只需要简单地把CommonsFileUpload的jar包放入/WEB-INF/lib/,不需任何配置就可以直接使用。核心代码见清单30。

viewsourceprint?

01.
//清单30.检测CommonsFileUpload

02.

03.
class
Dispatcher{

04.
private
boolean
multipartSupport=
false
;

05.
...

06.
void
initAll(Configconfig)
throws
Exception{

07.
try
{

08.
Class.forName(
"org.apache.commons.fileupload.servlet.ServletFileUpload"
);

09.
this
.multipartSupport=
true
;

10.
}

11.
catch
(ClassNotFoundExceptione){

12.
log.info(
"CommonsFileUploadnotfound."
);

13.
}

14.
...

15.
}

16.

17.
void
handleExecution(Executionexecution,HttpServletRequestrequest,

18.
HttpServletResponseresponse)
throws
ServletException,IOException{

19.
if
(
this
.multipartSupport){

20.
if
(MultipartHttpServletRequest.isMultipartRequest(request)){

21.
request=
new
MultipartHttpServletRequest(request,maxFileSize);

22.
}

23.
}

24.
...

25.
}

26.
...

27.
}


小结

要从头设计并实现一个MVC框架其实并不困难,设计WebWind的目标是改善Web应用程序的URL结构,并通过自动提取和映射URL中的参数,简化控制器的编写。WebWind适合那些从头构造的新的互联网应用,以便天生支持REST风格的URL。但是,它不适合改造已有的企业应用程序,企业应用的页面不需要搜索引擎的索引,其用户对URL地址的友好程度通常也并不关心。

参考资料

参考Servlet2.4规范:http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html

参考Spring框架:http://www.springsource.org/

参考Guice框架:http://code.google.com/p/google-guice/

参考Velocity引擎:http://velocity.apache.org/

参考CommonsFileUpload:http://commons.apache.org/fileupload/

下载

下载WebWind:http://code.google.com/p/webwind/downloads/list

下载WebWindSVN源码:http://webwind.googlecode.com/svn/trunk/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: