第 7 章 Request Contexts功能指南

7.1. <basic> - 提供基础特性
7.1.1. 拦截器接口
7.1.2. 默认拦截器
7.2. <set-locale> -设置locale区域和charset字符集编码
7.2.1. Locale基础
7.2.2. Charset编码基础
7.2.3. Locale和charset的关系
7.2.4. 设置locale和charset
7.2.5. 使用方法
7.3. <parser> - 解析参数
7.3.1. 基本使用方法
7.3.2. 上传文件
7.3.3. 高级选项
7.4. <buffered> - 缓存response中的内容
7.4.1. 实现原理
7.4.2. 使用方法
7.5. <lazy-commit> - 延迟提交response
7.5.1. 什么是提交
7.5.2. 实现原理
7.5.3. 使用方法
7.6. <rewrite> -重写请求的URL和参数
7.6.1. 概述
7.6.2. 取得路径
7.6.3. 匹配rules
7.6.4. 匹配conditions
7.6.5. 替换路径
7.6.6. 替换参数
7.6.7. 后续操作
7.6.8. 重定向
7.6.9. 自定义处理器
7.7. 本章总结

第 6 章 Filter、Request Contexts和Pipeline中,我们已经介绍了Request Contexts服务的作用和原理。本章我们将介绍除了session机制以外,每一个可用的Request Context的功能和用法。由于Session机制比较复杂,所以我们另辟单独的一章(第 8 章 Request Context之Session指南)来解释它。

本章涉及的内容包括:

名称接口功能
<basic>BasicRequestContext提供基础安全特性,例如:过滤response headers、cookies,限制cookie的大小等。
<set-locale>SetLocaleRequestContext设置locale区域和charset字符集编码。
<parser>ParserRequestContext解析参数,支持multipart/form-data(即上传文件请求)。
<buffered>BufferedRequestContext缓存response中的内容。
<lazy-commit>LazyCommitRequestContext延迟提交response。
<rewrite>RewriteRequestContext重写请求的URL和参数。

7.1. <basic> - 提供基础特性

7.1.1. 拦截器接口

BasicRequestContext提供了一组interceptors拦截器接口,通过它们,你可以拦截并干预一些事件。

BasicRequestContext所提供的拦截器

图 7.1. BasicRequestContext所提供的拦截器

你可以在<basic>中指定上图所示的任何一个Interceptor接口,以便干预特定的事件:

表 7.1. BasicRequestContext所提供的拦截器

拦载器接口说明
RequestContextLifecycleInterceptor 拦截“预处理(prepare)”和“提交(commit)”事件。
ResponseHeaderInterceptor 拦截所有对response header的修改。
HeaderNameInterceptor 拦截所有对header的修改、添加操作。可修改header name,或拒绝对header的修改。
HeaderValueInterceptor 拦截所有对header的修改、添加操作。可修改header value,或拒绝对header的修改。
CookieInterceptor 拦截所有对cookie的添加操作。可修改或拒绝cookie对象。需要注意的是,有两种方法可以添加cookie:通过cookie对象,或者直接写response header。对于后者,需要使用CookieHeaderValueInterceptor才能拦截得到。
CookieHeaderValueInterceptor 拦截所有通过添加header来创建cookie的操作。可修改或拒绝该cookie。
RedirectLocaitonInterceptor 拦截所有外部重定向的操作。可修改或拒绝重定向URL。
StatusMessageInterceptor 拦截所有设置status message的操作。可以修改或拒绝该message。

通过下面的配置,就可以指定任意多个interceptor的实现。

例 7.1. 配置interceptors(/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <basic>
        <request-contexts:interceptors
            xmlns="http://www.alibaba.com/schema/services/request-contexts/basic/interceptors">
                <interceptor class="...Interceptor1" />
                <interceptor class="...Interceptor2" />
        </request-contexts:interceptors>
    </basic>
    ...
</services:request-contexts>

7.1.2. 默认拦截器

即使你不加说明,BasicRequestContext也总是会启用一个默认的interceptor实现:ResponseHeaderSecurityFilter。这个类实现了下列功能:

  • 避免header name和value中出现CRLF字符 ── 在header中嵌入CRLF(回车换行)字符是一种常见的攻击手段。攻击者嵌入CRLF以后,使服务器对HTTP请求发生错误判断,从而执行攻击者的恶意代码。事实上,现在的servlet引擎如tomcat已经可以防御这种攻击。但作为框架,并不能依赖于特定的servlet引擎,所以加上这个额外的安全检查,确保万无一失。

  • 将status message用HTML entity编码重写 ── 通常status message会被显示在HTML页面中。攻击者可以利用这一点在页面中嵌入恶意代码。将status message以HTML entity编码重写以后,就可以避免这个问题。

  • 限制cookie的总大小 ── 过大的cookie可能使WEB服务器拒绝响应请求。攻击者同样可以利用这一点使用户无法正常访问网站。限制cookie的总大小可以部分地解决这种危机。

如果需要,你可以对ResponseHeaderSecurityFilter指定一些参数。

例 7.2. 配置ResponseHeaderSecurityFilter/WEB-INF/webx.xml

<request-contexts:interceptors
            xmlns="http://www.alibaba.com/schema/services/request-contexts/basic/interceptors">
    <interceptor class="...Interceptor1" />
    <interceptor class="...Interceptor2" />
    <response-header-security-filter maxSetCookieSize="5K" />
</request-contexts:interceptors>

7.2. <set-locale> -设置locale区域和charset字符集编码

区域和编码问题(尤其是后者)是每个WEB应用都必须处理好的基本问题。它虽然本身并不复杂,但是在现实开发中,由于涉及面很广,一旦发生问题(例如乱码)经常让人手足无措。<set-locale>提供了一个机制,确保Web应用能够设置正确的区域和编码。

7.2.1. Locale基础

Locale是国际化的基础。

一个locale的格式是:language_country_variant,例如:zh_CNzh_TWen_USes_ES_Traditional_WIN等。

Java和框架根据不同的locale,可以取得不同的文本、对象。下面的Java代码根据不同的locale,取得不同语言版本的文字:

例 7.3. 利用ResourceBundle和locale取得国际化字符

Locale.setDefault(Locale.US);

String s1 = getResourceBundle(Locale.CHINA).getString("happy"); // 快乐
String s2 = getResourceBundle(Locale.TAIWAN).getString("happy"); // 快樂
String s3 = getResourceBundle(Locale.US).getString("happy"); // happy
...
ResourceBundle getResourceBundle(Locale locale) {
    return ResourceBundle.getBundle("ApplicationResources", locale);
}

其中所用到的ResourceBundle文件定义如下:

ApplicationResources.properties
happy = happy
ApplicationResources_zh_CN.properties
happy = \u5FEB\u4E50
ApplicationResources_zh_TW.properties
happy = \u5FEB\u6A02

7.2.2. Charset编码基础

Charset全称Character Encoding或字符集编码。Charset是将字符(characters)转换成字节(bytes)或者将字节转换成字符的算法。Java内部采用unicode来表示一个字符。将unicode字符转换成字节的过程,称为“编码”;将字节恢复成unicode字符的过程,称为“解码”。

浏览器发送给WEB应用的request参数,是以字节流的方式来表示的。Request参数必须经过解码才能被Java程序所解读。用来解码request参数的charset被称为“输入字符集编码(Input Charset)”;

WEB应用返回给浏览器的response响应内容必须编码成字节流,才能被浏览器或客户端解读。用来编码response内容的charset被称为“输出字符集编码(Output Charset)”。

一般情况下,input charset和output charset是相同的。因为浏览器发送表单数据时,总是采用当前页面的charset来编码的。例如,有一个表单页面,它的“contentType=text/html; charset=GBK”,那么用户填完全表单并提交时,浏览器会以GBK来编码用户所输入的表单数据。如果input charset和output charset不相同,服务器就不能正确解码浏览器根据output charset所发回给WEB应用的表单数据。

然而有一些例外情况下面,输入和输出的charset可能会不同:

  • 通过Java Script发送的表单,总是用UTF-8编码的。这意味着你必须用UTF-8作为input charset方能正确解码参数。这样,除非output charset也是UTF-8,否则两者就是不同的。

  • 应用间互相用HTTP访问时,可能采用不同的编码。例如,应用A以UTF-8访问应用B,而应用B是以GBK作为input/output charset的。此时会产生参数解码的错误。

  • 直接在浏览器地址栏里输入包含参数的URL,根据不同的浏览器和操作系统的设置,会有不同的结果:

    • 例如,中文Windows中,无论ie还是firefox,经试验,默认都以GBK来编码参数。IE对直接输入的参数,连URL encoding也没做。

    • 而在mac系统中,无论safari还是firefox,经试验,默认都是以UTF-8来编码参数。

框架必须要能够应付上面各种不确定的charset编码。

7.2.3. Locale和charset的关系

Locale和charset是相对独立的两个参数,但是又有一定的关系。

Locale决定了要显示的文字的语言,而charset则将这种语言的文字编码成bytes或从bytes解码成文字。因此,charset必须能够涵盖locale所代表的语言文字,如果不能,则可能出现乱码。下表列举了一些locale和charset的组合:

表 7.2. Locale和Charset的关系

Locale英文字符集中文字符集全字符集
ISO-8859-1 GB2312 Big5 GBK GB18030 UTF-8
en_US(美国英文)
zh_CN(简体中文)  
zh_TWzh_HK(台湾中文、香港中文)  

在所有charset中,有几个“全能”编码:

UTF-8

涵盖了unicode中的所有字符。然而用UTF-8来编码中文为主的页面时,每个中文会占用3个字节。建议以非中文为主的页面采用UTF-8编码。

GB18030

中文国际标准,和UTF-8一样,涵盖了unicode中的所有字符。用GB18030来编码中文为主的页面时有一定优势,因为绝大多数常用中文仅占用2个字节,比UTF-8短1/3。然而GB18030在非中文的操作系统中,有可能不能识别,其通用性不如UTF-8好。因此仅建议以中文为主的页面采用GB18030编码。

GBK

严格说,GBK不是全能编码(例如对很多西欧字符就支持不好),也不是国际标准。但它支持的字符数量接近于GB18030

7.2.4. 设置locale和charset

在Servlet API中,以下API是和locale和charset有关的。

表 7.3. 和locale、charset相关的servlet API

HttpServletRequest
.getCharacterEncoding()读取输入编码 
.setCharacterEncoding(charset)设置输入编码
  • 必须在第一次调用request.getParameter()request.getParameterMap()前设置,否则无效。

  • 如果不设置,则默认以ISO-8859-1来解码参数。

  • 一般只影响POST请求参数的解码,但这里有一些复杂性,参见第 7.3 节 “<parser> - 解析参数”

.getLocale()取得Accept-Language中浏览器首选的locale 
.getLocales()取得所有Accept-Language中所指定的locales 
HttpServletResponse
.getCharacterEncoding()取得输出编码 
.setCharacterEncoding(charset)设置输出编码
  • Since Servlet 2.4

.getContentType()取得content type
  • Since Servlet 2.4

.setContentType(contentType)设置content type
  • Content type中可能包含charset定义,例如:text/html; charset=GBK

.getLocale()取得输出locale 
.setLocale(locale)设置输出locale
  • 必须在response被commit之前调用,否则无效。

  • 它同时也会设置charset,除非content type已经被设置过,并用包含了charset的定义。

设置locale和charset是一件看起来容易,做起来不容易的事:

  • 输入编码必须在第一个读取request参数的调用之前设置好,否则就无效。只有把<set-locale>作为Request Contexts服务的一环,才有可能确保读取request参数之前,设置好输入编码。

  • 在Servlet 2.3之前,设置输出参数的唯一方法,是通过设置带有charset定义的content type。这一点在Servlet 2.4以后得到改进,添加了独立的设置输出编码的方法。<set-locale>弥补了Servlet 2.3和Servlet 2.4之间的差异,使WEB应用在所有的环境下,都可以独立设置content type和charset。

7.2.5. 使用方法

7.2.5.1. 使用默认值

例 7.4. 设置默认的locale和charset

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <set-locale defaultLocale="zh_CN" defaultCharset="GB18030" />
    ...
</services:request-contexts>

上面的配置,将WEB应用的输入charset、输出charset均设置成GB18030,将输出locale设置成zh_CN

7.2.5.2. 临时覆盖默认的charset

前面讲到在一些情况下面,服务器所收到的参数(表单数据)不是用应用默认的charset来编码的。例如Java Script总是以UTF-8来提交表单;系统间通过HTTP协议通信;或者用户直接在浏览器地址栏中输入参数。

如何应付这些不确定的charset呢?<set-locale>提供的方法是,在URL中指定输入编码,并覆盖默认值。

假设当前应用的默认值是defaultLocale=zh_CNdefaultCharset=GB18030,那么下面的请求将使用默认的GB18030来解码参数,并用默认的GB18030来输出页面:

http://localhost:8081/myapp/myform

假如你希望改用UTF-8来解码参数,那么可以使用下面的URL来覆盖默认值:

例 7.5. 在URL中覆盖默认的input charset

http://localhost:8081/myapp/myform?_input_charset=UTF-8

这样,Webx将采用UTF-8来解码参数,但仍然使用默认的GB18030来输出页面

需要注意的是,对于POST请求,你必须把_input_charset这个特殊的参数写在URL中,而不能写成普通的表单字段,例如:

例 7.6. 在POST表单中覆盖默认的input charset

<form action="http://localhost:8081/myapp/myform?_input_charset=UTF-8" method="POST"> 
    <input type="hidden" name="param1" value="value1"/>
    <input type="hidden" name="param2" value="value2"/>
</form>

必须把_input_charset这个特殊的参数写在URL中,即便是POST类型的表单。

在写AJAX Java Script代码时,也要注意:

例 7.7. 在AJAX代码中覆盖默认的input charset

var xhreq = new XMLHttpRequest();
xhreq.open("post", "/myapp/myform?_input_charset=UTF-8", true); 
...
xhreq.send("a=1&b=2");

必须把_input_charset这个特殊的参数写在URL中。

此外,<set-locale>也提供了临时覆盖输出编码的方法:

例 7.8. 在URL中覆盖默认的output charset

http://localhost:8081/myapp/myform?_output_charset=UTF-8

临时覆盖的输入、输出编码只会影响当前请求,它不会被记住。当一个不带有覆盖参数的请求进来时,将仍然按照默认值来设置输入、输出编码。

7.2.5.3. 持久覆盖默认的locale和charset

还有一种需求,就是多语言网页的支持。用户可以选择自己的语言:简体中文、繁体中文等。一旦用户作出选择,那么后续的网页将全部以用户所选择的语言和编码来显示。<set-locale>直接支持这个功能。只要你按下面的URL访问页面,用户的语言和编码即被切换成简体中文和UTF-8编码。

例 7.9. 持久覆盖默认的locale和charset

http://localhost:8081/myapp?_lang=zh_CN:UTF-8

参数值_lang=zh_CN:UTF-8将被保存在session中,后续的请求不需要再次指定_lang参数。用户所作出的选择将一直持续在整个session中,直到session被作废。

需要说明的是,假如我们采用了<session> request context来取代原来的session机制,那么该参数实际的保存位置将取决于session框架的设置 ── 例如:你可以把参数值保存在某个cookie中。然而,<set-locale>并不需要关心于session的实现细节或是用来保存参数的cookie的细节。

7.2.5.4. <set-locale>的影响力

<set-locale>所设置的输出locale输出charset值将会被保存在当前线程中,从而对整个线程产生影响。

表 7.4. 被<set-locale>影响的API

API说明
LocaleUtil.getContext().getLocale() 可以通过这两个方法取得当前线程的输出locale和charset。Webx框架中凡是要用到默认locale和charset的地方,都会从这里去取得值。
LocaleUtil.getContext().getCharset()
StringEscapeUtil.escapeURL() Webx调用这两个方法进行URL编码、解码时,不需要指定charset(不同于JDK的URLEncoder/URLDecoder)。这两个函数将从LocaleUtil.getContext().getCharset()中取得当前线程的输出charset。
StringEscapeUtil.unescapeURL()
TemplateService TemplateService如果指定了searchLocalizedTemplates=true参数,那么它会利用当前线程的locale来搜索本地化的模板,例如: screen/myTemplate_zh_CN.vm

7.2.5.5. <set-locale>的配置参数

例 7.10. <set-locale>的配置参数

<set-locale defaultLocale="..."
            defaultCharset="..."
            inputCharsetParam="_input_charset"
            outputCharsetParam="_output_charset"
            paramKey="_lang"
            sessionKey="_lang" />

表 7.5. <set-locale>配置参数说明

参数名说明
defaultLocale 默认locale。
defaultCharset 默认charset。
inputCharsetParam 用来临时改变输入charset的参数名,支持多个名称,以“|”分隔,例如“_input_charset|ie”。 默认值为“_input_charset”。
outputCharsetParam 用来临时改变输出charset的参数名,支持多个名称,以“|”分隔,例如“_output_charset|oe”。 默认为“_output_charset”。
paramKey 用来持久改变输出locale和charset的参数名,默认为“_lang”。
sessionKey 用来在session中保存用户所选择的locale和charset的key,默认为“_lang”。

7.3. <parser> - 解析参数

7.3.1. 基本使用方法

7.3.1.1. 基本配置

例 7.11. <parser>基本配置

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <parser />
    ...
</services:request-contexts>

<services:upload sizeMax="5M" fileSizeMax="2M" />

绝大多数情况,你只需要上面的配置就足够了 ── <parser>会自动解析所有类型的请求,包括:

  • GET请求

  • 普通的POST请求(Content Type:application/x-www-form-urlencoded

  • 可上传文件的POST请求(Content Type:multipart/form-data

7.3.1.2. 通过HttpServletRequest接口访问参数

<parser>对于大部分应用是透明的。也就是说,你不需要知道<parser>的存在,就可以访问所有的参数,包括访问multipart/form-data请求的参数。

例 7.12. 通过HttpServletRequest接口访问参数

@Autowired
HttpServletRequest request;

...
String s = request.getParameter("myparam");

7.3.1.3. 通过ParserRequestContext接口访问参数

你也可以选择使用ParserRequestContext接口。

例 7.13. 通过ParserRequestContext接口访问参数

@Autowired
ParserRequestContext parser;

...
String s = parser.getParameters().getString("myparam");

HttpServletRequest接口相比,ParserRequestContext提供了如下便利:

直接取得指定类型的参数,例如:直接取得intboolean值等。

例 7.14. 直接取得指定类型的参数

// myparam=true, myparam=false
parser.getParameters().getBoolean("myparam");

// myparam=123
parser.getParameters().getInt("myparam");
如果参数值未提供,或者值为空,则返回指定默认值。

例 7.15. 取得参数的默认值

parser.getParameters().getBoolean("myparam", false);
parser.getParameters().getString("myparam", "no_value");
parser.getParameters().getInt("myparam", -1);
取得上传文件的FileItem对象(这是Apache Jakarta 项目commons-fileupload所定义的接口)。

例 7.16. 取得FileItem上传文件

FileItem fileItem = parser.getParameters().getFileItem("myfile");
FileItem[] fileItems = parser.getParameters().getFileItems("myfile");
ParserRequestContext还提供了比较方便的访问cookie值的方法。

例 7.17. 访问cookie值

parser.getCookies().getString("mycookie");

7.3.2. 上传文件

用于上传文件的请求是一种叫作multipart/form-data的特殊请求,它的格式类似于富文本电子邮件的样子。下面HTML创建了一个支持上传文件的表单:

例 7.18. 创建multipart/form-data表单

<form action="..." method="post" enctype="multipart/form-data">
    <input type="file" name="myfile" value="" />
    ...
</form>

提示:不是只有需要上传文件时,才可以用multipart/form-data表单。假如你的表单中包含富文本字段(即字段的内容是以 HTML或类似的技术描述的),特别是当字段的内容比较长的时候,用multipart/form-data比用普通的表单更高效,生成的HTTP请求也更短

只要upload服务存在,那么<parser>就可以解析multipart/form-data(即上传文件)的请求。Upload服务扩展于Apache Jakarta的一个项目:commons-fileupload。

7.3.2.1. 配置Upload服务

例 7.19. Upload服务的配置参数

<services:upload sizeMax="5M"
                 fileSizeMax="2M"
                 repository="/tmp"
                 sizeThreshold="10K"
                 keepFormFieldInMemory="true" />

各参数的说明如下:

表 7.6. Upload服务配置参数说明

参数名称说明
sizeMax HTTP请求的最大尺寸(字节,支持K/M/G),超过此尺寸的请求将被抛弃。值-1表示没有限制。
fileSizeMax 单个文件允许的最大尺寸(字节,支持K/M/G),超过此尺寸的文件将被抛弃。值-1表示没有限制。
repository 暂存上传文件的目录。 注意,这个目录是用Spring ResourceLoader装载的,而不是一个物理路径。关于ResourceLoader,详见ResourceLoading服务的文档。
sizeThreshold 将文件放在内存中的阈值(字节,支持K/M/G),小于此值的文件被保存在内存中。
keepFormFieldInMemory 是否将普通的form field保持在内存里? 默认为false,但当sizeThreshold0时,默认为true
[注意]注意

当上传文件的请求的总尺寸超过sizeMax的值时,整个请求将被抛弃 —— 这意味着你不可能读到请求中的其它任何参数。而当某个上传文件的尺寸超出fileSizeMax的限制,但请求的总尺寸仍然在sizeMax的范围内时,只有超出该尺寸的单个上传文件被抛弃,而你还是可以读到其余的参数。

假如有多个upload服务(当然这种情况极少),你也可以明确指定<parser>使用哪个upload服务:

例 7.20. 明确指定upload服务

<parser uploadServiceRef="myUpload" />

7.3.2.2. 手工解析上传请求

在默认情况下,当<parser>收到一个上传文件的请求时,会立即解析并取得所有的参数和文件。然而你可以延迟这个过程,在需要的时候,再手工解析上传请求。

例 7.21. 手工解析upload请求

首先,你需要关闭自动上传

<parser autoUpload="false">

可选参数autoUpload默认值为true,当你把它改成false时,就可以实现延迟手工解析请求。在你需要解析请求时,只需要调用下面的语句即可:

parser.getParameters().parseUpload();

手工调用parseUpload可以指定和默认不同的参数:

UploadParameters params = new UploadParameters();
        
params.applyDefaultValues();
params.setSizeMax(new HumanReadableSize("10M"));
params.setFileSizeMax(new HumanReadableSize("1M"));
params.setRepository(new File("mydir"));
        
parser.getParameters().parseUpload(params);

7.3.3. 高级选项

7.3.3.1. 参数名称大小写转换

在默认情况下,假设有一个参数名为:myProductId,那么你可以使用下列任意一种方法来访问到它:

例 7.22. 取得参数myProductId的值的方法

request.getParameter("MyProductId");
request.getParameter("myProductId");
request.getParameter("my_product_id");
request.getParameter("MY_PRODUCT_ID");
request.getParameter("MY_productID");

假如你不希望具备这种灵活性,则需要修改配置以关闭大小写转换功能:

例 7.23. 关闭大小写转换功能

<parser caseFolding="none">

7.3.3.2. 参数值去空白

在默认情况下,假设有一个参数:id=" 123 "(两端有空白字符),那么<parser>会把它转化成"123"(两端没有空白字符)。 假如你不希望<parser>做这件事,则需要修改配置:

例 7.24. 关闭参数值去空白功能

<parser trimming="false">

这样,所有的参数值将会保持原状,不会被去除空白。

7.3.3.3. 参数值entity解码

浏览器在提交表单时,如果发现被提交的字符不能以当前的charset来编码,浏览器就会把该字符转换成&#unicode;这样的形式。例如,假设一个表单页面的content type为:text/html; charset=ISO-8859-1。在这个页面的输入框中输入汉字“你好”,然后提交。你会发现,提交的汉字变成了这个样子:param="&#20320;&#22909;"

在默认情况下,<parser>会对上述参数进行entity解码,使之恢复成“你好”。但是,其它的entity如“&lt;”、“&amp;”等并不会被转换。 如果你不希望<parser>还原上述内容,则需要修改配置:

例 7.25. 关闭参数值entity解码功能

<parser unescapeParameters="false">

7.3.3.4. 取得任意类型的参数值

前面提到,ParserRequestContext支持直接取得booleanint等类型的参数值。事实上,它还支持取得任意类型的参数值 —— 只要Spring中有相应的PropertyEditor支持即可。

假设MyEnum是一个enum类型,这是Spring原生支持的一种类型。你可以用下面的代码来取得它:

例 7.26. 将参数值转换成enum类型

MyEnum myEnum = params.getObjectOfType("myparam", MyEnum.class);

但是,下面的语句就不是那么顺利了 —— 因为Spring不知道怎么把一个参数值,例如:“1975-12-15”,转换成java.util.Date类型。

例 7.27. 将参数值转换成java.util.Date类型

Date birthday = params.getObjectOfType("birthday", Date.class);

好在<parser>提供了一种扩展机制,可以添加新的类型转换机制。对于Date类型,你只需要添加下面的配置,就可以被支持了。

<parser>
    <property-editor-registrar
        class="com.alibaba.citrus.service.configuration.support.CustomDateRegistrar"
        p:format="yyyy-MM-dd" p:locale="zh_CN" p:timeZone="GMT+8" /> 
</parser>

PropertyEditorRegistrar是Spring提供的一种类型注册机制,其细节详见Spring的文档。

另一个问题是,如果类型转换失败怎么办?<parser>支持两种方法。默认情况下,类型转换失败会“保持安静”(不抛异常),然后返回默认值。但你也可以选择让类型转换失败的异常被抛出来,以便应用程序处理。

例 7.28. 设置“非安静”模式:当类型转换失败时,抛出异常

<parser converterQuiet="false">

程序里这样写:

MyEnum myEnum = null;

try {
    myEnum = params.getObjectOfType("myparam", MyEnum.class);
} catch (TypeMismatchException e) {
    ...
}

7.3.3.5. 解析GET请求的参数

GET请求是最简单的请求方式。它的参数以URL编码的方式包含在URL中。当你在浏览器地址栏中敲入“http://localhost:8081/user/login.htm?name=%E5%90%8D%E5%AD%97&password=password”这样一个址址的时候,浏览器就会向localhost:8081服务器出如下HTTP请求:

GET /user/login.htm?name=%E5%90%8D%E5%AD%97&password=password HTTP/1.1
Host: localhost:8081

GET请求中的参数是以application/x-www-form-urlencoded方式和特定的charset编码的。假如用来编码URL参数的charset与应用的默认charset不同,那么你必须通过特殊的参数来指定charset(参见第 7.2 节 “<set-locale> -设置locale区域和charset字符集编码”):

GET /user/login.htm?_input_charset=UTF-8&name=%E5%90%8D%E5%AD%97&password=password HTTP/1.1

可是,上面的请求在不同的Servlet引擎中,会产生不确定的结果。这是怎么回事呢?

原来,尽管<set-locale>会调用request.setCharacterEncoding(charset)这个方法来设置input charset编码,然而根据Servlet API的规范,这个设定只能对request content生效,而不对URL生效。换句话说,request.setCharacterEncoding(charset)方法只能用来解析POST请求的参数,而不是GET请求的参数。

那么,应该怎样处理GET请求的参数呢?根据URL规范,URL中非US-ASCII的字符必须进行基于UTF-8的URL编码。然而实际上,从浏览器到服务器,没有人完全遵守这些规范,于是便造成了一些混乱。目前应用服务器端,我们所遇到的,有下面几种不同的解码方案:

表 7.7. 服务器对参数进行解码的逻辑

服务器解码的逻辑
Tomcat 4
  • 根据request.setCharacterEncoding(charset)所设置的值来解码GET参数;

  • 如果未特别指定charset,则默认采用ISO-8859-1来解码参数。

Tomcat 5及更新版 以及搭载Tomcat 5以上版本的JBoss
  • 如果Tomcat配置文件conf/server.xml中设置了: <Connector useBodyEncodingForURI="true">那么根据request.setCharacterEncoding(charset)所设置的值来解码GET参数。

  • 如未设置useBodyEncodingForURI,或其值为false,则根据conf/server.xml中的配置<Connector URIEncoding="xxx">所指定的编码,来解码GET请求的参数。

  • 如未配置URIEncoding,默认采用ISO-8859-1

Jetty Server
  • Jetty总是以UTF-8来解码GET请求的参数。

综上所述,所有的应用服务器对于POST请求的参数的处理方法是没有差别的,然而对于GET请求的参数处理方法各有不同。

如果不加任何特别的设置,Tomcat最新版是以ISO-8859-1来解码GET请求的参数,而Jetty却是以UTF-8来解码的。因此,无论你以哪一种charset来编码GET请求的参数,都不可能在所有服务器上取得相同的结果 ── 除非修改服务器的配置,但这是一件既麻烦又容易出错的事情。为了使应用程序对服务器的配置依赖较少,且可以灵活地处理GET请求的解码,<parser>对GET请求进行了手工解码,从而解决了应用服务器解码的不确定性。

<parser>完全解决了上面的问题。依据默认值,<parser>会以<set-locale>中设定的input charset为准,来解码所有类型的请求,包括GET和POST请求,以及multipart/form-data(上传文件)类型的请求。

然而<parser>仍保留了一些可选方案,以备不时之需。

保留Servlet引擎的解码机制

例 7.29. 使用Servlet引擎原来的解码机制

<parser useServletEngineParser="true" />

这个选项在用HttpUnit进行单元测试时非常有用。因为HttpUnit单元测试工具并没有完全遵循Servlet API的规范 ── 目前版本的HttpUnit不能正确取得query string,从而导致<parser>解析GET参数错误。

使用固定的charset来解码GET请求

例 7.30. 使用固定的charset来解码GET请求

<parser URIEncoding="UTF-8" useBodyEncodingForURI="false" />

上面的配置强制所有的GET请求均使用UTF-8作为固定的charset编码。这段逻辑和tomcat的完全相同,但你却不需要去修改tomcat的conf/server.xml就可以实现上面的逻辑。 事实上,使用固定的charset来解码GET请求的参数是符合Servlet API规范以及URL的规范的。而根据情况设置charset是一种对现实的妥协。然而你有选择的自由 ── 无论你选择何种风格,<parser>都支持你

7.3.3.6. 过滤参数

出于安全的考虑,<parser>还支持对输入参数进行过滤。请看示例:

例 7.31. 配置过滤参数

<parser>
    <filters>
        <parser-filters:uploaded-file-whitelist extensions="jpg, gif, png" />
    </filters>
</parser>

上面的配置将会禁止文件名后缀不在列表中的文件被上传到服务器上。如果做得更好一点,你甚至可以对上传文件进行病毒扫描。

目前,<parser>支持两种过滤器接口:ParameterValueFilterUploadedFileFilter。前者用来对普通的参数值进行过滤(例如排除可能造成攻击的HTML代码);后者用来对上传文件的file item对象进行过滤,就像刚才的uploaded-file-whitelist的例子。

7.4. <buffered> - 缓存response中的内容

7.4.1. 实现原理

Webx Turbine支持用layout/screen/control等部件共同购成一个页面。其中,每个layout可包含一个screen和多个control,每个screen可包含多个control,每个control还可以再包含其它的control。Screen和control的内容都可以用程序代码直接生成:

例 7.32. 在Screen中直接输出页面内容

public class MyScreenOrControl {
    @Autowired
    private HttpServletResponse response;

    public void execute() throws IOException {
        PrintWriter out = response.getWriter();

        out.println("<p>hello world</p>");
    }
}

上面的代码是非常直观、易理解的。事实上,如果你写一个简单的servlet来生成页面,代码也是和上面的类似。

但是,在简单的代码后面有一个玄机 —— 那就是这段代码可被用于生成嵌套的页面部件,它所生成的内容可被上一层嵌套的部件所利用。例如,一个screen中包含了一个control,那么screen可以获得它所调用的control的完整的渲染内容。

这个玄机就是靠<buffered>来实现的。<buffered>改变了response的输出流,包括output stream(二进制流)和writer(文本流),使写到输出流中的内容被暂存在内存中。当需要时,可以取得缓存中的所有内容。

Webx利用<buffered>机制生成嵌套式页面的过程

图 7.2. Webx利用<buffered>机制生成嵌套式页面的过程

如图所示。BufferedRequestContext主要包括了两条用来操作buffer栈的指令:push和pop。

  • 每次push就会在栈顶创建一个新的buffer。

  • 每次pop就会弹出栈顶buffer,并返回其内容。当最后一个buffer被弹出时,就会自动push一个新的buffer,从而确保任何时候栈都非空。

  • 所有写入response.getWriter()response.getOutputStream()输出流的数据,将被保存在栈顶的buffer中。

  • Push和pop必须成对出现。如果在commit时发现栈内有两个或两个以上的buffer存在,说明有push/pop未匹配,则报错。

  • Commit时,将仅存的栈顶buffer提交给浏览器。

<buffered>还有一个重要的作用,就是可以用来支持基于cookie的session机制(参见:第 8 章 Request Context之Session指南)。因为cookie是response header的一部分,根据HTTP协议,headers出现在content的前面。一旦content开始向浏览器输出,headers就不可能再被改变了。这会导致基于cookie的session无法保存的问题。<buffered>将所有的输出内容缓存在内存中,从而避免了response过早地提交给浏览器,也就解决了cookie无法保存的问题。

7.4.2. 使用方法

7.4.2.1. 配置

<buffered>的配置比较简单,没有任何额外的参数。只要像下面这样写就可以了:

例 7.33. 配置<buffered>/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <buffered />
    ...
</services:request-contexts>

7.4.2.2. 操作buffer栈

例 7.34. 操作buffer栈

@Autowired
BufferedRequestContext buffered;

@Autowired
HttpServletResponse response;

...

PrintWriter out = response.getWriter();

buffered.pushBuffer(); // 创建新buffer,并压入栈顶
out.print("world");  // 在新buffer中写入

String content = buffered.popCharBuffer(); // 弹出顶层buffer

out.print("hello, ");
out.print(content); // 写入较低层的buffer

需要注意的是,response中有两种输出流:二进制流response.getOutputStream()和文本流response.getWriter()。与之对应的,BufferedRequestContext也会创建两种类型的buffer。这两种buffer类型是互斥的:

  • 假如你的应用使用了response.getWriter(),那么,你必须使用buffered.popCharBuffer()以取得文本buffer的内容;

  • 假如你的应用使用了response.getOutputStream(),那么,你必须使用buffered.popByteBuffer()以取得二进制buffer的内容。

  • 如果用错,则抛IllegalStateException

7.4.2.3. 关闭buffer机制

Buffer机制会延迟服务器对用户的响应。在大部分情况下,这不会造成明显的问题。但在某些情况下会产生严重的问题。此时,你需要把buffer机制关闭。

例如,动态生成excel文件、PDF文件以及图片文件。这样的需求有如下特点:

  • 数据量大 —— 有可能达到几兆。如果把这样大的数据放在内存中,势必导致服务器性能的下降。

  • 没有layout/screen/control这样的嵌套页面的需求,因此不需要buffer这样的机制来帮倒忙。

  • 无状态,不需要修改session,因此也不需要buffer机制来帮助延迟提交。反过来,对于这样的大文件,提交越早越好 —— 甚至可以在文档还未完全生成的时候,就开始向用户浏览器输出,边生成边下载,从而节省大量的下载时间。

下面的程序代码模拟了一种情况 —— 生成一个120M的PDF文件。每生成1M内容,就故意暂停半秒。这样一来,120M的文件需要大约一分钟才能生成完毕。

例 7.35. 模拟生成PDF文档,关闭buffer以提高性能

public class MyDocument {
    @Autowired
    private BufferedRequestContext buffered;

    @Autowired
    private HttpServletResponse response;

    public void execute() throws Exception {
        buffered.setBuffering(false);

        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=\"mydocument.pdf\"");

        OutputStream out = response.getOutputStream();

        for (int m = 0; m < 120; m++) {
            for (int k = 0; k < 1024; k++) {
                for (int b = 0; b < 1024; b++) {
                    out.write((byte) b);
                }
            }

            // 每生成1M,暂停半秒
            Thread.sleep(500);
        }
    }
}

把上述类代码,放在screen目录中。然后访问URL:http://localhost:8081/myapp/my_document.do,就可以启动下载。

假如不关闭buffer机制,从用户点击下载,到浏览器提示保存文件,中间会相隔一分钟。这种用户体验是不可接受的。更糟糕的是,文件会占用至少120M的服务器内存,这也是几乎不可接受的。关闭buffer机制以后,以上两个问题就没有了:

  • 用户点击下载链接,浏览器立即提示保存文件。

  • 边下载边生成数据,生成数据的时间是一分钟,下载所需的时间也是一分钟左右。

  • 生成的数据立即输出,不会占用过多的内存。

7.5. <lazy-commit> - 延迟提交response

7.5.1. 什么是提交

当浏览器向服务器发出请求,服务器就会返回一个response响应。每个response分成两部分:headers和content。下面是一个HTTP响应的例子:

例 7.36. HTTP请求的headers和content

HTTP/1.0 200 OK
Date: Sat, 08 Jan 2011 23:19:52 GMT
Server: Apache/2.0.63 (Unix)
...

<html>...

在服务器应用响应request的全过程中,都可以向浏览器输出response的内容。然而,已经输出到浏览器上的内容,是不可更改的;还没有输出的内容,还有改变的余地。这个输出的过程,被称为提交(commit)。

Servlet API中有一个方法,可以判定当前的response是否已经被提交。

例 7.37. 判断response是否已经被提交

if (response.isCommitted()) {
    ...
}

在Servlet API中,有下列操作可能导致response被提交:

  • response.sendError()

  • response.sendRedirect()

  • response.flushBuffer()

  • response.setContentLength() 或者response.setHeader("Content-Length", length)

  • response输出流被写入并达到内部buffer的最大值(例如:8KB)

7.5.2. 实现原理

当response被提交以后,一切headers都不可再改变。这对于某些应用(例如cookie-based session)的实现是一个问题。

<lazy-commit>通过拦截response中的某些方法,来将可能导致提交的操作延迟到请求处理结束的时候,也就是request context本身被提交的时候。

<lazy-commit>必须和<buffered>配合,才能完全实现延迟提交。如前所述,<buffered>将所有的输出暂存在内存里,从而避免了因输出流达到内部buffer的最大值(例如:8KB)而引起的提交。

7.5.3. 使用方法

7.5.3.1. 配置

<lazy-commit>的配置比较简单,没有任何额外的参数。只要像下面这样写就可以了:

例 7.38. 配置<lazy-commit>/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <lazy-commit />
    ...
</services:request-contexts>

7.5.3.2. 取得当前response的状态

通过LazyCommitRequestContext接口,你可以访问当前response的一些状态:

表 7.8. 通过LazyCommitRequestContext访问response状态

LazyCommitRequestContext方法名说明
isError() 判断当前请求是否已出错
getErrorStatus() 如果sendError()方法曾被调用,则该方法返回一个error状态值。
getErrorMessage() 如果sendError()方法曾被调用,则该方法返回一个error信息。
isRedirected() 判断当前请求是否已被重定向。
getRedirectLocation() 取得重定向的URI。
getStatus() 取得最近设置的HTTP status

7.6. <rewrite> -重写请求的URL和参数

7.6.1. 概述

<rewrite>的功能和设计完全类似于Apache HTTPD Server所提供的mod_rewrite模块。它可以根据规则,在运行时修改URL和参数。

Rewrite工作原理

图 7.3. Rewrite工作原理

当一个请求进入<rewrite>以后,它的处理过程如上图所示。过程可分为两个大的步骤,即:匹配和执行。

  • 匹配

    1. 取得URL中的path路径。

    2. 用所取得的path,依次匹配rule1、rule2、rule3中的pattern,直到找到第一个匹配。

    3. 假如rule中包含conditions,则测试conditions。如果condtions不满足,则当前的rule匹配失败,回到第2步,继续匹配下一个rules。

    4. 假如rule不包含conditions,或者conditions被满足,则当前的rule匹配成功,进入“执行”阶段。

  • 执行

    1. 执行substitution替换。这可能导致path和参数的改变。

    2. 执行所有的handlers。这为编程者提供了更灵活的手段来改变request中的数据。

    3. 根据substitution中的指示,结束<rewrite>的执行、或者回到匹配阶段,用新的path和参数继续匹配后续的rules。

    4. <rewrite>结束时,根据substitution中的指示,改写request或者重定向到新的URL。

下面是一个<rewrite>配置的模板:

例 7.39. 配置<rewrite>/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <rewrite>

        <!-- rule 1 -->
        <rule pattern="...">
            <condition test="..." pattern="..." flags="..." />
            <condition test="..." pattern="..." flags="..." />
            <substitution uri="..." flags="...">
                <parameter key="..." value="..." />
                <parameter key="..." value="..." />
                <parameter key="..." value="..." />
            </substitution>
            <handlers>
                <rewrite-handlers:handler class="..." />
            </handlers>
        </rule>

        <!-- rule 2 -->
        <rule pattern="...">
        </rule>

        <!-- rule 3 -->
        <rule pattern="...">
        </rule>

    </rewrite>
    ...
</services:request-contexts>

7.6.2. 取得路径

和Apache mod_rewrite不同,用来匹配rules的路径并不是URL的整个路径,而是由servletPath + pathInfo两部分组成,其中并不包含contextPath

这是因为<rewrite>是属于WEB应用的,它只能匹配当前应用中的路径。在基于servlet的WEB应用中,一个完整的URL路径是由contextPath + servletPath + pathInfo三部分组成的。其中contextPath是用来区分应用的,所以对<rewrite>没有意义。

例如,URL是http://localhost:8081/myapp/myservlet/path/path,那么<rewrite>用来匹配rules的路径是:/myservlet/path/path

7.6.3. 匹配rules

下面是一个简单的rule。

例 7.40. 匹配规则的配置

<rule pattern="/test1/hello\.htm">
    ...
</rule>

其中,rule pattern是一个正则表达式。特别需要注意的是,这个正则表达式是部分匹配的。如上例pattern可以匹配下面的路径:

  • /test1/hello.htm

  • /mypath/test1/hello.htm

  • /mypath/test1/hello.htm/mypath

如果你希望匹配整个path,请使用正则表达式的“^”和“$”标记。例如:

例 7.41. 匹配整个path

<rule pattern="^/test1/hello\.htm$">

部分匹配的正则表达式为你提供了较灵活的匹配能力,例如,下面的rule可以用来匹配所有以jpg为后缀的URL。

例 7.42. 后缀匹配

<rule pattern="\.jpg$">

此外,rules pattern还支持否定的pattern —— 即在正常的pattern前加上“!”即可。例如下面的rule匹配所有不以jpg为后缀的URL:

例 7.43. 否定匹配

<rule pattern="!\.jpg$">

7.6.4. 匹配conditions

每个rule都可以包含多个额外的conditions。Conditions提供了除path匹配以外的其它条件。下面是condition配置的基本格式:

例 7.44. 配置conditions

<rule pattern="/path">
    <condition test="..." pattern="..." flags="..." />
    <condition test="..." pattern="..." flags="..." />
    <condition test="..." pattern="..." flags="..." />
    ...
</rule>

每个condition由两个主要的参数:测试表达式和pattern。测试表达式中可以使用下面的变量:

表 7.9. Condition变量

客户端信息
%{REMOTE_HOST} 客户端主机名。相当于request.getRemoteHost()
%{REMOTE_ADDR} 客户端地址。相当于request.getRemoteAddr()
%{REMOTE_USER} 用户名。相当于request.getRemoteUser()
%{AUTH_TYPE} 验证用户的方法。例如BASIC、FORM、CLIENT_CERT、DIGEST等。相当于request.getAuthType()
服务端信息
%{SERVER_NAME} 服务器主机名。相当于request.getServerName()
%{SERVER_PORT} 服务器端口。相当于request.getServerPort()
%{SERVER_PROTOCOL} 服务器协议。相当于request.getProtocol()
请求信息
%{REQUEST_METHOD} HTTP方法名。例如GET、POST等。相当于request.getMethod()
%{REQUEST_URI} 所请求的URI,不包括主机名、端口和参数。相当于request.getRequestURI()
%{QUERY_STRING} 参数和值。注意,对于POST请求取得QUERY_STRING,可能会影响性能。相当于request.getQueryString()
%{QUERY:param} 取得参数值。无论哪一种类型的请求(GET/POST/上传文件),都可以取得参数值。 相当于request.getParameter("param")
HTTP headers
%{HTTP_USER_AGENT} 浏览器名称。相当于request.getHeader("User-Agent")
%{HTTP_REFERER} 前一个URL。相当于request.getHeader("Referer")
%{HTTP_HOST} HTTP请求中的主机名,一般代表虚拟主机。相当于request.getHeader("Host")
%{HTTP_ACCEPT} 浏览器可以接受的文档类型。相当于request.getHeader("Accept")
%{HTTP_COOKIE} 浏览器发送过来的cookie。相当于request.getHeader("Cookie")

Condition pattern和rule pattern类似,也是部分匹配的正则表达式,并且支持否定的pattern。举例说明:

例 7.45. Condition patterns

<rule pattern="/path"> 
    <condition test="%{SERVER_NAME}:%{SERVER_PORT}" pattern="www.(\w+).com:8080" />  
    <condition test="%{QUERY:x}" pattern="!1" /> 
    <condition test="%{QUERY:y}" pattern="2" /> 
</rule>

上面的rule匹配符合以下条件的请求:

匹配路径/path

服务器名为www.*.com,端口为8080

并且参数x!=1

并且参数y=2

默认情况下,必须所有的conditions条件都符合,rule才会继续执行下去。但是condition还支持一个选项:OR或者ornext。如果condtion带有这个选项,只要符合当前condition或者后续的conditions,rule就会执行下去。例如:

例 7.46. 部分匹配conditions

<rule pattern="/path"> 
    <condition test="%{QUERY:x}" pattern="1" flags="OR" /> 
    <condition test="%{QUERY:y}" pattern="2" flags="ornext" /> 
    <condition test="%{QUERY:z}" pattern="3" /> 
</rule>

上例中,“OR”和“ornext”代表完全一样的意思。这个rule匹配符合以下条件的请求:

匹配路径/path

参数x=1

或者y=2

或者z=3

7.6.5. 替换路径

当路径匹配,并且conditions也匹配(如果有的话),那么<rewrite>就会执行所匹配的rule。

例 7.47. 替换路径

<rule pattern="/test1/hello\.htm">
    <substitution uri="/test1/new_hello\.htm" />
</rule>

上例中的rule将执行下面的替换(别忘了,rule支持部分匹配,只有匹配的部分被替换):

  • /test1/hello.htm替换成/test1/new_hello.htm

  • /mypath/test1/hello.htm替换成/mypath/test1/new_hello.htm

  • /mypath/test1/hello.htm/mypath替换成/mypath/test1/new_hello.htm/mypath

路径替换时,还支持正则表达式变量。例如:

例 7.48. 用正则表达式变量替换路径

<rule pattern="/(\w+)\.htm">
    <condition test="%{SERVER_NAME}" pattern="(\w+).blogs.com" />
    <substitution uri="/%1/new_$1\.htm" />
</rule>

需要注意的是,rule pattern中的匹配项,是用“$1”、“$2”、“$3”表示的;而condition pattern中的匹配项,是用“%1”、“%2”、“%3”表示的。只有最后一个被匹配的condition中的匹配项,才被保留用于替换。

上面的rule将执行下面的替换:将http://myname.blogs.com/hello.htm替换成同服务器上的路径:/myname/new_hello.htm

7.6.6. 替换参数

<rewrite>不仅可以替换路径,还可以替换参数。

例 7.49. 替换参数

<rule pattern="/hello.(\w+)">
    <condition test="%{SERVER_NAME}" pattern="www.(\w+).com" />
    <substitution>
        <parameter key="ext" value="$1" />
        <parameter key="host" value="%1" />
        <parameter key="count">
            <value>1</value>
            <value>2</value>
            <value>3</value>
        </parameter>
    </substitution>
</rule>

替换参数和替换路径类似,也可以指定rule和condition pattern中的匹配项。参数支持多值,例如上例中的count参数。 上面的例子将执行以下替换行为:

  • 对于请求:http://www.myserver.com/hello.htm,不改变其路径,只改变其参数:

    • 创建单值参数:ext=htm(从rule pattern中取得$1

    • 创建单值参数:host=myserver(从condition pattern中取得%1

    • 创建多值参数:count=[1, 2, 3]

    • 删除其它所有参数

如果你想保留原来所有参数,只是修改或添加一些参数,可以指定QSAqsappend选项。

例 7.50. 保留原来的参数

<substitution flags="QSA">
    ...
</substitution>

7.6.7. 后续操作

当一个rule和其中的conditions被匹配时,<rewrite>就会执行这个rule。执行的结果通常是改变请求的路径或参数。当一个rule执行完毕以后,接下来做什么呢?有几种可能的情况。

7.6.7.1. 继续匹配剩余的rules

例 7.51. 默认后续操作:继续匹配剩余的rules

<rule pattern="...">
    <substitution uri="..." />
</rule>
<rule pattern="...">
    <substitution uri="..." />
</rule>

上面第一个rule执行完以后,<rewrite>会用改变过的路径和参数去继续匹配余下的规则。这是默认情况。

7.6.7.2. 停止匹配

例 7.52. 后续操作:停止匹配

<rule pattern="...">
    <substitution uri="..." flags="L" />
</rule>
<rule pattern="...">
    <substitution uri="..." />
</rule>

当在substitution中指定L或者last选项时,rule匹配会到此中止。后续的rules不会再被匹配。

7.6.7.3. 串接rules

例 7.53. 后续操作:串接rules

<rule pattern="^/common-prefix">
    <substitution flags="C" />
</rule>
<rule pattern="\.jpg">
    <substitution uri="..." />
</rule>
<rule pattern="\.htm">
    <substitution uri="..." />
</rule>

当在substitution中指定C或者chain选项时,假如当前rule匹配,则会像默认情况一样继续匹配剩余的rules;否则,就像last选项一样立即中止匹配。

串接rules在下面的情况下非常有用:即对一个路径进行匹配多个patterns。例如上面的例子中,第一个rule限定了路径前缀必须是“/common-prefix”,接下来的rules在此基础上继续判断:后缀是“jpg”还是“htm”?

7.6.8. 重定向

例 7.54. 重定向

永久重定向,status code=301

<rule pattern="^/hello1\.htm">
    <substitution uri="/new_hello.htm" flags="L,R=301" />
</rule>

临时重定向,status code=302,不保留参数

<rule pattern="^/hello2\.htm">
    <substitution uri="/new_hello.htm" flags="L,R" />
</rule>

临时重定向,status code=302,保留参数

<rule pattern="^/hello3\.htm">
    <substitution uri="/new_hello.htm" flags="L,R,QSA" />
</rule>

绝对URL重定向,status code=302

<rule pattern="^/hello4\.htm">
    <substitution uri="http://www.other-site.com/new_hello.htm" flags="L,R" />
</rule>

当在substitution中指定R或者redirect的时候,<rewrite>会返回“重定向”的响应。 重定向有两种:301永久重定向,和302临时重定向。默认是302临时重定向,但你可以指定301来产生一个永久的重定向。

通常,R标记会和L标记一起使用,使<rewrite>立即结束。

重定向和QSA标记一起使用时,可以将当前请求的所有参数附加到重定向请求中。不过这里需要注意的是,假如当前请求是一个post请求,那么将参数附加到新的URL中,可能会导致URL过长而重定向失败的问题。

重定向可以指向另一个不同域名的网站 —— 反过来说,假如你希望rewrite到另一个网站,那么你必须指定重定向的选项才行

7.6.9. 自定义处理器

例 7.55. 自定义处理器

<rule pattern="...">
    <handlers>
        <rewrite-handlers:handler class="..." />
        <rewrite-handlers:handler class="..." />
    </handlers>
</rule>

有时候,基于正则表达式替换的substitution不能满足较复杂的需求,好在<rewrite>还提供了另一种机制:自定义处理器。

当rule和conditions被匹配的时候,所有的handlers将被执行。Webx提供了一个handler参考实现:

例 7.56. 自定处理器参考实现:规格化路径

<rule pattern="...">
    <handlers>
        <rewrite-handlers:handler
            class="com.alibaba.citrus.service.requestcontext.rewrite.support.UrlNormalizer"
        />
    </handlers>
</rule>

7.7. 本章总结

本文详细介绍了Request Contexts的功能。

Request Contexts服务是Webx框架的核心功能之一。它看似简单,但却提供了很多有用功能。相对于其它框架中的解决方案,RequestContexts显得更加优雅,因为其中大部分功能对应用程序是透明的 —— 应用程序不需要知道它们的存在,就可以享受它们所提供的功能。