好吧!尽管这个标题有点吓人,但我并不是来摆显自己有多么的能耐,只不过是最近比较闲,而且程序员们天生爱折磨自己,所以就顺带研究了一下SiteMesh的原理。如果你是第一次听说SiteMesh,或者从未使用过它,而你又对SiteMesh感到兴趣的话,请务必先闻一闻、用一用,感受一下SiteMesh的魅力,本文并不会教你如何使用它。
总的来说,SiteMesh就是用来让你脱离<jsp:include/>标签的苦海的,它会为你自动地添加页头、脚注或者导航栏。公司里总会有人问我:你是怎么看源码的?而我总是告诉他们:如果你在高中阶段不是填鸭式学习的话,你应该会知道怎么看源码。他们总是一脸疑惑的看着我。事实上,我看源码是基于“猜想-验证”这样的步骤去做的,那么,要实现SiteMesh装饰器那样的效果,我的猜想是:
1、在装饰页面上留下类似于<dec:body/>这样的标记。
2、当jsp解释器遇到这个标记时,就把用户真正请求的页面塞进去。
然而,要做到这样的要求,要解决的问题有两个:
1、如何截取用户真正的请求页面
2、如何塞进去
幸好,这两个都不算什么大难题,我们可以使用Filter来拦截返回(Response)客户端浏览器的内容,从而实现内容的截取。第二个我们可以使用自定义JSP标签的方法实现“一遇到,则填充”这样的效果。
事实上,SiteMesh作者的想法跟我猜想的思路是一致的,SiteMesh所使用的Filter在web.xml中写得很清楚:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<
web-app
>
<!-- Start of SiteMesh stuff -->
<
filter
>
<
filter-name
>sitemesh</
filter-name
>
<
filter-class
>com.opensymphony.module.sitemesh.filter.PageFilter</
filter-class
>
</
filter
>
<
filter-mapping
>
<
filter-name
>sitemesh</
filter-name
>
<
url-pattern
>*</
url-pattern
>
</
filter-mapping
>
<
taglib
>
<
taglib-uri
>sitemesh-page</
taglib-uri
>
<
taglib-location
>/WEB-INF/sitemesh-page.tld</
taglib-location
>
</
taglib
>
<
taglib
>
<
taglib-uri
>sitemesh-decorator</
taglib-uri
>
<
taglib-location
>/WEB-INF/sitemesh-decorator.tld</
taglib-location
>
</
taglib
>
</
web-app
>
|
没错,PageFilter就是SiteMesh用来拦截数据的类,那么再看看PageFilter类的doFilter方法:
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
32
33
34
35
36
37
38
39
40
|
public
void
doFilter(ServletRequest rq, ServletResponse rs, FilterChain chain)
throws
IOException, ServletException {
if
(rq.getAttribute(FILTER_APPLIED) !=
null
) {
// ensure that filter is only applied once per request
chain.doFilter(rq, rs);
}
else
{
HttpServletRequest request = (HttpServletRequest) rq;
HttpServletResponse response = (HttpServletResponse) rs;
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// force creation of the session now because Tomcat 4 had problems with
// creating sessions after the response had been committed
if
(Container.get() == Container.TOMCAT) {
request.getSession(
true
);
}
// parse data into Page object (or continue as normal if Page not parseable)
Page page = parsePage(request, response, chain);
if
(page !=
null
) {
page.setRequest(request);
Decorator decorator = factory.getDecoratorMapper().getDecorator(request, page);
if
(decorator !=
null
&& decorator.getPage() !=
null
) {
applyDecorator(page, decorator, request, response);
page =
null
;
return
;
}
// if we got here, an exception occured or the decorator was null,
// what we don't want is an exception printed to the user, so
// we write the original page
writeOriginal(response, page);
page =
null
;
}
}
}
|
老实说,虽然代码注释很烂,但是基本的逻辑都体现在了doFilter方法里了,如果看代码不能让你拨开云雾的话,我还在网上扒了一幅图片:
当用户请求home.jsp,并且服务器处理完毕正准备返回数据之时,它被SiteMesh Filter拦截了下来,并且把数据包装成一个Page对象,具体是Page page = parsePage(request, response, chain)的调用,然后,它会去查询decorators.xml文件,看看该页面是否需要装饰[if (decorator != null && decorator.getPage() != null)]?是,则应用装饰器[applyDecorator(page, decorator, request, response)],否则,就发送原来的没经过装饰的页面[writeOriginal(response, page);]。
然而,我们到底如何做才能把返回内容剥离出来呢?答案就是使用自定义的响应结果包装器,其实就是一个继承了HttpServletResponseWrapper类的java类,如下:
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
32
33
34
35
36
37
|
package
servlet.util;
import
java.io.CharArrayWriter;
/**
* 自定义一个响应结果包装器,将在这里提供一个基于内存的输出器来存储所有
* 返回给客户端的原始HTML代码。
* @author lee
*/
public
class
MyResponseWrapper
extends
HttpServletResponseWrapper {
private
PrintWriter cacheWriter;
private
CharArrayWriter bufferWriter;
//用于保存截获的jsp内容
public
MyResponseWrapper(HttpServletResponse response) {
super
(response);
bufferWriter =
new
CharArrayWriter();
// 这个是包装PrintWriter的,让所有结果通过这个PrintWriter写入到bufferedWriter中
cacheWriter =
new
PrintWriter(bufferWriter);
}
/**
*当一个继承了HttpServletResponseWrapper的包装器复写了getWriter()方法时
*tomcat会把响应的内容塞入自定义的PrintWriter(cacheWriter)中
*/
@Override
public
PrintWriter getWriter()
throws
IOException {
return
cacheWriter;
}
/**
* 获取原始的HTML页面内容。
* @return
*/
public
String getResult(){
return
bufferWriter.toString();
}
}
|
一个很简单的类,然后,只需要在doFilter方法中如下调用,就可以截取jsp的页面内容了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
void
doFilter(ServletRequest servletrequest,
ServletResponse servletresponse, FilterChain filterchain)
throws
IOException, ServletException {
// 使用我们自定义的响应包装器来包装原始的ServletResponse
MyResponseWrapper wrapper =
new
MyResponseWrapper((HttpServletResponse) servletresponse);
// 这句话非常重要,注意看到第二个参数是我们的包装器而不是原始的servletresponse
//这样容器才会把响应内容写入自定义的包装器中
filterchain.doFilter(servletrequest, wrapper);
// 截获的结果并进行处理
String result = wrapper.getResult();
......
}
|
而SiteMesh就是这样做的,只不过复杂点罢了,你可以去看看PageFilter类的parsePage方法。
好了,现在我们可以截取jsp页面了,剩下的就是考虑如何把这些内容塞进我们的标签<dec:body/>中,而这就属于自定义JSP标签的范畴了,网上一搜一大把,总的来说就是在web.xml中指定标签库,又在标签库中指定处理类:
web.xml
1
2
3
4
5
6
7
8
9
|
<
web-app
>
......
<!-- 自定义JSP标签 -->
<
taglib
>
<
taglib-location
>/mytld/custom.tld</
taglib-location
>
</
taglib
>
......
</
web-app
>
|
然后,你需要在项目的根目录下建立好mytld文件夹,在里面建立好custom.tld文件,使用任何一种编辑器打开并敲入下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<!
DOCTYPE
taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<
taglib
>
<
tlibversion
>1.0</
tlibversion
>
<
jspversion
>1.1</
jspversion
>
<
shortname
>Custom Tags</
shortname
>
<
tag
>
<
name
>body</
name
>
<
tagclass
>custom.tag.BodyTag</
tagclass
>
<
bodycontent
>JSP</
bodycontent
>
</
tag
>
</
taglib
>
|
在custom.tld中我们指定了遇到<dec:body/>标签就交给BodyTag处理,而BodyTag事实上就是一个继承了TagSupport的java类,并且,你需要重写doStartTag方法和doEndTag方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package
custom.tag;
import
javax.servlet.jsp.tagext.TagSupport;
public
class
BodyTag
extends
TagSupport{
public
int
doStartTag(){
try
{
String reqPage = (String) pageContext.getAttribute(
"reqPage"
, pageContext.REQUEST_SCOPE);
if
(reqPage ==
null
){
pageContext.getOut().print(
"标签开始了<font color=\"red\">Hello</font>"
);
}
else
{
pageContext.getOut().print(reqPage);
}
}
catch
(Exception e) {
e.printStackTrace();
}
return
EVAL_BODY_INCLUDE;
}
public
int
doEndTag(){
return
EVAL_PAGE;
}
}
|
嗯,到底把截获的jsp页面放到哪里,才能让BodyTag类取到并使用呢?我把它放到了request对象当中:
下面看看自定义的MyFilter类的doFilter方法:
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
32
33
34
|
package
servlet.demo;
import
java.io.IOException;
public
class
MyFilter
implements
Filter {
private
ServletContext servletContext =
null
;
public
void
destroy() {
}
public
void
doFilter(ServletRequest servletrequest,
ServletResponse servletresponse, FilterChain filterchain)
throws
IOException, ServletException {
// 使用我们自定义的响应包装器来包装原始的ServletResponse
MyResponseWrapper wrapper =
new
MyResponseWrapper((HttpServletResponse) servletresponse);
// 这句话非常重要,注意看到第二个参数是我们的包装器而不是原始的response
//这样容器才会把响应内容写入自定义的包装器中
filterchain.doFilter(servletrequest, wrapper);
// 处理截获的结果并进行处理
String result = wrapper.getResult();
System.out.println(result);
//把jsp页面放到request中
servletrequest.setAttribute(
"reqPage"
, result);
//把smart.jsp包含进请求中,这一步会触发jsp解释器去解释smart.jsp页面
//当遇上<dec:body/>时,塞入result
servletrequest.getRequestDispatcher(
"decorators/smart.jsp"
).include(servletrequest, servletresponse);
}
public
void
init(FilterConfig filterconfig)
throws
ServletException {
servletContext = filterconfig.getServletContext();
}
}
|
这里的servletrequest.getRequestDispatcher("decorators/smart.jsp").include(servletrequest,servletresponse)很重要,因为程序执行到这里会触发jsp解释器去解释jsp页面,而smart.jsp就是我们的装饰页面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<%@ taglib uri="http://customtag.com" prefix="dec"%>
<
html
>
<
head
>
<
title
>欢迎访问 </
title
>
</
head
>
<
body
>
<
div
style
=
"margin-bottom:10px;padding:6px;border:1px solid gray;"
>我这里是头部,页面装饰器头部定义</
div
>
<
div
style
=
"margin-bottom:10px;padding:6px;border:1px solid gray;"
>
功能菜单:<
a
href
=
"#"
/>用户管理</
a
>
</
div
>
<!-- 这里是操作信息提示区域 -->
<
div
></
div
>
<!-- 这里是功能的内容区域 -->
<
div
>
<
dec:body
/>
</
div
>
<
div
style
=
"margin-top:10px;padding:6px;border:1px solid gray;"
>我这里是尾部,页面装饰器尾部定义</
div
>
</
body
>
</
html
>
|
当jsp解释器解释smart.jsp的过程中遇到了<dec:body/>,就会跑到BodyTag中执行标签解释工作,此时,我们就可以把早已准备好的用户真正请求的页面内容塞进去:
1
2
3
4
|
//从request中取出数据
String reqPage = (String) pageContext.getAttribute(
"reqPage"
, pageContext.REQUEST_SCOPE);
//填充
pageContext.getOut().print(reqPage);
|
SiteMesh正是使用这种方式实现自动装饰功能,这里面并没有什么高深的技术(可能是我还没有发现)。
现在,写一个需要应用装饰的页面index.jsp:
1
2
|
<!-- index.jsp -->
<
h1
>Hello World!!</
h1
>
|
在index.jsp中你甚至不用写<body/>标签
最终结果: