Webx框架指南

淘宝 专栏收录该内容
7 篇文章 0 订阅
引言
1. 阅读向导 2. Webx是什么? 3. Webx的历史 4. 为什么要用Webx而不是其它的开源框架? 5. Webx的优势
5.1. 成熟可靠性 5.2. 开放和扩展性
6. Webx还缺少什么?
部分 I. Webx框架概览
第 1 章 Webx总体介绍
1.1. 设计理念
1.1.1. 框架的本质 1.1.2. 基础框架 1.1.3. 层次化
1.2. Webx的层次
1.2.1. 三个大层次 1.2.2. 剪裁和定制Webx
1.3. 本章总结
第 2 章 SpringExt
2.1. 用SpringExt装配服务
2.1.1. Spring Beans 2.1.2. Spring Schema 2.1.3. SpringExt Schema
2.2. SpringExt原理
2.2.1. XML Schema中的秘密 2.2.2. 扩展点,Configuration Point 2.2.3. 捐献,Contribution 2.2.4. 组件和包 2.2.5. 取得Schemas
2.3. SpringExt其它特性 2.4. 本章总结
第 3 章 Webx Framework
3.1. Webx的初始化
3.1.1. 初始化级联的Spring容器 3.1.2. 初始化日志系统
3.2. Webx响应请求
3.2.1. 增强request、response、session的功能 3.2.2. Pipeline流程机制 3.2.3. 异常处理机制 3.2.4. 开发模式工具 3.2.5. 响应和处理请求的更多细节
3.3. 定制Webx Framework
3.3.1. 定制WebxRootController 3.3.2. 定制WebxController
3.4. 本章总结
第 4 章 Webx Turbine
4.1. 设计理念
4.1.1. 页面驱动 4.1.2. 约定胜于配置
4.2. 页面布局 4.3. 处理页面的基本流程 4.4. 依赖注入
4.4.1. Spring原生注入手段 4.4.2. 注入request、response和session对象 4.4.3. 参数注入
4.5. 定制Webx Turbine 4.6. 本章总结
部分 II. Webx基础设施服务
第 5 章 Resource Loading服务指南
5.1. 资源概述
5.1.1. 什么是资源? 5.1.2. 如何表示资源? 5.1.3. 如何访问资源? 5.1.4. 如何遍历资源? 5.1.5. 有什么问题?
5.2. Spring的ResourceLoader机制
5.2.1. Resource接口 5.2.2. ResourceLoaderResourcePatternResolver接口 5.2.3. 在代码中取得资源 5.2.4. Spring如何装载资源? 5.2.5. Spring ResourceLoader的缺点
5.3. Resource Loading服务
5.3.1. 替换Spring ResourceLoader 5.3.2. 定义新资源 5.3.3. 重命名资源 5.3.4. 重定向资源 5.3.5. 匹配资源 5.3.6. 在多个ResourceLoader中查找 5.3.7. 装载parent容器中的资源 5.3.8. 修改资源文件的内容 5.3.9. 直接使用ResourceLoadingService 5.3.10. 在非Web环境中使用Resource Loading服务
5.4. ResourceLoader参考
5.4.1. FileResourceLoader 5.4.2. WebappResourceLoader 5.4.3. ClasspathResourceLoader 5.4.4. SuperResourceLoader 5.4.5. 关于ResourceLoader的其它考虑
5.5. 本章总结
第 6 章 Filter、Request Contexts和Pipeline
6.1. Filter
6.1.1. Filter的用途 6.1.2. Filter工作原理 6.1.3. Filter的限制 6.1.4. Webx对filter功能的补充
6.2. Request Contexts服务
6.2.1. Request Contexts工作原理 6.2.2. Request Contexts的用途 6.2.3. Request Contexts的使用
6.3. Pipeline服务
6.3.1. Pipeline工作原理 6.3.2. Pipeline的用途 6.3.3. Pipeline的使用
6.4. 本章总结
第 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. 本章总结
第 8 章 Request Context之Session指南
8.1. Session概述
8.1.1. 什么是Session 8.1.2. Session数据存在哪? 8.1.3. 创建通用的session框架
8.2. Session框架
8.2.1. 最简配置 8.2.2. Session ID 8.2.3. Session的生命期 8.2.4. Session Store 8.2.5. Session Model 8.2.6. Session Interceptor
8.3. Cookie Store
8.3.1. 多值Cookie Store 8.3.2. 单值Cookie Store
8.4. 其它Session Store
8.4.1. Simple Memory Store
8.5. 本章总结
部分 III. Webx应用支持服务
第 9 章 表单验证服务指南
9.1. 表单概述
9.1.1. 什么是表单验证 9.1.2. 表单验证的形式
9.2. 设计
9.2.1. 验证逻辑与表现逻辑分离 9.2.2. 验证逻辑和应用代码分离 9.2.3. 表单验证的流程
9.3. 使用表单验证服务
9.3.1. 创建新数据 9.3.2. 修改老数据 9.3.3. 批量创建或修改数据
9.4. 表单验证服务详解
9.4.1. 配置详解 9.4.2. Validators 9.4.3. Form Tool 9.4.4. Field keys的格式 9.4.5. 外部验证
9.5. 本章总结
部分 IV. Webx应用实作
第 10 章 创建第一个Webx应用
10.1. 准备工作
10.1.1. 安装JDK 10.1.2. 安装和配置maven 10.1.3. 安装集成开发环境
10.2. 创建应用 10.3. 运行应用 10.4. 提问和解答
10.4.1. 在生产环境的应用上,也会出现前述的“开发者首页”吗? 10.4.2. “开发模式”是什么意思? 10.4.3. 所生成的应用中包含了什么?
第 11 章 Webx日志系统的配置
11.1. 名词解释
11.1.1. 日志系统(Logging System) 11.1.2. 日志框架(Logging Framework)
11.2. 在Maven中组装日志系统
11.2.1. 在Maven中配置logback作为日志系统 11.2.2. 在Maven中配置log4j作为日志系统
11.3. 在WEB应用中配置日志系统
11.3.1. 设置WEB应用 11.3.2. 定制/WEB-INF/logback.xml(或/WEB-INF/log4j.xml 11.3.3. 同时初始化多个日志系统
11.4. 常见错误及解决
11.4.1. 查错技巧 11.4.2. 异常信息:No log system exists 11.4.3. 异常信息:NoSuchMethodErrororg.slf4j.MDC.getCopyOfContextMap() 11.4.4. STDERR输出:Class path contains multiple SLF4J bindings 11.4.5. 看不到日志输出
11.5. 本章总结
部分 V. 辅助工具
第 12 章 安装和使用SpringExt插件
12.1. SpringExt插件有什么用? 12.2. Maven插件
12.2.1. 在pom.xml中定义插件 12.2.2. 启动schema服务器 12.2.3. 导出所有的schemas 12.2.4. Maven插件的可选参数
12.3. Eclipse插件
12.3.1. 安装插件 12.3.2. 利用插件编辑Webx/SpringExt配置文件 12.3.3. 利用插件编辑SpringExt组件
12.4. 本章总结
第 13 章 AutoConfig工具使用指南
13.1. 需求分析
13.1.1. 解决方案
13.2. AutoConfig的设计
13.2.1. 角色与职责 13.2.2. 分享二进制目标文件 13.2.3. 部署二进制目标文件 13.2.4. AutoConfig特性列表
13.3. AutoConfig的使用 —— 开发者指南
13.3.1. 建立AutoConfig目录结构 13.3.2. 建立auto-config.xml描述文件 13.3.3. 建立模板文件
13.4. AutoConfig的使用 —— 部署者指南
13.4.1. 在命令行中使用AutoConfig 13.4.2. 在maven中使用AutoConfig 13.4.3. 运行并观察AutoConfig的结果 13.4.4. 共享properties文件 13.4.5. AutoConfig常用命令
13.5. 本章总结
第 14 章 AutoExpand工具使用指南
14.1. AutoExpand工具简介
14.1.1. Java、JavaEE打包的格式 14.1.2. 应用部署的方式 14.1.3. AutoExpand的用武之地
14.2. AutoExpand的使用
14.2.1. 取得AutoExpand 14.2.2. 执行AutoExpand 14.2.3. AutoExpand和AutoConfig的合作
14.3. AutoExpand的参数 14.4. 本章总结

引言

1. 阅读向导

[注意]注意

2. Webx是什么?

Webx是一套基于Java Servlet API的通用Web框架。它在Alibaba集团内部被广泛使用。从2010年底,向社会开放源码。

3. Webx的历史

  • 2001年,阿里巴巴内部开始使用Java Servlet作为WEB服务器端的技术,以取代原先的Apache HTTPD server和mod_perl的组合。

  • 2002年,选择Jakarta Turbine作为WEB框架,并开始在此之上进行扩展。

  • 2003年,经过大约一年的扩展,框架开始成熟。我们私下称这个经过改进的Turbine框架为Webx 1.0。

  • 2004年,借着淘宝网的第一次改版,我们正式推出了Webx 2.0。由于Turbine开源项目发展过于缓慢,我们不得不放弃它。Webx 2.0是从零开始完全重写的,仅管它仍然延续了Turbine的使用风格。

  • 2004年11月,Webx 2.0和Spring框架整合。

  • 从那以后,Webx 2.0一直在进化,但没有作根本性的改动。

  • 2010年,Webx 3.0发布。Webx 3.0抛弃了Webx 2.0中过时的、从Turbine中发展而来的Service框架,直接采用Spring作为其基础,并对Spring作了重大改进。Webx 3.0完全兼容Webx 2.0的代码,只需要修改配置文件就可完成升级。

  • 2010年底,Webx 3.0开源。

4. 为什么要用Webx而不是其它的开源框架?

现在有很多Java的Web框架可供选择,并且它们也都是免费的。例如,

以上框架都是非常优秀的。说实话,如果阿里巴巴网站在2001年开始,就有这么多可选择的话,无论选择哪一个都不会有问题。因为这些年来,所有的开源Web框架都在互相学习、并趋于相似。Webx也不例外,它吸收了其它框架的很多想法。因此,当你使用Webx的时候,你会觉得在很多方面,它和其它开源的框架非常类似。

我并不是说所有的框架都一样好,而是说只要假以时日,所有的框架在发展过程中,必然会积聚好的方面,淘汰坏的方面,从而变得足够好。从这个角度看,的确没有特别明显的理由来选择Webx,但也没有明显的理由不选择Webx。

另一方面,由于每一种框架采用不同的设计,必然会有各自的优势。Webx也是如此 —— 它在某些方面有一些独到的设计,超越了同类框架。Webx有哪些优势呢?

5. Webx的优势

5.1. 成熟可靠性

这个优势主要是针对阿里巴巴及属下网站而言。因为Webx在阿里巴巴和淘宝用了很多年。对于这种超大访问量的电子商务网站,Webx经受了考验,被证明是成熟可靠的。

5.2. 开放和扩展性

  • 对Spring的直接支持 —— Spring是当今主流的轻量级框架。Webx 3.0和Spring MVC一样,完全建立在Spring框架之上,故可运用Spring的所有特性。

  • 扩展性 —— Webx 3.0对Spring做了扩展,使Spring Bean不再是“bean”,而是升级成“组件”。一个组件可以扩展另一个组件,也可以被其它组件扩展。这种机制造就了Webx的非常好的扩展性,且比未经扩展的Spring更易使用。

  • 开放性 —— Webx被设计成多个层次,层次间的分界线很清晰。每个层次都足够开放和易于扩展。你可以使用全部的Webx,也可以仅仅使用到Webx的任何一个层次。

6. Webx还缺少什么?

和目前快速发展的开源框架相比,Webx似乎不够时髦,因为它还缺少对很多流行功能的直接支持 —— 并非不支持,而是没有方便的方法来直接完成。例如:

  • 目前Webx只支持服务端的表单验证,而没有直接支持客户端的JS验证。

  • 目前Webx没有直接支持AJAX编程。

  • 目前Webx没有直接支持REST编程。

  • 目前Webx没有直接支持Web Flow。

凡是缺少的功能,我们将在未来的版本中陆续加上。

部分 I. Webx框架概览

第 1 章 Webx总体介绍

本章概要地介绍了Webx框架的整体结构和设计。如果你想了解更多,请参考其它详细文档。

1.1. 设计理念

1.1.1. 框架的本质

框架结构的建筑

图 1.1. 框架结构的建筑

应用框架(Application Framework),让人联想到建筑的框架(Frame Structure)。

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

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

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

Inversion of Control 反转控制应用的流程不是由应用控制的,而是由框架控制的。
Default Behavior 默认行为框架会定义一系列默认的行为。
Extensibility 扩展性应用可以扩展框架的功能,也可以修改框架的默认行为。
Non-modifiable Framework Code 框架本身不可更改框架在被扩展时,自身的代码无须被改变。

在一个框架中,实现丰富的功能固然重要,然而更重要的是:建立良好的扩展机制。我们知道,Webx目前虽然欠缺一些流行的功能。然而Webx却有一个良好的扩展机制,来支持开发者增加新的功能。

1.1.2. 基础框架

纵观开源的Web框架,做得比较好的框架,都有一个共性 —— 它们并不是简单地实现Web应用所需要的功能(诸如Action、模板、表单验证等),而是把框架建立在另一个基础框架之上。这个基础框架的作用是:组装模块;提供扩展机制。建立在这种基础上的Web框架有很好的适应性和扩展性,可以应对Web应用不断变化和发展的需求。

  • 早期的Turbine,建立在Service框架之上。

  • Webwork,建立在Xwork框架之上。

  • Tapestry,建立在HiveMind或Tapestry IOC框架之上。

  • 早期的Struts 1.x由于没有一个轻量框架作为基础,因此很难扩展。而Struts 2.x使用了Webwork和Xwork,因此适用能力大为提高。

  • Spring MVC,建立在Spring框架之上。

一个Web框架的好坏,往往不是由它所实现的具体功能的好坏决定的,而是由其所用的基础框架的好坏决定的。

Webx建立在SpringExt的基础上 —— SpringExt是对Spring的扩展。Spring是当今主流的轻量级框架。SpringExt没有损失任何Spring的功能,但它能够提供比Spring自身更强大的扩展能力。

1.1.3. 层次化

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

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

模块的层次

图 1.2. 模块的层次

如图所示,层次之间有如下的关系:

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

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

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

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

  • 下层扩展上层时,不需要修改到上层的任何代码和配置。即符合开闭原则(Open-Closed Principle简称OCP – Open for extension, Closed for modification)。

  • 每一层均可被替换。

层次化的设计,使软件中的每一个部分都可被增强或替换。

层次化不是自然而然的,而是需要精心的设计。设计一个层次化的组件,可以从下面几方面来考虑:

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

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

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

Webx鼓励层次化的模块设计,而SpringExt提供了创建和配置层次化组件的机制。

1.2. Webx的层次

1.2.1. 三个大层次

很多用过Webx框架的人说起Webx,就想到:Webx如何处理页面、如何验证表单、如何渲染模板等等功能。事实上,这些只不过是Webx最外层、最易变、非本质的功能。

Webx框架不仅鼓励层次化设计,它本身也是层次化的。你既可以使用全部的Webx框架,也可以只使用部分的Webx框架。大体上,Webx框架可以划分成三个大层次,如图所示。

Webx的层次

图 1.3. Webx的层次

  1. SpringExt:基于Spring,提供扩展组件的能力。它是整个框架的基础。

  2. Webx Framework:基于Servlet API,提供基础的服务,例如:初始化Spring、初始化日志、接收请求、错误处理、开发模式等。Webx Framework只和servlet及spring相关 —— 它不关心Web框架中常见的一些服务,例如Action处理、表单处理、模板渲染等。因此,事实上,你可以用Webx Framework来创建多种风格的Web框架。

  3. Webx Turbine:基于Webx Framework,实现具体的网页功能,例如:Action处理、表单处理、模板渲染等。

1.2.2. 剪裁和定制Webx

并非所有的开发者都需要使用Webx的全部。下面列举几种情形。

1.2.2.1. 级别一:仅使用SpringExt,适用于非Web应用、单元测试

对于非Web应用和单元测试,但却想拥有Spring和SpringExt的功能,可以直接创建SpringExt容器:

例 1.1. 直接创建SpringExt容器

import java.io.File;
import org.springframework.core.io.FileSystemResource;
import com.alibaba.citrus.springext.support.context.XmlApplicationContext; 

...

XmlApplicationContext parentContext = new XmlApplicationContext(
        new FileSystemResource(new File(srcdir, "parent.xml")));  

XmlApplicationContext context = new XmlApplicationContext(
        new FileSystemResource(new File(srcdir, "app.xml")), parentContext); 

Object mybean = context.getBean("mybean");

请注意代码所使用的ApplicationContext实现类为SpringExt扩展的类型(c.a.c.springext.s.c.XmlApplicationContext)。通过这个实现类,你除了可以使用原来Spring的所有功能以外,还可以使用SpringExt的所有功能,包括:Schema、Configuration Points和Contributions、ResourceLoadingService等。

这行代码从一个配置文件中创建容器。

这行代码创建了一个子容器。多个子容器和父容器之间可组成一个树状级联的容器结构。在子容器中可以访问到所有父容器中的beans和服务,但反过来是不成立的。

1.2.2.2. 级别二:仅使用SpringExt及Web组件,在此基础上运行Spring MVC、Struts等非webx框架

非webx框架也可以使用SpringExt的全部功能。

例 1.2. 修改/WEB-INF/web.xml,让非webx框架支持SpringExt

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">

    <!-- 初始化日志系统,装载/WEB-INF/log4j.xml或/WEB-INF/logback.xml -->
    <listener>
        <listener-class>com.alibaba.citrus.logconfig.LogConfiguratorListener</listener-class>
    </listener>

    <!-- 初始化Spring容器,装载/WEB-INF/webx.xml, /WEB-INF/webx-*.xml -->
    <listener>
        <listener-class>com.alibaba.citrus.webx.context.WebxContextLoaderListener</listener-class> 
    </listener>

    <!-- 下面配置:Spring MVC、Struts的filter、servlet、mapping... -->
    ……

</web-app>

这里使用了webx的WebxContextLoaderListener来初始化spring容器,而不是用spring原生的ContextLoaderListener。不用担心,前者完全兼容后者。事实上前者是从后者派生而来的。

1.2.2.3. 级别三:仅使用Webx Framework,创造新的Web框架

也许你想做一个新的Web框架 —— 因为你并不想使用Webx Turbine中提供的页面处理的方案,但你仍然可以使用Webx Framework所提供的服务,例如:错误处理、开发模式等。

例 1.3. 修改/WEB-INF/webx.xml,以创建新的WEB框架

<webx-configuration xmlns="http://www.alibaba.com/schema/services">
    <components defaultControllerClass="com.myframework.MyController"> 
        <rootController class="com.myframework.MyRootController" /> 
    </components>
</webx-configuration>

MyController扩展了AbstractWebxController

MyRootController扩展了AbstractWebxRootController

这个方案非常适合作为一个新Web框架的起点 —— 免去了创建Servlet/Filter、初始化Spring容器、处理request/response等繁杂事务,并且完全支持SpringExt的所有功能,此外还包含了错误处理、开发模式等Webx Framework中的一切便利。

另一种以Webx Framework为基础的创建新框架的方法,是从pipeline入手。通过pipeline,理论上可以实现任何框架的功能

1.2.2.4. 级别四:使用整个Webx框架,定制Turbine

假如你想使用几乎大部分Webx的功能,但希望对少数步骤进行改进,你可以修改pipeline。

Webx Turbine本身定义了一套pipeline的实现,但是你完全可以去修改它:插入一些步骤、删除一些步骤、修改一些步骤 —— 所有都取决于你。

最常见的一个需求,是在Webx Turbine中添加权限验证 —— 只需要插入一个步骤就可以做到了。

1.3. 本章总结

Webx框架是一个稳定、强大的Web框架。倒不是说它实现了所有的功能,而是它建立在SpringExt的基础上,具有超强的扩展能力。你可以使用全部的Webx,也可以使用部分Webx。你也可以比较容易地用SpringExt做出自己的可扩展组件。

第 2 章 SpringExt

Webx是一套基于Java Servlet API的通用Web框架。Webx致力于提供一套极具扩展性的机制,来满足Web应用不断变化和发展的需求。而SpringExt正是这种扩展性的基石。SpringExt扩展了Spring,在Spring的基础上提供了一种扩展功能的新方法。

本章将告诉你SpringExt是什么?它能做什么?本章不会涉及太深的细节,如果你想了解更多,请参考其它文档。

2.1. 用SpringExt装配服务

在Webx中有一个非常有用的ResourceLoadingService。现在我们以这个服务为例,来说明SpringExt的用途。

ResourceLoadingService是一个可以从各种输入源中(例如从File System、Classpath、Webapp中)查找和读取资源文件的服务。有点像Linux的文件系统 —— 你可以在一个统一的树形目录结构中,定位(mount)任意文件系统,而应用程序不需要关心它所访问的资源文件属于哪个具体的文件系统。

ResourceLoadingService的结构如图所示。这是一个既简单又典型的面向对象的设计。

Resource Loading服务的设计

图 2.1. Resource Loading服务的设计

下面我们尝试在Spring容器中装配ResourceLoadingService服务。为了更好地说明问题,下文所述的Spring配置是被简化的,未必和ResourceLoadingService的真实代码相吻合。

2.1.1. Spring Beans

在Spring 2.0以前,你只能装配beans,就像下面这样:

例 2.1. 用Spring Beans装配Resource Loading服务

<bean id="resourceLoadingService" class="com.alibaba...ResourceLoadingServiceImpl">
    <property name="mappings">
        <map>
            <entry key="/file" value-ref="fileLoader" />
            <entry key="/webroot" value-ref="webappLoader" />
        </map>
    </property>
</bean>

<bean id="fileLoader" class="com.alibaba...FileResourceLoader">
    <property name="basedir" value="${user.home}" />
</bean>

<bean id="webappLoader" class=" com.alibaba...WebappResourceLoader" />

以上是一个典型的Spring beans的配置方案。这种方案简单易行,很好地体现了Spring的基础理念:IoC(Inversion of Control,依赖反转)。ResourceLoadingServiceImpl并不依赖FileResourceLoader和WebappResourceLoader,它只依赖它们的接口ResourceLoader。至于如何创建FileResourceLoader、WebappResourceLoader、需要提供哪些参数,这种琐事全由spring包办。

然而,其实spring本身并不了解如何创建ResourceLoader的对象、需要用哪些参数、如何装配和注入等知识。这些知识全靠应用程序的装配者(assembler)通过上述spring的配置文件来告诉spring的。也就是说,尽管ResourceLoaderServiceImpl类的作者不需要关心这些琐事,但还是有人得关心。

为了说明问题,我先定义两个角色:“服务提供者”和“服务使用者”(即“装配者”)。在上面的例子中,ResourceLoadingService的作者就是服务的提供者,使用ResourceLoadingService的人,当然就是服务使用者。服务使用者利用spring把ResourceLoadingService和ResourceLoader等其它服务装配在一起,使它们可以协同工作。当然这两个角色有时会是同一个人,但多数情况下会是两个人。因此有必要把这两个角色的职责区分清楚,才能合作。

服务提供者和使用者的关系

图 2.2. 服务提供者和使用者的关系

如图所示。虚线左边代表“服务提供者”的职责,虚线右边代表“服务使用者”(即“装配者”)的职责。

从图中可以看到,Spring的配置文件会依赖于服务实现类的公开API。装配者除非查看源代码(如ResourceLoadingServiceImpl的源码)或者API文档才能精确地获知这些API的细节。这有什么问题呢?

  • 没有检验机制,错误必须等到运行时才会被发现。装配者仅从spring配置文件中,无法直观地了解这个配置文件有没有写对?例如:应该从constructor args注入却配成了从properties注入;写错了property的名称;注入了错误的类型等等。

  • 无法了解更多约束条件。即使装配者查看API源码,也未必能了解到某些约束条件,例如:哪些properties是必须填写的,哪些是可选的,哪些是互斥的?

  • 当服务的实现被改变时,Spring配置文件可能会失败。因为Spring配置文件是直接依赖于服务的实现,而不是接口的。接口相对稳定,而实现是可被改变的。另一方面,这个问题也会阻碍服务提供者改进他们的服务实现。

难怪有人诟病Spring说它只不过是用XML来写程序代码而已。

2.1.2. Spring Schema

这种情况直到Spring 2.0发布以后,开始有所改观。因为Spring 2.0支持用XML Schema来定义配置文件。同样的功能,用Spring Schema来定义,可能变成下面的样子:

例 2.2. 用Spring Schema装配Resource Loading服务

<resource-loading id="resourceLoadingService"
                  xmlns="http://www.alibaba.com/schema/services/resource-loading">
    <resource pattern="/file">
        <file-loader basedir="${user.home}" />
    </resource>
    <resource pattern="/webroot">
        <webapp-loader />
    </resource>
</resource-loading>

怎么样?这个配置文件是不是简单很多呢?和直接使用Spring Beans配置相比,这种方式有如下优点:

  • 很明显,这个配置文件比起前面的Spring Beans风格的配置文件简单易读得多。因为在这个spring配置文件里,它所用的“语言”是“领域相关”的,也就是说,和ResourceLoadingService所提供的服务内容相关,而不是使用像bean、property这样的编程术语。这样自然易读得多。

  • 它是可验证的。你不需要等到运行时就能验证其正确性。任何一个支持XML Schema的标准XML编辑器,包括Eclipse自带的XML编辑器,都可以告诉你配置的对错。

  • 包含更多约束条件。例如,XML Schema可以告诉你,哪些参数是可选的,哪些是必须填的;参数的类型是什么等等。

  • 服务的实现细节对装配者隐藏。当服务实现改变时,只要XML Schema是不变的,那么Spring的配置就不会受到影响。

以上优点中,最后一点是最重要优点。通过Spring Schema来定义配置文件,装配者无须再了解诸如“ResourceLoadingService的实现类是什么”、“需要什么参数”等细节。那么Spring是如何得知这些内容呢?

奥秘在于所有的schema都会有一个“解释器”和它对应(即BeanDefinitionParser)。这个解释器负责将符合schema定义的XML配置,转换成Spring能解读的beans定义。解释器是由服务的开发者来提供的 —— 在本例中,ResourceLoadingService的开发者会提供这个解释器。

用Schema改善服务角色之间的关系

图 2.3. 用Schema改善服务角色之间的关系

如图所示,虚线右侧的装配者,不再需要了解服务具体实现类的API,它只要遵循标准的XML Schema定义来书写spring配置文件,就可以得到正确的配置。这样一来,虚线左侧的服务提供者就有自由可以改变服务的实现类,他相信只要服务的接口和XML Schema不改变,服务的使用者就不会受影响。

将和具体实现相关的工作,例如提供类名、property名称和类型等工作,交还给服务的提供者,使服务的使用者(即装配者)可以用它所能理解的语言来装配服务,这是Spring Schema所带来的核心价值。

然而,Spring Schema有一个问题 —— 它是不可扩展的。

仍以ResourceLoadingService为例。仅管在API层面, ResourceLoadingService支持任何对ResourceLoader接口的扩展,例如,你可以添加一种新的DatabaseResourceLoader,以便读取数据库中的资源。但在Spring配置文件上,你却无法自由地添加新的元素。比如:

例 2.3. 尝试在Spring Schema所装配的Resource Loading服务中,添加新的装载器

<resource-loading id="resourceLoadingService"
                  xmlns="http://www.alibaba.com/schema/services/resource-loading">
    <resource pattern="/file">
        <file-loader basedir="${user.home}" />
    </resource>
    <resource pattern="/webroot">
        <webapp-loader />
    </resource>
    <resource pattern="/db">
        <database-loader connection="jdbc:mysql:mydb" /> 
    </resource>
</resource-loading>

装配者希望在这里添加一种新的装载器:database-loader。然而,如果在设计<resource-loading>的schema时,并没有预先考虑到database-loader这种情况,那么这段配置就会报错。

使用Spring Schema时,装配者无法自主地往Spring配置文件中增加新的Resource Loader类型,除非通知服务提供者去修改<resource-loading>的schema —— 然而这违反了面向对象设计中的基本原则 —— OCP(Open Closed Principle)。OCP原则是面向对象设计的强大之源。它使得我们可以轻易地添加新的功能,却不需要改动老的代码;它使设计良好的代码成果可以被叠加和组合,以便实现更复杂的功能。

从本质意义来讲,Schema是API的另一种表现形式。你可以把Schema看作一种接口,而接口的实质是服务的提供者与使用者之间的合约(contract)。可惜的是,我们只能在传统API层面来贯彻OCP原则,却无法在Schema上同样遵循它。我们无法做到不修改老的schema,就添加新的元素 —— 这导致Spring Schema的作用被大大削弱。

2.1.3. SpringExt Schema

SpringExt改进了Spring,使得Spring Schema可以被扩展。下面的例子对例 2.2 “用Spring Schema装配Resource Loading服务”作了少许修改,使之能被扩展。

例 2.4. 用SpringExt Schema装配Resource Loading服务

<resource-loading id="resourceLoadingService"
                  xmlns="http://www.alibaba.com/schema/services"
                  xmlns:loaders="http://www.alibaba.com/schema/services/resource-loading/loaders"> 
    <resource pattern="/file">
        <loaders:file-loader basedir="${user.home}" /> 
    </resource>
    <resource pattern="/webroot">
        <loaders:webapp-loader /> 
    </resource>
</resource-loading>

重新定义namespaces —— 将ResourceLoader<resource-loading>所属的namespace分离。

 

file-loaderwebapp-loader放在loaders名字空间中,表示它们是Resource Loaders的扩展。

上面的配置文件和前例中使用Spring Schema的配置文件差别很小。没错,SpringExt Schema和Spring Schema是完全兼容的!唯一的差别是,我们把ResourceLoader<resource-loading>所属的namespace分开了,然后将ResourceLoader的配置放在专属的namespace “loaders”中。例如:<loaders:file-loader>。这样一来,我们就有办法在不修改<resource-loading>的schema的前提下,添加新的ResourceLoader实现。例如我们要添加一种新的ResourceLoader扩展 —— DatabaseResourceLoader,只需要做以下两件事:

  1. 将包含DatabaseResourceLoader所在的jar包添加到项目的依赖中。如果你是用maven来管理项目,那么意味着你需要修改一下项目的pom.xml

  2. 在spring配置文件中添加如下行:

    例 2.5. 在SpringExt Schema所装配的Resource Loading服务中,添加新的装载器

    <resource-loading id="resourceLoadingService"
                      xmlns="http://www.alibaba.com/schema/services"
                      xmlns:loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
        <resource pattern="/file">
            <loaders:file-loader basedir="${user.home}" />
        </resource>
        <resource pattern="/webroot">
            <loaders:webapp-loader />
        </resource>
        <resource pattern="/db">
            <loaders:database-loader connection="jdbc:mysql:mydb" /> 
        </resource>
    </resource-loading>

    添加一个新的loader,而无须改变<resource-loading>的schema。

完美!你无须通知ResourceLoadingService的作者去修改它的schema,一种全新的ResourceLoader扩展就这样被注入到ResourceLoadingService中。正如同你在程序代码里,无须通知ResourceLoadingService的作者去修改它的实现类,就可以创建一种新的、可被ResourceLoadingService调用的ResourceLoader实现类。这意味着,我们在Spring配置文件的层面上,也满足了OCP原则。

2.2. SpringExt原理

2.2.1. XML Schema中的秘密

下面这段配置是例 2.5 “在SpringExt Schema所装配的Resource Loading服务中,添加新的装载器”的spring配置文件的片段。

<resource-loading>
    ...
    <resource pattern="/db">
        <loaders:database-loader connection="jdbc:mysql:mydb" /> 
    </resource>
</resource-loading>

其中,<resource-loading>是由resource-loading.xsd这个schema来定义的。而在开发resource-loading服务的时候,database-loader这种新的扩展还不存在 —— 也就是说,resource-loading.xsd对于database-loader一无所知。可为什么以上配置能通过XML Schema的验证呢?我们只需要查看一下resource-loading.xsd就可以知道答案了:

例 2.6. Schema片段:<resource-loading>中如何定义loaders

<xsd:element name="resource" type="ResourceLoadingServiceResourceType">
<xsd:complexType name="ResourceLoadingServiceResourceType">
    <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:any namespace="http://www.alibaba.com/schema/services/resource-loading/loaders" /> 
    </xsd:choice>
    <xsd:attribute name="pattern" type="xsd:string" use="required" />
</xsd:complexType>

这里运用了XML Schema中的<xsd:any>定义,相当于说:<resource> element下面,可以跟任意多个<loaders:*> elements。

<xsd:any>定义只关心namespace,不关心element的名称,自然可以接受未知的<database-loader> element,前提是<database-loader>的namespace是“http://www.alibaba.com/schema/services/resource-loading/loaders”。

在这段配置中,<loaders:database-loader>标签通知SpringExt:将database-loader的实现注入到resource-loading的服务中。这种对应关系是如何建立起来的呢?

在XML里,loaders前缀代表namespace:“http://www.alibaba.com/schema/services/resource-loading/loaders”;但对SpringExt而言,它还代表一个更重要的意义:扩展点,或称为ConfigurationPoint。ConfigurationPoint将namespace和可扩展的ResourceLoader接口关联起来。

在XML里,database-loader代表一个XML element;但对SpringExt而言,它还代表一个更重要的意义:捐献,或称为Contribution。Contribution将element和对ResourceLoader接口的具体扩展关联起来。

SpringExt的概念:扩展点和捐献

图 2.4. SpringExt的概念:扩展点和捐献

2.2.2. 扩展点,Configuration Point

SpringExt用“扩展点,Configuration Point”来代表一个可被扩展的接口。每个扩展点都:

  • 对应一个唯一的名称,例如:services/resource-loading/loaders

  • 对应一个唯一的namespace,例如:http://www.alibaba.com/schema/services/resource-loading/loaders

  • 对应一个唯一的schema,例如:services-resource-loading-loaders.xsd

2.2.3. 捐献,Contribution

SpringExt把每一个对扩展点的具体扩展称作“捐献,Contriubtion”。每个捐献都:

  • 在对同一扩展点的所有捐献中,拥有一个唯一的名字,例如:file-loaderwebapp-loaderdatabase-loader等。

  • 对应一个唯一的schema,例如:

    • services/resource-loading/loaders/file-loader.xsd

    • services/resource-loading/loaders/webapp-loader.xsd

    • services/resource-loading/loaders/database-loader.xsd

2.2.4. 组件和包

在前面的例子中,resource-loading服务调用了loaders扩展点,而file-loaderwebapp-loader等则扩展了loaders扩展点。然而事实上,resource-loading服务本身也是对另一个扩展点“services”的扩展。services扩展点是Webx内部定义了一个顶级扩展点

在SpringExt中,一个模块既可以成为别的模块的扩展,也可以被别的模块来扩展。这样的模块被称为“组件”。

组件

图 2.5. 组件

如图所示,resource-loading组件既扩展了services扩展点,又可被其它组件所扩展。

当你需要增加一种新的扩展时,你不需要改动原有包(例如resource-loadings.jar)中的任何内容,你只需要将新的扩展所在的jar包(例如database-loader.jar)加入到依赖表中即可。假如你使用maven来管理项目,意味着你需要修改项目的pom.xml描述文件,以便加入新的扩展包。

2.2.5. 取得Schemas

最后剩下的一个问题是,如何找到Schemas?为了找到schema,我们必须在Spring配置文件中指定Schema的位置。

例 2.7. 在XML中指定Schema Location

<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:services="http://www.alibaba.com/schema/services"
    xmlns:loaders="http://www.alibaba.com/schema/services/resource-loading/loaders"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.alibaba.com/schema/services
            http://localhost:8080/schema/services.xsd 
        http://www.alibaba.com/schema/services/resource-loading/loaders
            http://localhost:8080/schema/services-resource-loading-loaders.xsd 
        http://www.springframework.org/schema/beans
            http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd 
    ">
    ...
</beans:beans>

  

指定schema的位置。

这里看起来有一点奇怪,因为它把schema的位置(xsi:schemaLocation)指向了一台本地服务器:localhost:8080。为什么这样做呢?要回答这个问题,先要搞清楚另一个问题:有哪些部件需要用到schema?

2.2.5.1. XML编辑器需要读取schemas

XML编辑器通过访问schema,可以实现两大功能:

  • 语法提示的功能。

    Eclipse XML编辑器弹出的语法提示

    图 2.6. Eclipse XML编辑器弹出的语法提示

  • 验证spring配置文件的正确性。

    Eclipse XML编辑器验证spring配置文件时,显示的错误信息

    图 2.7. Eclipse XML编辑器验证spring配置文件时,显示的错误信息

XML编辑器取得schema内容的途径有两条,一条途径是访问schemaLocation所指示的网址。因此,

  • 假如你声明的schemaLocation为:http://www.alibaba.com/schema/services.xsd,那么XML编辑器就会尝试访问www.alibaba.com服务器。

  • 假如你声明的schemaLocation为: http://www.springframework.org/schema/beans/spring-beans.xsd,那么XML编辑器就会尝试访问www.springframework.org服务器。

然而,在外部服务器(例如www.alibaba.comwww.springframework.org)上维护一套schema是很困难的,因为:

  • 你未必拥有外部服务器的控制权;

  • 你很难让外部服务器上的schema和你的组件版本保持一致;

  • 当你无法连接外部服务器的时候(例如离线状态),会导致XML编辑器无法帮你验证spring配置文件的正确性,也无法帮你弹出语法提示。

XML编辑器取得schema内容的另一条途径是将所有的schema转换成静态文件,然后定义一个标准的XML Catalog来访问这些schema文件。然而这种方法的难点类似于将schema存放在外部服务器上 —— 你很难让这些静态文件和你的组件版本保持一致。

SpringExt提供了两个解决方案,可以完全解决上述问题 —— 使用maven或eclipse插件。你可以使用SpringExt所提供的maven插件,在localhost本机上启动一个监听8080端口的Schema Server,通过它就可以访问到所有的schemas:

mvn springext:run

上述命令执行以后,打开浏览器,输入网址http://localhost:8080/schema就可以看到类似下面的内容:

用SpringExt maven插件罗列schemas

图 2.8. 用SpringExt maven插件罗列schemas

这就是为什么例 2.7 “在XML中指定Schema Location”中,把schemaLocation指向localhost:8080的原因。只有这样,才能让任何普通的XML编辑器不需要任何特殊的设置,就可以读到正确的schema。

你也可以使用Eclipse插件 —— 这比maven插件更方便,只要你是用eclipse来开发应用的话。

关于这两个插件,详情请见:第 12 章 安装和使用SpringExt插件

2.2.5.2. SpringExt需要读取schemas

当SpringExt在初始化容器时,需要读取schema以验证spring配置文件。

请记住,SpringExt永远不需要通过访问网络来访问schemas。事实上,即使你把例 2.7 “在XML中指定Schema Location”中的schema的网址改成指向“外部服务器”的链接,SpringExt也不会真的去访问它们。例如:

  • 将:http://localhost:8080/schema/services.xsd

    改成:http://www.alibaba.com/schema/services.xsd

  • 将:http://localhost:8080/schema/services-resource-loading-loaders.xsd

    改成:http://www.alibaba.com/schema/services-resource-loading-loaders.xsd

  • 将:http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd

    改成:http://www.springframework.org/schema/beans/spring-beans.xsd(这个就是spring原来的schema网址了)

以上修改在任何时候都不会影响Spring的正常启动。Spring是通过一种SpringExt定制的EntityResolver来访问schemas的。SpringExt其实只关注例子中加亮部分的schema网址,而忽略前面部分。

然而,如前所述,上面两种网址对于普通的XML编辑器来说是有差别的。因此,SpringExt推荐总是以“http://localhost:8080/schema”作为你的schemaLocation网址的前缀。下面的图总结了SpringExt是如何取得schemas的。

SpringExt如何取得schemas

图 2.9. SpringExt如何取得schemas

2.3. SpringExt其它特性

SpringExt实际上是一个增强了的Spring的ApplicationContext容器。除了提供前面所说的Schema扩展机制以外,SpringExt还提供了一个增强的Resource Loading机制。前文例子中所说的Resource Loading服务是Webx中的真实功能,而且它能完全取代Spring原有的ResourceLoader功能 —— 也就是说,应用程序并不需要直接调用ResourceLoading服务,它们可以直接使用Spring本身的ResourceLoader功能,其背后的ResourceLoading机制就会默默地工作

如果不加额外的配置,SpringExt context所用的ResourceLoader实现和Spring自带的完全相同。然而,你只要添加类似下面的配置,Spring的ResourceLoader就会被增强:

例 2.8. 配置Webx resource-loading服务

<services:resource-loading xmlns="http://www.alibaba.com/schema/services">
    <resource-alias pattern="/" name="/webroot" />

    <resource-alias pattern="/myapp" name="/webroot/WEB-INF" />

    <resource pattern="/webroot" internal="true">
        <res-loaders:webapp-loader />
    </resource>
    <resource pattern="/classpath" internal="true">
        <res-loaders:classpath-loader />
    </resource>
</services:resource-loading>

一种典型的Resource Loading服务的用途是读取CMS生成的模板。假设模板引擎从装载模板/templates,默认情况下,/templates就在webapp的根目录下。但是有一部分模板/templates/cms是由外部的内容管理系统(CMS)生成的,这些模板文件并不在webapp目录下。对此,我们只需要下面的配置:

例 2.9. 配置CMS目录

    <resource pattern="/templates/cms">
        <res-loaders:file-loader basedir="${cms.dir}" />
    </resource>

这样,在模板引擎浑然不知的情况下,我们就把/templates/cms目录指向webapp外部的一个文件系统目录,而保持/templates下其它模板的位置不变。

2.4. 本章总结

至此,我们简单领略了SpringExt所带来的好处和便利。SpringExt完全兼容Spring原来schema的概念和风格,但是却可以让schema像程序代码一样被扩展。Webx完全建立在SpringExt的基础上。这个基础决定了Webx是一个高度可扩展的框架,其配置虽然灵活,却又不失方便和直观。

第 3 章 Webx Framework

Webx是一套基于Java Servlet API的通用Web框架。整个Webx框架分成三个层次,本章将简单介绍其第二个层次:Webx Framework。事实上,这是第一个真正涉足WEB技术的层次。前一个层次SpringExt只是提供了一个通用的扩展机制。

Webx Framework负责完成一系列基础性的任务,如下表所示:

表 3.1. Webx Framework的任务

系统初始化 响应请求
初始化Spring容器增强request/response/session的功能
初始化日志系统提供pipeline流程处理机制
 异常处理
 开发模式

本章不会涉及太深的细节,如果你想了解更多,请参考其它文档。

3.1. Webx的初始化

3.1.1. 初始化级联的Spring容器

Webx Framework将负责创建一组级联的Spring容器结构。Webx所创建的Spring容器完全兼容于Spring MVC所创建的容器,可被所有使用Spring框架作为基础的WEB框架所使用。

例 3.1. 初始化Spring容器 - /WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">
    ...
    <listener>
        <listener-class>com.alibaba.citrus.webx.context.WebxContextLoaderListener</listener-class> 
    </listener>
    ...
</web-app>

Webx利用WebxContextLoaderListener来初始化Spring,用来取代Spring的ContextLoaderListener。事实上,前者是从后者派生的。

Webx Framework将会自动搜索/WEB-INF目录下的XML配置文件,并创建下面这种级联的spring容器。

级联的Spring容器

图 3.1. 级联的Spring容器

如图所示。Webx Framework将一个WEB应用分解成多个小应用模块:app1app2,当然名字可以任意取。

  • 每个小应用模块独享一个Spring Sub Context子容器。两个子容器之间的beans无法互相注入。

  • 所有小应用模块共享一个Spring Root Context根容器。根容器中的bean可被注入到子容器的bean中;反之不可以。

将一个大的应用分解成若干个小应用模块,并使它们的配置文件相对独立,这是一种很不错的开发实践。然而,如果你的应用确实很简单,你不希望把你的应用分成多个小应用模块,那么,你还是需要配置至少一个小应用模块(子容器)。

3.1.2. 初始化日志系统

每个现代的WEB应用,都需要日志系统。流行的日志系统包括Log4j、Logback。

Webx Framework使用SLF4J作为它的日志框架。因此Webx Framework理论上支持所有日志系统。然而目前为止,它只包含了log4j和logback这两种日志系统的初始化模块(如有需要,可以扩充)。初始化日志系统很简单。

例 3.2. 初始化日志系统 - /WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">
    ...
    <listener>
        <listener-class>com.alibaba.citrus.logconfig.LogConfiguratorListener</listener-class> 
    </listener>
    ...
</web-app>

Webx利用LogConfiguratorListener来初始化日志系统。

LogConfiguratorListener会根据你当前应用所依赖的日志系统(通常配置在maven project中),来自动选择合适的日志配置文件。

  • 假设你的应用依赖了logback的jar包,那么listener就会查找/WEB-INF/logback.xml,并用它来初始化logback;

  • 如果你的应用依赖了log4j的jar包,那么listener也会很聪明地查找/WEB-INF/log4j.xml配置文件。

  • 假如以上配置文件不存在,listener会使用默认的配置 —— 把日志打印在控制台上。

  • Listener支持对配置文件中的placeholders进行替换。

  • Listener支持同时初始化多种日志系统。

[注意]注意

有关日志系统的使用方法,另有文档详细讲述。

3.2. Webx响应请求

Webx Framework如何响应请求

图 3.2. Webx Framework如何响应请求

当Webx Framework接收到一个来自WEB的请求以后,实际上它主要做了两件事:

  1. 首先,它会增强request、response、session的功能,并把它们打包成更易使用的RequestContext对象。

  2. 其次,它会调用相应子应用的pipeline,用它来做进一步的处理。

  3. 假如在上面的过程中出现异常,则会触发Webx Framework处理异常的过程。

此外,Webx Framework还提供了一组辅助开发的功能,例如查看环境变量,查看schema等。这些功能只在开发模式生效,生产模式下自动关闭。

3.2.1. 增强request、response、session的功能

Webx Framework提供了一个request contexts服务。Request contexts服务利用HttpServletRequestWrapperHttpServletResponseWrapper对request和response对象进行包装,以实现新的功能。

一个基本的request contexts的配置看起来是下面的样子:

例 3.3. 配置request contexts服务

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <basic />
    <buffered />
    <lazy-commit />
    <parser />
    <set-locale defaultLocale="zh_CN" defaultCharset="UTF-8" />
    ...
</services:request-contexts>

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

Request contexts所有的功能都是可配置、可扩展的 —— 它是基于SpringExt的扩展机制。

Request contexts所增加的功能对于所有的基于标准Servlet API的应用都是透明的 —— 这些应用根本不需要知道这些扩展的存在。例如,假如你在request contexts服务中配置了增强的session框架,那么所有通过标准的Servlet API取得session的应用,都将获得新功能:

例 3.4. 取得增强的session对象

HttpSession session = request.getSession();

再比如,只要你配置了upload服务,那么下面的调用将同样适用于multipart/form-data类型的请求(Servlet API本身是不支持upload表单的):

例 3.5. 取得upload表单的参数

String value = request.getParameter("myparam");
[注意]注意

有关Request Contexts的原理和使用方法的详情,请参阅第 6 章 Filter、Request Contexts和Pipeline

3.2.1.1. Request contexts中可用的功能

表 3.2. 可用的RequestContext扩展

名称 说明
<basic> 对输入、输出的数据进行安全检查,排除可能的攻击。例如:XSS过滤、CRLF换行回车过滤等。
<buffered> 对写入response中的数据进行缓存,以便于实现嵌套的页面。
<lazy-commit> 延迟提交response,用来支持基于cookie的session。
<parser> 解析用户提交的参数,无论是普通的请求,还是multipart/form-data这样的用于上传文件的请求。
<set-locale> 设置当前请求的区域(locale)、编码字符集(charset)。
<rewrite> 改写URL及参数,类似于Apache HTTPD Server中的rewrite模块。
<session>增强的Session框架,可将session中的对象保存到cookie、数据库或其它存储中。
[注意]注意

有关以上所有Request Contexts的详情,请参阅第 7 章 Request Contexts功能指南第 8 章 Request Context之Session指南

3.2.1.2. 注入特殊对象

在Webx中,你可以这样做,例如:

例 3.6. 注入request、response、session

public class LoginAction {
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private HttpSession session;
    ...
}

在这个例子中,LoginAction类可以是一个singleton。一般来说,你不能把request scope的对象,注入到singleton scope的对象中。但你可以把HttpServletRequestHttpServletResponseHttpSession对象注入到singleton对象中。为什么呢?原来,Request contexts服务对这几个常用对象进行了特殊处理,将它们转化成了singleton对象。

如果没有这个功能,那么我们就不得不将上例中的LoginAction配置成request scope。这增加了系统的复杂性,也成倍地降低了性能。而将LoginAction设置成singleton,只需要在系统启动时初始化一次,以后就可以快速引用它。

3.2.2. Pipeline流程机制

Webx Framework赋予开发者极大的自由,来定制处理请求的流程。这种机制就是pipeline。

Pipeline的意思是管道,管道中有许多阀门(Valve),阀门可以控制水流的走向。Webx Framework中的pipeline可以控制处理请求的流程的走向。如图所示。

Pipeline工作原理示意

图 3.3. Pipeline工作原理示意

Webx Framework并没有规定管道的内容 —— 定制管道是应用开发者的自由。然而Webx Framework提供了一系列通用valves,你可以使用它们:

表 3.3. 通用valves

分类 Valves 说明
循环 <while> 有条件循环
<loop> 无条件循环
选择分支 <if> 单分支
<choose><when><otherwise> 多分支
中断 <break> 无条件中断

<break-if>

<break-unless>

有条件中断
<exit> 无条件退出整个pipeline(结束所有的嵌套层次)
异常捕获 <try-catch-finally> 类似Java中的try-catch-finally结构
嵌套 <sub-pipeline>创建嵌套的子pipeline。
[注意]注意

有关Pipeline的原理和使用方法的详情,请参阅第 6 章 Filter、Request Contexts和Pipeline

3.2.3. 异常处理机制

当应用发生异常时,Webx Framework可以处理这些异常。

表 3.4. Webx如何处理异常

条件 处理
开发模式 展示详细出错信息。
生产模式 假如存在exception pipeline 用exception pipeline来处理异常;
不存在exception pipeline显示web.xml中定义的默认错误页面。

3.2.4. 开发模式工具

Webx Framework提供了一个开关,可以让应用运行于“生产模式(Production Mode)”或是“开发模式(Development Mode)” 。

例 3.7. 配置运行模式

<services:webx-configuration>
    <services:productionMode>${productionMode:true}</services:productionMode> 
</services:webx-configuration>

使用这行配置,并且在启动应用服务器时指定参数“-DproductionMode=false”,就会让Webx以开发模式启动。

在开发模式下,会有一系列不同于生产模式的行为。

  • 不同的主页 —— 在开发模式的主页中,可以查看和查询系统内部的信息。

    开发模式的主页

    图 3.4. 开发模式的主页

  • 不同的详细出错页面。

    开发模式的详细出错页面

    图 3.5. 开发模式的详细出错页面

  • 开发模式下,可展示所有可用的schemas。

    开发模式下展示所有可用的schemas

    图 3.6. 开发模式下展示所有可用的schemas

  • 开发模式下,可以查阅容器内部的信息。

    开发模式下查阅容器内部的信息

    图 3.7. 开发模式下查阅容器内部的信息

    可供查阅的信息包括:

    表 3.5. 开发模式中可供查阅的容器信息

    名称 说明
    Beans

    查看各Spring容器中的全部bean的定义。

    这个工具有助于开发者理解用schema所定义的services和spring beans之间的联系。

    Configurations

    查看用来创建各Spring容器的配置文件。

    这个工具会以树状和语法高亮显示配置文件以及所有被import的配置文件的内容。

    不同于Beans工具,Configurations工具只忠实地展现配置文件的内容。而Beans工具展现的是真实的Beans结构。

    Resolvable Dependencies

    查看所有由框架置入到容器中的对象,例如:HttpServletRequest对象。这些对象不需要在配置文件中定义,就可被注入到应用中。

    Resources

    跟踪Resources的装载过程,显示Resources的树状结构。

    这个工具有助于开发者理解ResourceLoadingService的工作原理。

    URIs

    查看所有的URI brokers。

    Pull Tools

    查看所有模板中可用的pull tools。

事实上,Webx Framework提供了一套专用的内部框架,使你可以往开发模式中添加更多的开发工具。例如,创建下面的功能并非难事:

  • 查看session对象。

  • 提供各种编码、解码的工具,以方便开发、调试应用。例如:将UTF-8编码的字符串转换成GBK编码;或者将字符串进行URL escape编码、解码等。

Webx Framework提供了一个接口:ProductionModeAware。Spring context中的beans,如果实现了这个接口,就可以感知当前系统的运行模式,从而根据不同的模式选择不同的行为 —— 例如:在生产模式中打开cache,在开发模式中关闭cache。

例 3.8. 利用ProductionModeAware接口感知运行模式,并自动开关cache

public class ModuleLoaderServiceImpl implements ProductionModeAware {  
    public void setProductionMode(boolean productionMode) { 
        this.productionMode = productionMode;
    }

    @Override
    protected void init() {
        ……
        if (cacheEnabled == null) {
            cacheEnabled = productionMode; 
        }
        ……
    }
}

 

实现ProductionModeAware接口。

根据当前运行模式自动开关cache。

3.2.5. 响应和处理请求的更多细节

当一个HTTP请求到达时,首先由WebxFrameworkFilter接手这个请求:

例 3.9. 配置WebxFrameworkFilter - /WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">
    ...
    <filter>
        <filter-name>webx</filter-name>
        <filter-class>com.alibaba.citrus.webx.servlet.WebxFrameworkFilter</filter-class> 
        <init-param>
            <param-name>excludes</param-name>
            <param-value><!-- 需要被“排除”的URL路径,以逗号分隔,前缀!表示“包含”。例如/static, *.jpg, !/uploads/*.jpg --></param-value>  
        </init-param>
        <init-param>
            <param-name>passthru</param-name>
            <param-value><!-- 需要被“略过”的URL路径,以逗号分隔,前缀!表示“不要略过”。例如/myservlet, *.jsp --></param-value>  
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>webx</filter-name>
        <url-pattern>/*</url-pattern>  
    </filter-mapping>
    ...
</web-app>

定义WebxFrameworkFilter

可选的参数:“排除”指定名称的path,以逗号分隔,例如:/static*.jpg。如果路径以!开始,表示“不排除”特殊目录。例如:*.jpg, !/uploads/*.jpg表示排除所有JPG图像文件,但不排除/uploads目录下的JPG图像文件。

可选的参数:“略过”指定名称的path,以逗号分隔,例如:/myservlet*.jsp。和excludes参数一样,也支持!前缀,表示“不要略过”特殊目录。

匹配所有的path。

为什么使用filter而不是servlet呢?传统的WEB框架的控制器一般都是用servlet实现的。原因是:

  • Filter可以“返还控制” —— 上面的配置文件直接把“/*”映射到webx filter中,这意味着webx接管了这个应用的所有请求。静态页面和资源怎么办?没关系,如果webx发现这个请求不应该由webx来处理,就会把控制“返还”给原来的控制器 —— 可能是另一个filter、servlet或者返回给servlet引擎,以默认的方式来处理。而Servlet是不具备“返还控制”的机制的。

  • Servlet/Filter mapping的局限性 —— 标准的servlet引擎将URL映射到filter或servlet时,只支持前缀映射和后缀映射两种方式,非常局限。而实际情况往往复杂得多。Webx建议将所有请求都映射给webx来处理,让webx对请求做更灵活的映射。

如果你的web.xml中还有一些其它的servlet mappings,为了避免和Webx的URL起冲突,你可以把这些mapping加在excludespassthru参数里。这样,WebxFrameworkFilter就会排除或略过指定的URL。例如:

        <init-param>
            <param-name>excludes</param-name>
            <param-value>/static, *.jpg, !/uploads/*.jpg</param-value>
        </init-param>
        <init-param>
            <param-name>passthru</param-name>
            <param-value>/myservlet, *.jsp</param-value>
        </init-param>

passthru略过”和“excludes排除”的区别在于,如果一个servlet或filter接手被webx passthru的请求时,它们还是可以访问到webx的部分服务,包括:

  • RequestContext服务,例如:解析参数、解析upload请求、重写请求、设置字符集编码和区域、基于cookie的session等。

  • 开发模式及工具。

  • 异常处理。

  • 共享webx的spring容器。

也就是说,对于一个被passthru的请求,webx的行为更像是一个普通的filter。而“排除”则不同,如果一个请求被“排除”,webx将会立即放弃控制,将请求交还给服务器。接手控制的servlet或filter将无法访问webx一切的服务。

下图是WebxFrameworkFilter处理一个WEB请求的过程。

WebxFrameworkFilter处理请求的详细过程

图 3.8. WebxFrameworkFilter处理请求的详细过程

如图所示,WebxFrameworkFilter接到请求以后,就会调用WebxRootController。从这里开始,进入Spring的世界 —— 此后所有的对象:WebxRootControllerWebxControllerRequestContextPipeline等,全部是通过SpringExt配置在Spring Context中的。

WebxRootController对象存在于root context中,它被所有子应用所共享。它会创建RequestContext实例 —— 从而增强request、response、session的功能。接下来,WebxController对象会被调用。

WebxController对象是由每个子应用独享的,子应用app1app2可以有不同的WebxController实现。默认的实现,会调用pipeline。

Pipeline也是由各子应用自己来配置的。假如pipeline碰到无法处理的请求,如静态页面、图片等,pipeline应当执行<exit/> valve强制退出。然后WebxRootController就会“放弃控制” —— 这意味着request将被返还给/WEB-INF/web.xml中定义的servlet、filter或者返还给servlet engine本身来处理。

3.3. 定制Webx Framework

3.3.1. 定制WebxRootController

WebxRootController是被所有子应用所共享的逻辑。 假如你想创建一种新的WEB框架,可以自己定义一个新的WebxRootController的实现。这个方案非常适合作为一个新Web框架的起点。

例 3.10. 自定义WebxRootController

<webx-configuration xmlns="http://www.alibaba.com/schema/services">
    <components>
        <rootController class="com.myframework.MyRootController" /> 
    </components>
</webx-configuration>

创建自己的WebxRootController。最简便的方法是:扩展AbstractWebxRootController,免去了创建Servlet/Filter、初始化Spring容器、处理request、response等繁杂事务,并且完全支持SpringExt的所有功能,此外还包含了错误处理、开发模式等Webx Framework中的一切便利。。

3.3.2. 定制WebxController

WebxController是用来控制子应用的。每个子应用可以拥有不同的WebxController实现。

Webx Framework默认的WebxController是调用pipeline。假如你不想用pipeline,而希望实现自己的针对子应用的逻辑,那么最简单的方法就是实现自己的WebxController或者扩展AbstractWebxController

例 3.11. 自定义WebxController

<webx-configuration xmlns="http://www.alibaba.com/schema/services">
    <components defaultControllerClass="com.myframework.MyController"> 
        <component name="app1">
            <controller class="com.myframework.MyController" /> 
        </component>
    </components>
</webx-configuration>

指定默认的WebxController实现类。

对特定子应用明确指定WebxController实现类。

3.4. 本章总结

Webx Framework提供了一个可剪裁、可扩展的处理WEB请求基本框架。它所提供的基本功能事实上是每个WEB框架都需要用到的。Webx Framework为进一步实现WEB框架提供了坚实的基础。

第 4 章 Webx Turbine

Webx是一套基于Java Servlet API的通用Web框架。整个Webx框架分成三个层次,本章将简单介绍其第三个层次:Webx Turbine。Webx Turbine建立在Webx Framework的基础上,实现了页面渲染、布局、数据验证、数据提交等一系列工作。

Webx Turbine之所以叫这个名字,是因为Webx最早的版本,是从Apache Turbine项目上发展而来的。到现在,Turbine的代码已经荡然无存,然而Turbine中的一些风格和想法依赖保存在Webx框架中。

4.1. 设计理念

Webx Turbine所遵循下面的设计理念包括:

  • 页面驱动

  • 约定胜于配置

4.1.1. 页面驱动

创建一个WEB应用,一般会经历三个阶段:产品设计、用户界面设计、功能实现。分别由产品设计师、用户界面设计师和程序员协作完成。如下图所示。

协作图:创建一个WEB应用

图 4.1. 协作图:创建一个WEB应用

通常,界面设计师只完成纯静态页面的设计,需要由程序员来把静态页面转换、分解成模板,才能在最终的WEB应用中被使用。为什么不让界面设计师直接创建模板呢?这样一定可以提高很多效率。然而在一般的WEB框架中,由于模板不能独立于程序元素(如action)而存在,因此在程序员介入以前,界面设计师是没有办法展示模板的效果的。

Webx Turbine推崇页面驱动的理念。它的意思是,在程序员介入以前,让界面设计师可以直接创建模板,并展示模板的效果。页面驱动的反面,是程序驱动,或者是Action驱动 —— 这是多数WEB框架的模式。

页面驱动不止提高了开发的效率,也使界面设计师在早期阶段,就可以利用框架所提供的工具,做一些以前做不到的事,例如:页面跳转、简单的表单验证、字符串操作等。这些工具是通过Webx Turbine中的一个服务来完成的:pull tools。Pull tools服务预先准备了很多模板中可用的工具,让模板可以“按需”取得这些对象 —— 这就是pull这个单词的意思。

4.1.2. 约定胜于配置

Webx Turbine的另一个理念,是约定胜于配置。“约定”即规则。规则是预先定义的,工程师只需要按着规则来做事,就不需要额外的“配置”。对比其它一些框架 —— 往往每增加一个页面,都需要在配置文件中增加若干行内容。

Webx Turbine的规则主要是指一系列映射规则。

表 4.1. Webx Turbine映射规则

映射规则 说明
将URL映射成target target是一个抽象的概念,指明当前请求要完成的任务。Target由pipeline来解释,它可能被解释成模板名,也可能被解释成别的东西。
将target转换成模板名 模板用来展现页面的内容。Velocity、Freemarker、JSP都可以作为模板的格式,但在Webx建议使用velocity模板。
将target转换成layout布局 你可以为一组页面选择相同的布局(菜单、导航栏、版权信息等),为另一组页面选择另一种布局。
将target转换成module在Webx Turbine中,module是指screen、action、control等,大致相当于其它框架中的action或者controller。

工程师只需要根据上述规则,将模板放在指定的目录、按照预定的方式命名module(也就是screen、action、control等),就不再需要额外的配置。

4.2. 页面布局

Webx Turbine的页面,由以下几个部分组成:

Webx Turbine页面的构成

图 4.2. Webx Turbine页面的构成

其中:

  • Screen,代表页面的主体。

  • Layout,代表页面的布局。

  • Control,代表嵌在screen和layout中的页面片段。

4.3. 处理页面的基本流程

Webx Turbine的处理流程被定义在pipeline中。Webx Framework没有规定Pipeline的内容,但Webx Turbine却定义了一系列valves。下面是一个Webx Turbine推荐的pipeline配置:

例 4.1. Webx Turbine推荐的pipeline配置 - pipeline.xml

<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">

    <!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。 -->
    <prepareForTurbine />

    <!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 -->
    <setLoggingContext />

    <!-- 分析URL,取得target。 -->
    <analyzeURL homepage="homepage" />

    <!-- 检查csrf token,防止csrf攻击和重复提交。假如request和session中的token不匹配,则出错,或显示expired页面。 -->
    <checkCsrfToken />

    <loop>
        <choose>
            <when>
                <!-- 执行带模板的screen,默认有layout。 -->
                <pl-conditions:target-extension-condition extension="null, vm, jsp" />
                <performAction />
                <performTemplateScreen />
                <renderTemplate />
            </when>
            <when>
                <!-- 执行不带模板的screen,默认无layout。 -->
                <pl-conditions:target-extension-condition extension="do" />
                <performAction />
                <performScreen />
            </when>
            <otherwise>
                <!-- 将控制交还给servlet engine。 -->
                <exit />
            </otherwise>
        </choose>

        <!-- 假如rundata.setRedirectTarget()被设置,则循环,否则退出循环。 -->
        <breakUnlessTargetRedirected />
    </loop>

</services:pipeline>

假设用户以URL:http://localhost:8081/来访问Webx应用。域名和端口不重要,取决于应用服务器的配置,这里假设为localhost:8081。Webx Framework的处理流程,从WebxFrameworkFilter接收请求,并且一路顺利到达pipeline。然后Pipeline开始依次执行它的valves。(下面的描述略过一些相对次要的步骤。)

  1. <analyzeURL> - 分析URL

    分析URL的目的是取得target。由于用户访问的URL中并没有提供path信息,通常被理解为:用户想要访问“主页”。AnalyzeURL valve提供了一个可选的参数“homepage”,即是在这种情况下起作用 —— http://localhost:8081/对应的target为“homepage”。

    需要注意的是,target不代表模板名,也不代表类名。Target只是一个抽象的概念 —— 当前页面需要达成的目标。Target可能被后续的valves解释成模板名、类名或者其它东西。

  2. 进入<choose> - 多重分支

    很明显,“homepage”满足了第一个<when>所附带的条件:<target-extension-condition extension="null, vm, jsp">,意思是target的后缀不存在(null)或为“jsp”或为“vm”。

  3. <performAction> - 执行action

    和其它框架中的action概念不同,在Webx Turbine中,action是用来处理用户提交的表单的。

    因为本次请求未提供action参数,所以跳过该步骤。

  4. <performTemplateScreen> - 查找并执行screen。

    这里要用到一个规则:target映射成screen module类名的规则。

    假设target为xxx/yyy/zzz,那么Webx Turbine会依次查找下面的screen模块:

    • screen.xxx.yyy.Zzz

    • screen.xxx.yyy.Default

    • screen.xxx.Default

    • screen.Default

    本次请求的target为homepage,因此它会尝试查找screen.Homepagescreen.Default这两个类。

    如果找到screen类,Webx Turbine就会执行它。Screen类的功能,通常是读取数据库,然后把模板所需要的对象放到context中。

    如果找不到,也没关系 —— 这就是“页面优先”:像homepage这样的主页,通常没有业务逻辑,因此不需要screen类,只需要有模板就可以了。

  5. <renderTemplate> - 渲染模板

    这里用到两个规则:target映射成screen template,以及target映射成layout template。

    假设target为xxx/yyy/zzz,那么Webx Turbine会查找下面的screen模板:/templates/screen/xxx/yyy/zzz。Screen模板如果未找到,就会报404 Not Found错误。 找到screen模板以后,Webx Turbine还会试着查找下面的layout模板:

    • /templates/layout/xxx/yyy/zzz

    • /templates/layout/xxx/yyy/default

    • /templates/layout/xxx/default

    • /templates/layout/default

    Layout模板如果找不到,就直接渲染screen模板;如果存在,则把渲染screen模板后的结果,嵌入到layout模板中。

    Layout模板和screen模板中,都可以调用control。每个页面只有一个screen,却可以有任意多个controls。

  6. <breakUnlessTargetRedirected> - 内部重定向

    在screen和action中,可以进行“内部重定向”。内部重定向实质上就是由<breakUnlessTargetRedirected>实施的 —— 如果没有重定向标记,就退出;否则循环到<loop>标签。

    和外部重定向不同,外部重定向是向浏览器返回一个302303 response,其中包含Location header,浏览器看到这样的response以后,就会发出第二个请求。而内部重定向发生在pipeline内部,浏览器并不了解内部重定向。

4.4. 依赖注入

4.4.1. Spring原生注入手段

依赖注入是Spring的重要特性,Webx既然建立在Spring基础上,当然支持Spring原有的依赖注入手段,例如,你可以在Screen/control/action module类中这样写:

例 4.2. 通过@Autowired annotation注入

public class LoginAction {
    @Autowired
    private UserManager userManager; 
    ...
}

UserManager是在spring context中配置的bean。

在使用Spring原生注入手段时,需要注意beans的scope。你只能注入相同scope或较大的scope中的bean。例如,screen/action/control的scope为singleton,因此用@Autowired注入时,只能注入singleton的对象,不能注入诸如request、session等较小的scope对象。

4.4.2. 注入request、response和session对象

在Webx Framework中,你可以这样做:

例 4.3. 注入request、response和session对象

public class LoginAction {
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private HttpSession session;
    ...
}

前面我们刚讲过,你不能把request scope的对象,注入到singleton scope的对象中。但在Webx中,你可以将HttpServletRequestHttpServletResponseHttpSession对象注入到singleton对象中。为什么呢?原来,<request-contexts>对这几个常用对象进行了特殊处理,将它们转化成了singleton对象。

4.4.3. 参数注入

有一些对象,是无法通过Spring的bean来注入的,例如:用户提交的参数、表单等。好在Webx Turbine提供了一种可扩展的机制(DataResolver service),通过它,我们可以在screen/control/action的方法中注入任意对象。

表 4.2. 参数注入

功能 代码示例 适用于module类型
注入一个query参数
void doGetInt(@Param("aaa") int i)
screen、 action、 control
将query参数注入bean properties
void doSetData(@Params MyData data) 
screen、 action、 control
注入框架对象
void doGetNavigator(Navigator nav)
screen、 action
void doGetContext(Context context) 
screen、 action、 control
void execute(ControlParameters params) 
control
注入context和control参数
void execute(@ContextValue("myvalue") int value) 
screen、 action、 control
注入表单对象
void doGetGroup(@FormGroup("myGroup1") Group group) 
action
void doGetGroups(@FormGroups("myGroup1") Group[] groups) 
action
将表单值注入bean properties
void doGetGroupsBeans(@FormGroups("myGroup1") MyData[] data) 
action

4.5. 定制Webx Turbine

通过改进pipeline中的valves,我们很容易改变webx turbine的行为。

最常见的一种需求,是要对页面进行授权 —— 只有符合条件的用户才能访问相应的页面。在pipeline中,很容易添加这样的逻辑:

例 4.4. 改进pipeline,增加页面授权功能 - pipeline.xml

<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">
    <prepareForTurbine />
    <setLoggingContext />
    <analyzeURL homepage="homepage" />
    <checkCsrfToken />
    <valve class="com.mycompany.auth.PageAuthorizationValve" /> 
    ...
</services:pipeline>

插入用于验证权限的valve。

事实上,你甚至可以重写整个pipeline,以实现另一种风格的WEB框架。

4.6. 本章总结

Webx Turbine建立在pipeline的基础上,基于页面驱动和约定胜于配置的理念,定义了一组处理页面的流程。Webx Turbine的灵活性在于,你可以轻易定制pipeline,以改变它的任何一个方面。

部分 II. Webx基础设施服务

第 5 章 Resource Loading服务指南
5.1. 资源概述
5.1.1. 什么是资源? 5.1.2. 如何表示资源? 5.1.3. 如何访问资源? 5.1.4. 如何遍历资源? 5.1.5. 有什么问题?
5.2. Spring的ResourceLoader机制
5.2.1. Resource接口 5.2.2. ResourceLoaderResourcePatternResolver接口 5.2.3. 在代码中取得资源 5.2.4. Spring如何装载资源? 5.2.5. Spring ResourceLoader的缺点
5.3. Resource Loading服务
5.3.1. 替换Spring ResourceLoader 5.3.2. 定义新资源 5.3.3. 重命名资源 5.3.4. 重定向资源 5.3.5. 匹配资源 5.3.6. 在多个ResourceLoader中查找 5.3.7. 装载parent容器中的资源 5.3.8. 修改资源文件的内容 5.3.9. 直接使用ResourceLoadingService 5.3.10. 在非Web环境中使用Resource Loading服务
5.4. ResourceLoader参考
5.4.1. FileResourceLoader 5.4.2. WebappResourceLoader 5.4.3. ClasspathResourceLoader 5.4.4. SuperResourceLoader 5.4.5. 关于ResourceLoader的其它考虑
5.5. 本章总结
第 6 章 Filter、Request Contexts和Pipeline
6.1. Filter
6.1.1. Filter的用途 6.1.2. Filter工作原理 6.1.3. Filter的限制 6.1.4. Webx对filter功能的补充
6.2. Request Contexts服务
6.2.1. Request Contexts工作原理 6.2.2. Request Contexts的用途 6.2.3. Request Contexts的使用
6.3. Pipeline服务
6.3.1. Pipeline工作原理 6.3.2. Pipeline的用途 6.3.3. Pipeline的使用
6.4. 本章总结
第 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. 本章总结
第 8 章 Request Context之Session指南
8.1. Session概述
8.1.1. 什么是Session 8.1.2. Session数据存在哪? 8.1.3. 创建通用的session框架
8.2. Session框架
8.2.1. 最简配置 8.2.2. Session ID 8.2.3. Session的生命期 8.2.4. Session Store 8.2.5. Session Model 8.2.6. Session Interceptor
8.3. Cookie Store
8.3.1. 多值Cookie Store 8.3.2. 单值Cookie Store
8.4. 其它Session Store
8.4.1. Simple Memory Store
8.5. 本章总结

第 5 章 Resource Loading服务指南

Webx框架中,包含了一套用来查找和装载资源的服务 —— Resource Loading服务。

Resource Loading服务从Spring ResourceLoader机制中扩展而来,并且和Spring框架融为一体。因此,你不需要写特别的Java代码,就可以让所有利用Spring ResourceLoader机制的代码,直接享用Webx所提供的新的Resource Loading机制。

5.1. 资源概述

5.1.1. 什么是资源?

在一个稍具规模的应用程序中,经常要做的一件事,就是查找资源、读取资源的内容。这里所谓的“资源”,是指存放在某一介质中,可以被程序利用的文件、数据。例如,基于Java的WEB应用中,常用到下面的资源:

  • 配置文件:*.xml*.properties等。

  • Java类文件:*.class

  • JSP页面、Velocity模板文件:*.jsp*.vm等。

  • 图片、CSS、JavaScript文件:*.jpg*.css*.js等。

5.1.2. 如何表示资源?

在Java中,有多种形式可以表示一个资源:

表 5.1. 资源的表示

可表示资源的对象 说明
java.io.​File

可代表文件系统中的文件或目录。例如:

  • 文件系统中的文件:“c:\config.sys”。

  • 文件系统中的目录:“c:\windows\”。

java.net.​URL

统一资源定位符。例如:

  • 文件系统中的文件:c:\config.sys,可以表示成URL:“file:///c:/config.sys”。

  • 文件系统中的目录:c:\windows\,可以表示成URL:“file:///c:/windows/”。

  • 远程WEB服务器上的文件:“http://www.springframework.org/schema/beans.xml”。

  • Jar包中的某个文件,可以表示成URL:“jar:file:///c:/my.jar!/my/file.txt”。

java.io.​InputStream

输入流对象,可用来直接访问资源的内容。例如:

  • 文件系统中的文件:c:\config.sys,可以用下面的代码来转换成输入流:

    new FileInputStream("c:\\config.sys");
  • 远程WEB服务器上的文件,可以用下面的代码来转换成输入流:

    new URL("http://www.springframework.org/schema/beans.xml")​.openStream();
  • Jar包中的某个文件,可以用下面的代码来转换成输入流:

    new URL("jar:file:///c:/my.jar!/my/file.txt")​.openStream();

然而,并不是所有的资源,都可以表现成上述所有的形式。比如,

  • Windows文件系统中的目录,无法表现为输入流。

  • 而远程WEB服务器上的文件无法转换成File对象。

  • 多数资源都可以表现成URL形式。但也有例外,例如,如果把数据库中的数据看作资源,那么一般来说这种资源无法表示成URL

5.1.3. 如何访问资源?

不同类型的资源,需要用不同的方法来访问。

访问CLASSPATH中的资源

将资源放在CLASSPATH是最简单的做法。我们只要把所需的资源文件打包到Jar文件中,或是在运行java时,用-classpath参数中指定的路径中。接下来我们就可以用下面的代码来访问这些资源:

例 5.1. 访问CLASSPATH中的资源

URL resourceURL = getClassLoader().getResource("java/lang/String.class"); // 取得URL
InputStream resourceContent = getClassLoader().getResourceAsStream("java/lang/String.class"); // 取得输入流
访问文件系统中的资源

下面的代码从文件资源中读取信息:

例 5.2. 访问文件系统中的资源

File resourceFile = new File("c:\\test.txt"); // 取得File
InputStream resourceContent = new FileInputStream(resourceFile); // 取得输入流
访问Web应用中的资源

Web应用既可以打包成war文件,也可以展开到任意目录中。因此Web应用中的资源(JSP、模板、图片、Java类、配置文件)不总是可以用文件的方式存取。虽然Servlet API提供了ServletContext.getRealPath()方法,用来取得某个资源的实际文件路径,但该方法很可能返回null —— 这取决于应用服务器的实现,以及Web应用的部署方式。更好的获取WEB应用资源的方法如下:

例 5.3. 访问Web应用中的资源

URL resourceURL = servletContext.getResource("/WEB-INF/web.xml"); // 取得URL
InputStream resourceContent = servletContext.getResourceAsStream("/WEB-INF/web.xml"); // 取得输入流
访问Jar/Zip文件中的资源

下面的代码读取被打包在Jar文件中的资源信息:

例 5.4. 访问Jar/Zip文件中的资源

URL jarURL = new File(System.getProperty("java.home") + "/lib/rt.jar").toURI().toURL();
URL resourceURL = new URL("jar:" + jarURL + "!/java/lang/String.class"); // 取得URL
InputStream resourceContent = resourceURL.openStream(); // 取得输入流
访问其它资源

还可以想到一些访问资源的方法,例如从数据库中取得资源数据。

5.1.4. 如何遍历资源?

有时候,我们不知道资源的路径,但希望能找出所有符合条件的资源,这个操作叫作遍历。例如,找出所有符合pattern “/WEB-INF/webx-*.xml”的配置文件。

遍历文件系统

例 5.5. 遍历文件系统

File parentResource = new File("c:\\windows");
File[] subResources = parentResource.listFiles();
遍历WEB应用中的资源

例 5.6. 遍历WEB应用中的资源

Set<String> subResources = servletContext.getResourcePaths("/WEB-INF/");
遍历Jar/zip文件中的资源

例 5.7. 遍历Jar/zip文件中的资源

File jar = new File("myfile.jar");
ZipInputStream zis = new ZipInputStream(new FileInputStream(jar));

try {
    for (ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry()) {
        // visit entry
    }
} finally {
    zis.close();
}

并非所有类型的资源都支持遍历操作。通常遍历操作会涉及比较复杂的递归算法。

5.1.5. 有什么问题?

应用程序访问资源时,有什么问题呢?

首先,资源表现形式的多样性,给应用程序的接口设计带来一点麻烦。假如,我写一个ConfigReader类,用来读各种配置文件。那么我可能需要在接口中列出所有的资源的形式:

例 5.8. 用来读取配置文件的接口

public interface ConfigReader {
    Object readConfig(File configFile);
    Object readConfig(URL configURL);
    Object readConfig(InputStream configStream);
}

特别是当一个通用的框架,如Spring和Webx,需要在对象之间传递各种形式的资源的时候,这种多样性将导致很大的编程困难。

其次,有这么多种查找资源和遍历资源的方法,使我们的应用程序和资源所在的环境高度耦合。这种耦合会妨碍代码的可移植性和可测试性。

比如,我希望在非WEB的环境下测试一个模块,但这个模块因为要存取Web应用下的资源,而引用了ServletContext对象。在测试环境中并不存在ServletContext而导致该模块难以被测试。再比如,我希望测试的一个模块,引用了classpath下的某个配置文件(这也是一种耦合)。而我希望用另一个专为该测试打造的配置文件来代替这个文件。由于原配置文件是在classpath中,因此是难以替换的。

对于不打算重用的应用程序来说,这个问题还不太严重:大不了我预先设定好,就从这个地方,以固定的方式存取资源。然而就算这样,也是挺麻烦的。有的人喜欢把资源放在某个子目录下,有的人喜欢把资源放在CLASSPATH下,又有人总是通过ServletContext来存取Web应用下的资源。当你要把这些不同人写的模块整合起来时,你会发现很难管理。

一种可能发生的情形是,因为某些原因,环境发生改变,导致资源的位置、存取方式不得不跟着改变。比如将老系统升级为新系统。但一些不得不继续使用的老代码,由于引用了旧环境的资源而不能工作 —— 除非你去修改这些代码。有时修改老代码是很危险的,可能导致不可预知的错误。又比如,由于存储硬件的改变或管理的需要,我们需要将部分资源移到另一个地方(我们曾经将Web页面模板中的某个子目录,移动到一个新的地方,因为这些模板必须由新的CMS系统自动生成)。想要不影响现有代码来完成这些事,是很困难的。

5.2. Spring的ResourceLoader机制

Spring内置了一套ResourceLoader机制,很好地解决了访问资源的大部分问题。

5.2.1. Resource接口

Spring将所有形式的资源表现概括成一个Resource接口。如下所示(下面的接口定义是被简化的,有意省略了一些东西,以便突出重点):

例 5.9. Spring的Resource接口(简化)

public interface Resource {
    InputStream getInputStream();
    URL getURL();
    File getFile();
    boolean exists();
}

Resource接口向应用程序屏蔽了资源表现形式的多样性。于是,前面例子中的ConfigReader就可以被简化成下面的样子:

例 5.10. 用来读取配置文件的接口(简化后)

public interface ConfigReader {
    Object readConfig(Resource configResource);
}

事实上,Spring正是利用Resource接口来初始化它的ApplicationContext的:

例 5.11. Spring用Resource接口来代表用来初始化ApplicationContext的配置文件

public abstract class AbstractXmlApplicationContext extends ... {
    ...
    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) {
        Resource[] configResources = getConfigResources();
        ...
    }

    protected Resource[] getConfigResources();
}

5.2.2. ResourceLoaderResourcePatternResolver接口

Spring不仅可以通过ResourceLoader接口来取得单一的资源对象,还可以通过ResourcePatternResolver遍历并取得多个符合指定pattern的资源对象。这个设计向应用程序屏蔽了查找和遍历资源的复杂性。

ResourceLoader和ResourcePatternResolver接口

图 5.1. ResourceLoaderResourcePatternResolver接口

5.2.3. 在代码中取得资源

5.2.3.1. 通过ResourceLoader取得资源

例 5.12. 通过ResourceLoader取得资源

public class MyBean implements ResourceLoaderAware  {
    private ResourceLoader loader;

    public void setResourceLoader(ResourceLoader loader)  {
        this.loader = loader;
    }

    public void func() {
        Resource resource = loader.getResource("myFile.xml"); 
        ...
    }
}

 

实现了ResourceLoaderAware接口。要取得资源,必须要拿到ResourceLoader对象。而通过ResourceLoaderAware接口拿到ResourceLoader是最简单的方法。

调用所取得的ResourceLoader来取得资源。

5.2.3.2. 直接注入资源

另一种更简便的方法是,将资源直接“注入”到bean中 —— 你不需要手工调用ResourceLoader来取得资源的方式来设置资源。例如:

例 5.13. 直接注入资源

public class MyBean {
    private URL resource;

    public void setLocation(URL resource)  {
        this.resource = resource;
    }

    ……
}

Spring配置文件可以这样写:

<bean id="myBean" class="MyBean">
    <property name="location" value="myFile.xml" />
</bean>

此处注入资源的URL

这样,Spring就会把适当的myFile.xml所对应的资源注入到myBean对象中。此外,Spring会自动把Resource对象转换成URLFile等普通对象。在上面的例子中,MyBean并不依赖于Resource接口,只依赖于URL类。

将代码稍作修改,就可以注入一组资源:

例 5.14. 注入一组资源

    public void setLocations(URL[] resources)  {
        this.resources = resources;
    }

配置文件:

    <property name="locations" value="WEB-INF/webx-*.xml" />

此处注入资源的URL的数组。

上例中,可以直接得到所有符合pattern “WEB-INF/webx-*.xml”的配置文件。显然这是通过ResourcePatternResolver取得的。

5.2.4. Spring如何装载资源?

Spring是如何装载资源文件的呢?Spring装载资源的方案是由ApplicationContext决定的。不同的ApplicationContext类,实现了不同的资源装载方案。

Spring ApplicationContext实现了资源装载的具体方案

图 5.2. Spring ApplicationContext实现了资源装载的具体方案

5.2.4.1. ClassPathXmlApplicationContext

ClassPathXmlApplicationContext支持从classpath中装载资源。

例 5.15. ClassPathXmlApplicationContext - 从classpath中装载资源

假如我以下面的方式启动Spring,那么系统将支持从classpath中装载资源。

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

ClassPathXmlApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

URL resource = getClassLoader().getResource("myFile.xml");

5.2.4.2. FileSystemXmlApplicationContext

FileSystemXmlApplicationContext支持从文件系统中装载资源。

例 5.16. FileSystemXmlApplicationContext - 从文件系统中装载资源

假如我以下面的方式启动Spring,那么系统将支持从文件系统中装载资源。

ApplicationContext context = new FileSystemXmlApplicationContext("beans.xml");

FileSystemXmlApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

File resource = new File("myFile.xml");

5.2.4.3. XmlWebApplicationContext

XmlWebApplicationContext支持从webapp上下文中(也就是ServletContext对象中)装载资源。

例 5.17. XmlWebApplicationContext - 从Web应用的根目录中装载资源

假如我以下面的方式启动Spring,那么系统将支持从Web应用的根目录中装载资源。

ApplicationContext context = new XmlWebApplicationContext();

context.setConfigLocation("/WEB-INF/beans.xml");
context.setServletContext(servletContext);
context.refresh();

也可以让ContextLoaderListener来创建XmlWebApplicationContext,只需要在/WEB-INF/web.xml中添加如下配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/beans.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

XmlWebApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

URL resource = servletContext.getResource("myFile.xml");

5.2.4.4. ClasspathClasspath*前缀

除了用ClassPathXmlApplicationContext以外,事实上所有的Spring ApplicationContext实现也都支持装载classpath中的资源。可以用下面两种方法:

表 5.2. Spring ApplicationContext装载classpath资源的方法

方法 说明
使用classpath:前缀 例如:“classpath:myFile.xml” —— 在classpath中装载资源myFile.xml
使用classpath*:前缀例如:“classpath*:/META-INF/my*.xml” —— 在classpath中装载所有符合pattern的资源。

5.2.5. Spring ResourceLoader的缺点

鱼和熊掌不可得兼

Spring ResourceLoader是由ApplicationContext来实现的。而你一次只能选择一种ApplicationContext的实现 —— 如果你选择了XmlWebApplicationContext,你就放弃了FileSystemXmlApplicationContext;反之亦然。

在WEB应用中,由于Spring使用了XmlWebApplicationContext,因此你就无法装载文件系统下的资源。

不透明性

你必须用“绝对路径”来引用Spring中的资源。

假如你使用FileSystemXmlApplicationContext来访问资源,你必须使用绝对路径来访问文件或目录资源。这妨碍了应用程序在不同系统中部署的自由。因为在不同的系统中,例如Windows和Linux,文件的绝对路径是不同的。为了系统管理的需要,有时也需要将文件或目录放在不同于开发环境的地方。

即便是访问WEB应用下的资源,或者是classpath下的资源,你也必须明确指出它们的位置,例如:WEB-INF/myFile.xmlclasspath:myFile.xml等。如果我希望把classpath:myFile.xml挪到另一个物理位置,就必须修改所有的引用。

无扩展性

我无法在Spring ResourceLoader机制中增加一种新的装载资源的方法。例如,我希望把资源文件保存在数据库中,并用ResourceLoader来取得它。用Spring很难做到这点。

5.3. Resource Loading服务

5.3.1. 替换Spring ResourceLoader

Webx Resource Loading服务可作为Spring ResourceLoader机制的替代品(Drop-in Replacement,投入既替换):

  • 当你不使用它时,Spring原有的ResourceLoader功能不受影响;

  • 当你在spring配置文件中添加Resource Loading服务时,ResourceLoader即被切换到新的机制。新的机制可兼容原有的Spring配置和代码,但支持更多的资源装载方式,以及更多的功能,如资源重命名、资源重定向等。

你只需要在配置文件中增加以下内容,就可以将Spring ResourceLoader机制替换成Webx的Resource Loading服务:

例 5.18. Resource Loading服务的基本配置(/WEB-INF/webx.xml

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">

    <resource-alias pattern="/" name="/webroot" /> 

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader /> 
    </resource>
    <resource pattern="/classpath" internal="true"> 
        <res-loaders:classpath-loader /> 
    </resource>

</ resource-loading>

关于这段配置的具体含义,请参见本章其它小节:

请参见:第 5.3.3 节 “重命名资源”

 

请参见:第 5.3.2 节 “定义新资源”

请参见:第 5.4.2 节 “WebappResourceLoader

请参见:第 5.4.3 节 “ClasspathResourceLoader

这段配置使得Resource Loading服务的行为和原来的Spring ResourceLoader完全兼容:

  • 仍然支持classpath:classpath*:前缀所定义的资源。

  • 如不加前缀,则代表访问WEB应用根目录下的文件。例如:

    • /myFile.xml代表着Web应用根目录下的/myFile.xml

    • /WEB-INF/myFile.xml代表着Web应用根目录下的/WEB-INF/myFile.xml

加上这段配置以后,虽然功能和原来相比并没有变化,然而它已经准备好向系统中添加新的资源装载的功能了。

5.3.2. 定义新资源

定义一种新资源,需要回答两个问题:

  1. 资源的名称是什么?

  2. 资源在哪里(或如何装载资源)?

下面的例子定义了一种新的资源,它的名称是“/jdk/*”,通过“file-loader”从文件系统${java.home}文件夹中装载。

例 5.19. 定义新资源:/jdk/*

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource pattern="/jdk"> 
        <res-loaders:file-loader basedir="${java.home}" /> 
    </resource>

</resource-loading>

定义新资源,资源名以/jdk为前缀。

<file-loader>表示从文件系统中装载资源。详见:第 5.4.1 节 “FileResourceLoader

${java.home}是Java提供的system property,它的值指向当前Java运行环境的根目录。

前文讲过,Spring可以直接把资源注入到对象中。使用Resource Loading服务以后,你仍然可以这样做。下面的配置把JDK目录下的tools.jar文件(如果存在的话)的URL注入到myBean中:

例 5.20. 注入JAVA_HOME/lib/tools.jar

<bean id="myBean" class="MyBean">
    <property name="location" value="/jdk/lib/tools.jar" />
</bean>

5.3.3. 重命名资源

重命名资源是指对于即有的资源,改变其名字。

为什么需要修改资源的名字?理由是:取消资源名称和环境的关联性。有一些资源的名称,具有明显的环境相关性,比如:

  • classpath:myFile.xml或者/classpath/myFile.xml —— 从资源的名称就可以看出,这些资源是从classpath中装载的。

  • /WEB-INF/myFile.xml或者/webroot/WEB-INF/myFile.xml —— 从资源的名称可以看出,这些资源是从web应用中装载的。

使用和环境相关的资源名称有什么问题?问题就是,当环境改变时,应用代码会受到影响。最常见的一种状况是:单元测试时,用于测试的资源文件往往被放在专供测试的目录中,这些目录和应用运行时的环境是不同的 —— 你可能希望将classpath:myFile.xml/WEB-INF/myFile.xml改成/src/test/config/myFile.xml

对资源重命名就可以解决这类问题:

  • classpath:myFile.xml或者/WEB-INF/myFile.xml重命名成:myapp/conf/myFile.xml

  • 在测试环境中,将myapp/conf/myFile.xml名称指向另一个物理地址src/test/config/myFile.xml

重命名资源是通过alias别名实现的:

例 5.21. 重命名资源

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource-alias pattern="/myapp/conf" name="/webroot/WEB-INF" /> 

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader /> 
    </resource>

</resource-loading>

定义了一个资源的别名:/myapp/conf

当你查找/myapp/conf/myFile.xml时,Resource Loading服务实际上会去找/webroot/WEB-INF/myFile.xml。而/webroot/*则是由  所定义的资源。

定义以/webroot为前缀的新资源。

其中,attribute internal=true是一个可选项,当它的值为true时,代表它所修饰的资源是不能被外界所直接访问的。例如,你想直接在myBean中注入/webroot/WEB-INF/myFile.xml是不行的。把internal选项设成true,可以让强制用户转向新的资源名称。Internal参数的默认值为false,意味着,新旧两种名称同时可用。

<webapp-loader>表示从Web应用中装载资源。详见:第 5.4.2 节 “WebappResourceLoader

5.3.4. 重定向资源

重定向资源的意思是,将部分资源名称,指向另外的地址。

一个常见的需求是这样的:通常我们会把页面模板保存在WEB应用的/templates目录下。但是有一批模板是由外部的CMS系统生成的,这些模板文件不可能和WEB应用打包在一起,而是存放在某个外部的目录下的。我们希望用/templates/cms来引用这些模板。

由于/templates/cms只不过是/templates的子目录,所以如果没有Resource Loading服务所提供的重定向功能,是不可能实现上述功能的。用Resource Loading服务重定向的配置如下:

例 5.22. 重定向资源

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource-alias pattern="/templates" name="/webroot/templates" /> 

    <resource pattern="/templates/cms"> 
        <res-loaders:file-loader basedir="${cms_root}" />
    </resource>

    <resource pattern="/webroot" internal="true">
        <res-loaders:webapp-loader />
    </resource>

    ...
</resource-loading>

定义了一个资源的别名:/templates,指向internal资源:/webroot/templates

/templates的子目录/templates/cms重定向到某个外部的文件目录${cms_root}中。

其中cms_root是启动服务器时所指定的system property(-Dcms_root=...)或者spring所定义的placeholder。

通过上述配置,可以达到如下效果:

表 5.3. 访问/templates目录下的资源

资源名 如何装载?
/templates/xxx.vm 不受重定向影响。访问/webroot/templates/xxx.vm,继而通过webapp-loader访问Web应用根目录下的/templates/xxx.vm文件。
/templates/cms/yyy.vm 被重定向。通过file-loader访问${cms_root}目录下的文件:${cms_root}/yyy.vm
/templates/subdir/zzz.vm不受重定向影响。访问/webroot/templates/subdir/zzz.vm,继而通过webapp-loader访问Web应用根目录下的/templates/subdir/zzz.vm文件。

最重要的是,访问/templates目录的应用程序并不知道这个资源重定向的存在,当cms所对应的实际目录被改变时,应用程序也不会受到任何影响 —— 这个正是Resource Loading服务的“魔法”。

5.3.5. 匹配资源

无论是定义新资源(<resource>)或是重命名资源(资源别名、<resource-alias>),都需要指定一个pattern attribute来匹配资源的名称。

5.3.5.1. 匹配绝对路径和相对路径

资源或资源别名的pattern支持对绝对路径和相对路径的匹配:

表 5.4. 资源或别名的pattern格式

pattern类型 格式 说明
匹配绝对路径

/absolute/path

/开头的pattern代表一个绝对路径的匹配。

例如:pattern="/absolute/path"可以匹配资源名/abslute/path/xxx/yyy,但不能匹配资源名/xxx/abslute/path/yyy

匹配相对路径

relative/path

不以/开头的pattern代表一个相对路径的匹配。

例如:pattern="relative/path"可以匹配资源名/relative/path/xxx/yyy,也可以匹配资源名/xxx/relative/path/yyy

5.3.5.2. 匹配通配符

表 5.5. 通配符格式

格式 说明
星号 * 匹配0-n个字符,但不包括“/”。即,“*”只匹配一级目录或文件中的零个或多个字符。
双星号 ** 匹配0-n个字符,包括“/”。即,“**”匹配多级目录或文件。
问号 ?匹配0-1个字符,但不包括“/”。即,“?”匹配一级目录或文件中的零个或一个字符。

所有被通配符匹配的内容,将被按顺序赋给变量“$1”、“$2”、“$3”、“$4”、……。这些变量可以在其它地方被引用。

通配符匹配的名称既可以是绝对路径,也可以是相对路径。把相对路径和通配符结合起来的最常见用法,就是匹配文件名后缀,例如:pattern="*.xml"

下面是一些使用通配符的例子:

例 5.23. 用通配符来匹配资源名称或资源别名

重命名WEB-INF及其子目录下的所有的xml文件

例如,将/myapp/conf/my/file.xml转换成/webroot/WEB-INF/my/file.xml

<resource-alias pattern="/myapp/conf/**/*.xml" name="/webroot/WEB-INF/$1/$2.xml" />
修改文件名后缀

例如,将/myapp/conf/myfile.conf转换成/webroot/WEB-INF/myfile.xml

<resource-alias pattern="/myapp/conf/*.conf" name="/WEB-INF/$1.xml"/>
按首字母划分子目录

a开头的文件名放到a子目录下,b开头的文件名放到b子目录下,以此类推。

例如,将/profiles/myname转换成文件路径${profile_root}/m/myname;将/profiles/othername转换成文件路径${profile_root}/o/othername

<resource pattern="/profiles/?*">
    <res-loaders:file-loader basedir="${profile_root}">
        <res-loaders:path>$1/$1$2</res-loaders:path>
    </res-loaders:file-loader>
</resource>

5.3.6. 在多个ResourceLoader中查找

假如,在我的Web应用中,我有一些配置文件放在/WEB-INF目录中,另外一部分配置放在classpath中。我可以这样做:

例 5.24. 在多个ResourceLoader中查找

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource pattern="/myapp/conf">
        <res-loaders:super-loader name="/webroot/WEB-INF" /> 
        <res-loaders:super-loader name="/classpath" /> 
    </resource>

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader />
    </resource>
    <resource pattern="/classpath" internal="true"> 
        <res-loaders:classpath-loader />
    </resource>

    ...
</resource-loading>

 

依次尝试两个loaders。

定义internal资源/webroot/*,从Web应用中装载资源。详见第 5.4.2 节 “WebappResourceLoader

定义internal资源/classpath/*,从classpath中装载资源。详见第 5.4.3 节 “ClasspathResourceLoader

Resource Loading服务根据上面的配置,会这样查找资源“/myapp/conf/myFile.xml”:

  1. 先查找:/webroot/WEB-INF/myFile.xml,如果找不到,

  2. 则再查找:/classpath/myFile.xml,如果找不到,则放弃。

在上例中,<super-loader>(详见第 5.4.4 节 “SuperResourceLoader)是一种特殊的ResourceLoader,它等同于<resource-alias>。下面的两种写法是完全等同的:

例 5.25. <super-loader>和等效的<resource-alias>

<resource pattern="/myapp/conf">
    <res-loaders:super-loader name="/webroot/WEB-INF" />
</resource>

<resource-alias pattern="/myapp/conf " name="/webroot/WEB-INF" />

但是用<resource-alias>没有办法实现上面所述的多重查找的功能。

5.3.7. 装载parent容器中的资源

在Webx中,Spring容器被安排成级联的结构。

Spring容器的级联结构

图 5.3. Spring容器的级联结构

如图所示,每个Spring容器都可以配置自己的Resource Loading服务。当调用子容器的Resource Loading服务时,遵循这样的逻辑:

  1. 先在子容器的Resource Loading服务中查找资源,如果找不到,

  2. 则再到parent容器的Resource Loading服务中查找,如果找不到,则放弃。

运用这种级联装载资源的方法,子应用可以把共享的资源定义在root context中,而把自己独享的资源定义在自己的容器当中。

前文所述的<super-loader>也支持级联装载资源。<super-loader>会先在当前容器的Resource Loading服务中查找,如果找不到,就到parent容器的Resource Loading服务中查找。利用<super-loader>,你甚至可以改变资源搜索的顺序。例如,你可以命令Resource Loading服务先查找parent容器中的Resource Loading服务,再查找当前容器中的ResourceLoaders:

例 5.26. 利用<super-loader>改变资源搜索的顺序

<resource pattern="...">
    <res-loaders:super-loader /> 
    <res-loaders:file-loader /> 
</resource>

先找parent容器中的Resource Loading服务。

再找当前容器中的ResourceLoaders。

5.3.8. 修改资源文件的内容

Resource Loading服务支持内容过滤 —— 你可以在获取资源以前读取甚至修改资源文件的内容。一种常见的情形是,将XML格式的资源文件用XSLT转换格式:

例 5.27. 将XML格式的资源文件用XSLT转换格式

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
        xmlns:res-filters="http://www.alibaba.com/schema/services/resource-loading/filters">
    ...

    <resource-filters pattern="test-*.xml">
        <res-filters:xslt-filter xslt="/stylesheet.for.test/test.xsl" saveTo="/tempdir" /> 
    </resource-filters>

    <resource pattern="/tempdir"> 
        <loaders:file-loader basedir="${project.home}/target/test" />
    </resource>

</resource-loading>

所有目录下(因为是相对路径)的名称为test-*.xml文件,用指定的XSL文件进行转换。

这里引进了一种新的扩展点:ResourceFilterResourceFilter可以在应用获取资源之前,取得控制,以便对资源做一点事。

<xslt-filter>是对ResourceFilter的扩展,它能够把XML资源用指定的xsl文件转换成新的格式。假如指定了saveTo参数,就可以把转换的结果保存下来,避免每次访问都重新转换。

此处定义tempdir目录资源,以便保存xslt转换的结果。

[注意]注意

<xslt-filter>的参数xslt所指向的xsl文件,以及参数saveTo所指向的目录,它们本身也是由Resource Loading服务装载的。

有哪些情况需要这种内容过滤的功能呢?

  • 单元测试 —— 我们可能需要对单元测试的资源文件进行特殊的转换。

  • 高速缓存 —— 有一些ResourceLoader可能会有性能的开销,例如:从数据库中装载资源。利用ResourceFilter功能,就可以把装载的资源缓存在高速cache中,以提高系统的性能。

5.3.9. 直接使用ResourceLoadingService

前面所讲的Resource Loading服务的用法,对应用程序而言,是完全透明的。也就是说,应用程序并不需要关心Resource Loading服务的存在,而是按照Spring ResourceLoader的老用法,就可以工作。

但是你也可以直接注入ResourceLoadingService对象,以取得更多的功能。

例 5.28. 注入ResourceLoadingService对象

public class MyClass {
    @Autowired
    private ResourceLoadingService resourceLoadingService;
}

下面列举了可通过ResourceLoadingService接口实现的功能。

取得资源

例 5.29. 通过ResourceLoadingService接口取得资源

Resource resource = resourceLoadingService.getResource("/myapp/conf/myFile.xml"); 
Resource resource = resourceLoadingService.getResource("/myapp/conf/myFile.xml",
                                                       ResourceLoadingService.FOR_CREATE); 

和Spring不同的是,如果你直接调用ResourceLoadingService取得资源,当资源文件不存在时,你会得到一个ResourceNotFoundException。而Spring无论如何都会取得Resource对象,但随后你需要调用Resource.exists()方法来判断资源存在于否。

ResourceLoadingService.getResource()方法还支持一个选项:FOR_CREATE。如果提供了这个选项,那么对于某些类型的资源(如文件系统的资源),即使文件或目录不存在,仍然会返回结果。这样,你就可以创建这个文件或目录 —— 这就是FOR_CREATE参数的意思。

取得特定类型的资源

例 5.30. 通过ResourceLoadingService接口取得特定类型的资源

// 取得资源文件
File file = resourceLoadingService.getResourceAsFile("/myapp/conf/myFile.xml");

// 取得资源URL
URL url = resourceLoadingService.getResourceAsURL("/myapp/conf/myFile.xml");

// 取得资源输入流
InputStream stream = resourceLoadingService.getResourceAsStream("/myapp/conf/myFile.xml");
判断资源存在于否

例 5.31. 通过ResourceLoadingService接口判断资源存在于否

if (resourceLoadingService.exists("/myapp/conf/myFile.xml")) {
    ...
}
列举子资源

例 5.32. 通过ResourceLoadingService接口列举子资源

String[] resourceNames = resourceLoadingService.list("/myapp/conf");
Resource[] resources = resourceLoadingService.listResources("/myapp/conf");

相当于列出当前目录下的所有子目录和文件。

不是所有的ResourceLoader都支持这个操作 —— FileResourceLoaderWebappResourceLoader支持列举子资源,ClasspathResourceLoader则不支持。

跟踪取得资源的过程

例 5.33. 通过ResourceLoadingService接口跟踪取得资源的过程

ResourceTrace trace = resourceLoadingService.trace("/myapp/conf/webx.xml");

for (ResourceTraceElement element : trace) {
    System.out.println(element);
}

这是用来方便调试的功能。有点像Throwable.getStackTrace()方法,可以得到每一个方法调用的历史记录 —— ResourceLoadingService.trace()方法可以将取得资源的步骤记录下来。上面代码会在console中输出类似下面的内容:

"/myapp/conf/webx.xml" matched [resource-alias pattern="/myapp/conf"], at "resources.xml", beanName="resourceLoadingService"
"/webroot/WEB-INF/webx.xml" matched [resource pattern="/webroot"], at "resources.xml", beanName="resourceLoadingService"
列出所有可用的资源定义和别名的pattern

例 5.34. 通过ResourceLoadingService接口列出所有可用的资源定义和别名的pattern

String[] patterns = resourceLoadingService.getPatterns(true);

5.3.10. 在非Web环境中使用Resource Loading服务

在非Web环境中使用的ResourceLoadingXmlApplicationContext

图 5.4. 在非Web环境中使用的ResourceLoadingXmlApplicationContext

在非Web环境中使用Resource Loading服务的最好方法,是创建ResourceLoadingXmlApplicationContext作为Spring容器。

例 5.35. 创建ResourceLoadingXmlApplicationContext容器

ApplicationContext context = new ResourceLoadingXmlApplicationContext(
                                                          new FileSystemResource("beans.xml"));

只要beans.xml中包含<resource-loading>的配置,就会自动启用Resource Loading服务,并取代Spring原来的ResourceLoader机制。

5.4. ResourceLoader参考

Resource Loading服务的核心是ResourceLoader。和Spring ResourceLoader不同,Resource Loading服务的ResourceLoader是可扩展的轻量级对象,担负着装载某一种类型的资源的具体任务。例如FileResourceLoader负责装载文件系统的资源;WebappResourceLoader负责装载WEB应用中的资源等等。

当你需要新的资源装载方式时,你所要做的,就是实现一种新的ResourceLoader。例如,你想从数据库中装载资源,那么就可以实现一个DatabaseResourceLoader

5.4.1. FileResourceLoader

FileResourceLoader的功能是:从文件系统中装载资源。

基本用法

例 5.36. FileResourceLoader的基本用法

<resource pattern="/my/virtual">
    <res-loaders:file-loader />
</resource>

这样,file-loader会从哪里装载资源呢?

答案是:从当前配置文件所在的目录中装载。假如上述资源配置所在的配置文件是c:/myapp/conf/resources.xml,那么file-loader就会从c:/myapp/conf/myFile.xml文件中装载/my/virtual/myFile.xml资源。

这样做的思路源自于Apache的一个项目:Ant。Ant是一个广为使用的build工具。每一个Ant项目,都有一个build.xml脚本,在里面定义了很多target,诸如编译项目、打包等。通常我们都会把build.xml这个文件放在项目的根目录中,然后build.xml中的命令全是使用相对于build.xml所在的项目根目录计算出来的相对路径。例如:

例 5.37. Ant脚本(build.xml

<project basedir=".">
    ...
    <target ...>
        <copy todir="bin">
            <fileset dir="src"/>
        </copy>
    </target>
    ...
</project>

在上面的Ant脚本中,binsrc目录全是相对于build.xml所在目录的相对目录。这样做的好处是,当你把项目移到不同的环境中,你也无需改变配置文件和脚本。

FileResourceLoader采用了和Ant完全类似的想法。

指定basedir

例 5.38. 在FileResourceLoader中指定basedir

<resource pattern="/my/virtual">
    <res-loaders:file-loader basedir="${my.basedir}" />
</resource>

FileResourceLoader当然也支持指定basedir根目录。这样,它就会从指定的basedir的子目录中查找资源。

一般来说,我们需要利用Spring Property Placeholder来设置basedir。在上面的例子中,我们可以在系统启动时,指定JVM参数:-Dmy.basedir=c:/mydata。在不同的系统环境中,必须指定正确的basedir,否则,<file-loader>有可能找不到资源。

搜索多个路径

例 5.39. 在FileResourceLoader中指定多个搜索路径

<resource pattern="/my/virtual">
    <res-loaders:file-loader basedir="...">
        <res-loaders:path>relativePathToBasedir</res-loaders:path> 
        <res-loaders:path type="absolute">c:/absolutePath</res-loaders:path> 
    </res-loaders:file-loader>
</resource>

搜索路径默认为相对路径,相对于指定的basedir。如果basedir未指定,则相对于当前resource-loading所在的配置文件的路径。

搜索路径也可以是绝对路径。

FileResourceLoader支持搜索多个路径,类似于操作系统在PATH环境变量所指定的路径中,搜索可执行文件;也类似于Java在CLASSPATH参数所指定的路径中,搜索classes。

5.4.2. WebappResourceLoader

WebappResourceLoader的功能是:从当前WEB应用中装载资源,也就是从ServletContext对象中装载资源。

例 5.40. 配置WebappResourceLoader

<resource pattern="/my/virtual">
    <res-loaders:webapp-loader />
</resource>

5.4.3. ClasspathResourceLoader

ClasspathResourceLoader的功能是:从classpath中装载资源,也就是从当前的ClassLoader对象中装载资源。

例 5.41. 配置ClasspathResourceLoader

<resource pattern="/my/virtual">
    <res-loaders:classpath-loader />
</resource>

5.4.4. SuperResourceLoader

SuperResourceLoader的功能是:调用Resource Loading服务来取得资源。它有点像Java里面的super操作符。

取得新名字所代表的资源

例 5.42. 用SuperResourceLoader取得新名字所代表的资源

<resource pattern="/my/virtual">
    <res-loaders:super-loader basedir="/webroot/WEB-INF" />
</resource>

这个操作类似于<resource-alias>

如果在当前context的Resource Loading服务中找不到资源,它会前往parent context中查找。

在parent context中查找资源

例 5.43. 用SuperResourceLoader查找parent context中的资源

<resource pattern="/my/virtual">
    <res-loaders:super-loader />
</resource>

如果你不指定name参数,那么SuperResourceLoader会直接去parent context中查找资源,而不会在当前context的Resource Loading服务中找。

5.4.5. 关于ResourceLoader的其它考虑

以上所有的ResourceLoader都被设计成可以在任何环境中工作,即使当前环境不适用,也不会报错。

WebappResourceLoader可以兼容非WEB环境

在非WEB环境中,例如单元测试环境、你直接通过XmlApplicationContext创建的Spring环境,WebappResourceLoader也不会出错 —— 只不过它找不到任何资源而已。

SuperResourceLoader可以工作于非级联的环境

也就是说,即使parent context不存在,或者parent context中没有配置Resource Loading服务,SuperResourceLoader也是可以工作的。

这样,同一套资源配置文件,可以被用于所有环境。

5.5. 本章总结

Resource Loading服务提供了一套高度可扩展的、强大的资源装载机制。这套机制和Spring ResourceLoader无缝连接。使用它并不需要特殊的技能,只要掌握Spring的风格即可。

第 6 章 Filter、Request Contexts和Pipeline

Filter是Servlet规范2.3版及更新版所支持的一种机制。和Servlet/JSP不同,Filter自己往往不会直接产生response,相反,它提供了一种“符加”的功能,可以作用在任何一个servlet、JSP以及其它filter之上。然而,在实际的应用中,我们发现filter有很多不足之处。

Webx框架提供了两种机制(Request Contexts和Pipeline)来作为filter机制的补充。在大多数情况下,它们都可以实现类似filter的功能,但比filter更容易扩展、更容易配置、也更轻量。Webx并没有打算完全替代filter,相反它还是可以和任何filter搭配使用。

本章先简略介绍filter的功能和不足,再向你介绍Request Contexts和Pipeline的工作原理,及使用方法。

6.1. Filter

6.1.1. Filter的用途

Filter这种机制常被用来实现下面的功能:

页面授权 根据登录用户的权限,阻止或许可用户访问特定的页面。
日志和审计 记录和检查用户访问WEB应用的情况。
图片转换 改变图片的格式、精度、尺寸等。
页面压缩 压缩页面内容,加快下载速度。
本地化 显示本地语言和风格的页面。
XSLT转换 对XML内容进行XSLT转换,使之适用于多种客户端。
高速缓存高速缓存页面,提高响应速度。

当然还有更多种的应用,我们不可能一一列举。

Filter的通用性很好。任何filter均独立于其它filter和servlet,因此它可以和任意其它filter和servlet组合搭配。下面是一段配置示例 ── 通过SetLoggingContextFilter,日志系统可以记录当前请求的信息,例如:URL、referrer URL、query string等。

例 6.1. Filter配置示例(/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/j2ee  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd
    ">
    <filter>
        <filter-name>mdc</filter-name>
        <filter-class>com.alibaba.citrus.webx.servlet.SetLoggingContextFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>mdc</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

6.1.2. Filter工作原理

Filter Chain

图 6.1. Filter Chain

如图所示。多个filter和至多一个servlet被串联成一个链,被称为Filter Chain。执行的时候,引擎将控制权交给链条中的头一个filter(如果有的话)。然后,就像击鼓传花一样,控制权被依次传递给filter chain中的下一个filter或servlet。每一个得到控制权的filter可以做下面的事:

  • 继续传递控制权或立即终止filter chain。

    • Filter可将控制权传递给链条中的下一个filter或者最终的servlet。

    • Filter也可以不将控制权传递给下一个filter或servlet,这样便中止了整个filter chain的执行。

  • 预处理。在传递控制权给下一个filter或servlet之前,filter可以预先做一些事情:

    • 设置request、response中的参数,例如:character encoding、content type等。

    • HttpServletRequestWrapper传递给链条中的下一位,filter可以通过wrapper改变request中的任意值。

    • HttpServletResponseWrapper传递给链条中的下一位,filter可以通过wrapper来拦截后续filter或servlet对response的修改。

  • 提交。在控制权从filter chain中返回以后,filter还可以做一些后续提交的操作。

    • 例如,将response中拦截而来的数据,压缩或转换格式,并发送给客户端或filter chain的上一级。

    • 通过trycatch还可以捕获filter chain下一级所有的异常,并做处理。

6.1.3. Filter的限制

Filter是很有用的。作为servlet的补充,filter也是很成功的。但是filter并没有被设计用来完成一切事情。事实上,filter的设计限制了filter的用途。每个filter具有下面的限制:

  • Filter可以访问和修改数据。但它只能访问和修改HttpServletRequestHttpServletResponseServletContext等容器级的对象,而不能(或很难)访问应用程序中的状态。所以filter无法实现和应用逻辑密切相关的功能。

  • Filter可以影响执行流程。但它不能改变filter chain的结构和顺序。Filter chain的结构和顺序是由web.xml中定义的。当filter得到控制权以后,它只能选择继续下去,或者立即结束,而没法进行循环、分支、条件判断等更复杂的控制。因此,filter只能用来实现粗粒度的流程控制功能(例如,当用户未获授权时,停止执行filter chain),难以应付更细致的应用程序内的控制需求。

  • Filter与其它filter和servlet之间,除了request和response对象以外,无法共享其它的状态。这既是优点又是缺点。优点是使filter更独立、更通用;缺点是filter与其它filter、servlet之间难以协作,有时甚至会引起无谓的性能损失。

6.1.4. Webx对filter功能的补充

综上所述,一个filter常常做的两件事是:

  • 改变request/response对象(通过HttpServletRequestWrapperHttpServletResponseWrapper);

  • 改变应用执行的流程。

其实,大部分filter只做其中一件事。例如:

  • 页面压缩filter仅仅改变response,并不改变应用的流程。

  • 页面授权filter根据当前请求用户的身份,判定他是否有权限访问当前页面。这个filter会影响应用流程,却不会去改变request和response。

当然也有例外。有一些filter不做上面两件事中任何一件。例如,日志filter仅仅读取request对象并记录日志而已,既不改变request/response,也不影响应用的流程。还有一些filter同时做上面两件事。比如高速缓存页面的filter不仅要修改response,而且当cache被命中时,不再执行下一步的流程,而是直接返回cache中的内容,以提高性能。

Webx框架提供了两个服务,正好吻合了上述两个最常用的filter的功能。

Request Contexts服务 该服务负责访问和修改request和response,但不负责改变应用执行的流程。
Pipeline服务提供应用执行的流程,但不关心request和response。

虽然这两个服务看起来和filter的功能类似,但是它们远比filter要强大和方便 ── 它们克服了上述filter的几个限制:

  • 和Filter不同,Request Contexts和Pipeline服务可以访问应用内部的状态和资源,效率更高,功能更强。

  • 和Filter不同,Pipeline服务可以定义灵活(但仍然简单)地控制应用的流程 。Pipeline不仅可以控制流程的中断或继续,还可以实现子流程、循环、条件转移、异常处理等更精细的流程控制。Pipeline服务甚至可以运用在非WEB的环境中。

  • 和Filter不同,Request Contexts服务中的每一个环节(Request Context)之间并非完全独立、互不干涉的。每个request context可以访问它所依赖的其它request context中的状态。

6.2. Request Contexts服务

6.2.1. Request Contexts工作原理

Request Context,顾名思义,就是一个请求的上下文。事实上,你可以把Request Context看作是HttpServletRequestHttpServletResponse这两个对象的总和。除此之外,多个Request Context可以被串接起来,被称为Request Context Chain,类似于filter chain。

Request Context Chain

图 6.2. Request Context Chain

如上图所示,每一个Request Context都可以包括两个基本的操作:“预处理”和“提交”。

  • 在一个请求开始的时候,每个Request Context的“预处理”过程被依次调用。最内层的(即最先的)Request Context最先被调用,最外层的(即最后的)Request Context最后被调用;

  • 在一个请求结束的时候,每个Request Context的“提交”过程被依次调用。和“预处理”的顺序相反,最外层的(即最后的)Request Context最先被调用,最内层的(即最先的)Request Context最后被调用。

Request Context在预处理的时候,可以利用HttpServletRequestWrapperHttpServletResponseWrapper来包装和修改request和response ── 这一点和filter相同。每一层Request Context,都会增加一个新的特性。最先的Request Context成为最内层的包装,最后的Request Context成为最外层的包装。如下图所示。

Request Contexts的嵌套

图 6.3. Request Contexts的嵌套

和filter原理中的图进行对比,你会发现,尽管Request Contexts和Filter的执行方案有明显的不同,但是Request Contexts预处理和提交的顺序是和filter chain完全一致的。预处理时,由内层执行到外层;提交时,反过来由外层执行到内层。不同的是,filter能够决定是否继续传递控制权给filter chain中的下一位,而Request Context则没有这个权利。

6.2.2. Request Contexts的用途

Webx目前提供了以下几种request context的实现,每个都有独特的功能。

表 6.1. Request Contexts的功能

名称 功能
<basic> 提供基础安全特性,例如:过滤response headers、cookies,限制cookie的大小等。
<buffered> 缓存response中的内容。
<lazy-commit> 延迟提交response。
<parser> 解析参数,支持multipart/form-data(即上传文件请求)。
<rewrite> 重写请求的URL和参数。
<session> 一套可扩展的session框架,重新实现了HttpSession接口。
<set-locale>设置locale区域和charset字符集编码。
[注意]注意

本章对以上所有的request contexts的功能和用法不作具体的介绍,详情请参阅第 7 章 Request Contexts功能指南第 8 章 Request Context之Session指南

需要特别指出的是,你还可以扩展出更多的Request Context,以实现新的功能。

6.2.3. Request Contexts的使用

6.2.3.1. 配置

除了下面例子所示的一段配置之外,你不需要做太多的事,就可以使用Request Contexts。因为Request Contexts对于应用来说是透明的 ── 多数应用只需要依赖于HttpServletRequestHttpServletResponse就可以了。

例 6.2. Request Context的配置(/WEB-INF/webx.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:services="http://www.alibaba.com/schema/services"
    xmlns:request-contexts="http://www.alibaba.com/schema/services/request-contexts"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.alibaba.com/schema/services
                http://localhost:8080/schema/services.xsd
        http://www.alibaba.com/schema/services/request-contexts
                http://localhost:8080/schema/services-request-contexts.xsd
        http://www.springframework.org/schema/beans
                http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd
    ">
    ...

    <services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
        <basic />
        <buffered />
        <lazy-commit />
        <parser />
        <set-locale defaultLocale="zh_CN" defaultCharset="UTF-8" />
        <!-- Optional -
        <session />
        <rewrite />
        -->
    </services:request-contexts>

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

</beans:beans>

由于使用了SpringExt的schema机制,所以在支持schema的XML编辑器的帮助下,很容易书写和验证Request Contexts的配置。

6.2.3.2. 排序

Request Contexts之间,有时会有依赖关系,所以Request Contexts出现的先后顺序是非常重要的。例如,

  • <session>提供了基于cookie的session支持。然而cookie属于response header。一旦response被提交,header就无法再修改了。因此<session>依赖于<lazy-commit>,以阻止response过早提交。也就是说,<lazy-commit>必须排在<session>之前。

  • <rewrite>需要访问参数,而参数是能过<parser>解析的,所以<parser>要排在<rewrite>之前。

类似的约束还有很多。如果把Request Contexts的顺序排错,可能会导致某项功能错误或失效。然而,对于一般的应用开发者而言,这些约束往往是神秘的、并非显而易见的,需要经过细致地分析才能了解它们。

好在Request Contexts内部提供了一个机制,可以根据预定义的约束条件,对所有的Request Contexts进行自动排序。和Filter不同,应用开发者不需要在意Request Contexts在配置文件中的排列顺序,就可以保证所有的Request Contexts能够正常工作。下面的两种配置文件是等效的:

例 6.3. Request Contexts等效配置1

<services:request-contexts>
    <basic />
    <buffered />
    <lazy-commit />
    <parser />
    <set-locale />
    <session />
    <rewrite />
</services:request-contexts>

例 6.4. Request Contexts等效配置2

<services:request-contexts>
    <rewrite />
    <session />
    <set-locale />
    <parser />
    <lazy-commit />
    <buffered />
    <basic />
</services:request-contexts>

6.2.3.3. 访问特定的Request Context

一般来说,Request Contexts对于应用程序是透明的 ── 也就是说,应用程序最多只需要访问Servlet API中的接口:HttpServletRequestHttpServletResponse即可,就好像Request Contexts不存在一样。

比如,Request Context <parser>能够解析multipart/form-data类型的请求(即上传图片请求)。但你不需要用另一个API来访问请求中的普通数据,你只需要用HttpServletRequest中定义的方法就可以访问,仿佛这是一个普通的请求:

例 6.5. 访问任意类型的请求中的参数

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

再比如,Request Context <session>重新实现了HttpSession的接口,但是应用程序并不需要关心这些,他们还是和原来一样访问session:

例 6.6. 访问session

HttpSession session = request.getSession();

String value = (String) session.getAttribute("myattr");
session.setAttribute("myattr", newValue);

然而,有一些功能在原有的Servlet API中是不存在的。对于这一类功能,你必须访问特定的RequestContext接口,才能使用它们。例如,你只能用另一个API才能读取用户上传的文件。下面的代码可以用来取得上传文件的信息:

例 6.7. 访问特定的RequestContext接口

ParserRequestContext parserRequestContext =
                RequestContextUtil.findRequestContext(request, ParserRequestContext.class);

ParameterParser params = parserRequestContext.getParameters();

FileItem myfile = params.getFileItem("myfile");

String filename = myfile.getName();
InputStream istream = myfile.getInputStream();

另外有一些功能,使用Request Context接口比原来的Servlet API接口更方便。例如,原来的request.getParameter()方法只能取得字符串的参数值,但是利用ParserRequestContext所提供的接口,就可以直接取得其它类型的值:

例 6.8. 通过ParserRequestContext接口访问参数比HttpServletRequest更方便

ParameterParser params = parserRequestContext.getParameters();

String stringValue = params.getString("myparam"); // 取得字符串值,默认为null
int intValue = params.getInt("myparam"); // 取得整数值,默认为0
boolean booleanValue = params.getBoolean("myparam", true); // 取得boolean值,指定默认值为true

6.2.3.4. 注入request作用域的对象

Spring最强大的功能是依赖注入。但是依赖注入有一个限制:小作用域的对象不能被注入到大作用域的对象。你不能够把request和session作用域的对象注入到singleton对象中。前者在每次WEB请求时,均会创建新的实例,每个线程独享这个request/session作用域的对象;后者是在Spring初始化或第一次使用时被创建,然后被所有的线程共享。假如你把某个request/session作用域的对象意外注入到singleton对象中,将可能产生致命的应用错误,甚至导致数据库的错乱。

表 6.2. Webx中的重要对象及其作用域

对象类型 作用域
ServletContext Singleton scope
HttpServletRequest Request scope
HttpServletResponse Request scope
HttpSession Session scope
所有RequestContext对象,如:ParserRequestContextSessionRequestContextRequest scope

在一般的情况下,对于一个singleton对象而言,例如,Webx中的action module、pipeline valve对象等,下面的代码是错误的:

例 6.9. 在action(singleton对象)中注入request scope的对象

public class MyAction {
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private ParserRequestContext parser;
}

因为你不能把一个短期的对象如request、response和request context注入到MyAction这个singleton对象。然而,在Webx中,这样做是可以的!奥秘在于Request Contexts服务对上表所列的这些短期对象作了特殊的处理,使它们可以被注入到singleton对象中。事实上,被注入的只是一个“空壳”,真正的对象是在被访问到的时候才会从线程中取得的。

Webx鼓励应用程序使用singleton作用域的对象,不仅更简单,也更高效。经过上述技术处理以后,singleton对象访问request作用域对象的方法被大大简化了。

6.3. Pipeline服务

6.3.1. Pipeline工作原理

Pipeline的意思是管道,管道中有许多阀门(Valve),阀门可以控制水流的走向。在Webx中,pipeline的作用就是控制应用程序流程的走向。

Pipeline和Valves

图 6.4. Pipeline和Valves

Pipeline的设计和filter非常相似,也是击鼓传花式的流程控制。但是有几点不同:

  • Pipeline只能控制流程,不能改变request和response。

  • Pipeline是轻量级组件,它甚至不依赖于WEB环境。Pipeline既可以在程序中直接装配,也可以由spring和schema来配置。

  • Pipeline支持更复杂的流程结构,例如:子流程、条件分支、循环等。

6.3.2. Pipeline的用途

Pipeline可以说是Webx框架的核心功能之一。利用pipeline,你可以定制一个请求处理过程的每一步。

例 6.10. 一个典型的Webx应用的pipeline配置文件(pipeline.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:services="http://www.alibaba.com/schema/services"
    xmlns:pl-conditions="http://www.alibaba.com/schema/services/pipeline/conditions"
    xmlns:pl-valves="http://www.alibaba.com/schema/services/pipeline/valves"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.alibaba.com/schema/services
                http://localhost:8080/schema/services.xsd
        http://www.alibaba.com/schema/services/pipeline/conditions
                http://localhost:8080/schema/services-pipeline-conditions.xsd
        http://www.alibaba.com/schema/services/pipeline/valves
                http://localhost:8080/schema/services-pipeline-valves.xsd
        http://www.springframework.org/schema/beans
                http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">

        <!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。 -->
        <prepareForTurbine />

        <!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 -->
        <setLoggingContext />

        <!-- 分析URL,取得target。 -->
        <analyzeURL homepage="homepage" />

        <!-- 检查csrf token,防止csrf攻击和重复提交。 -->
        <checkCsrfToken />

        <loop>
            <choose>
                <when>
                    <!-- 执行带模板的screen,默认有layout。 -->
                    <pl-conditions:target-extension-condition extension="null, vm, jsp" />
                    <performAction />
                    <performTemplateScreen />
                    <renderTemplate />
                </when>
                <when>
                    <!-- 执行不带模板的screen,默认无layout。 -->
                    <pl-conditions:target-extension-condition extension="do" />
                    <performAction />
                    <performScreen />
                </when>
                <otherwise>
                    <!-- 将控制交还给servlet engine。 -->
                    <exit />
                </otherwise>
            </choose>

            <!-- 假如rundata.setRedirectTarget()被设置,则循环,否则退出循环。 -->
            <breakUnlessTargetRedirected />
        </loop>

    </services:pipeline>

</beans:beans>

6.3.3. Pipeline的使用

6.3.3.1. 创建一个valve

例 6.11. 一个简单的valve实现

public class MyValve implements Valve {
    public void invoke(PipelineContext pipelineContext) throws Exception {
        System.out.println("valve started.");

        pipelineContext.invokeNext(); // 调用后序valves

        System.out.println("valve ended.");
    }
}

配置(pipeline.xml

<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">
    ...
    <valve class="com.alibaba.myapp.pipeline.MyValve" />
    ...
</services:pipeline>

上面的代码和配置创建了一个基本的valve ── 事实上,它只是打印了一些消息,然后把控制权传递给后序的valves。

6.3.3.2. 执行一个pipeline

例 6.12. 在代码中执行pipeline

@Autowired
private Pipeline myPipeline;

public void invokePipeline() {
    PipelineInvocationHandle invocation = myPipeline.newInvocation();

    invocation.invoke();

    System.out.println(invocation.isFinished());
    System.out.println(invocation.isBroken());
}

从spring容器中取得一个pipeline对象以后(一般是通过注入取得),我们就可以执行它。上面代码中,PipelineInvocationHandle对象代表此次执行pipeline的状态。Pipeline执行结束以后,访问invocation对象就可以了解到pipeline的执行情况 ── 正常结束还是被中断?

Pipeline对象是线程安全的,可被所有线程所共享。但PipelineInvocationHandle对象不是线程安全的,每次执行pipeline时,均需要取得新的invocation对象。

6.3.3.3. 调用子流程

Pipeline支持子流程。事实上,子流程不过是另一个pipeline对象而已。

Pipeline和子流程

图 6.5. Pipeline和子流程

子流程是从valve中发起的。下面的Valve代码启动了一个子流程。

例 6.13. 在valve中发起一个子流程

public class MyNestableValve implements Valve {
    private Pipeline subPipeline;

    public void setSubPipeline(Pipeline subPipeline) {
        this.subPipeline = subPipeline;
    }

    public void invoke(PipelineContext pipelineContext) throws Exception {
        // 发起子流程,以当前流程的pipelineContext为参数
        PipelineInvocationHandle subInvocation = subPipeline.newInvocation(pipelineContext);

        subInvocation.invoke();

        System.out.println(subInvocation.isFinished());
        System.out.println(subInvocation.isBroken());

        pipelineContext.invokeNext(); // 别忘了调用后序的valves
    }
}

配置文件(pipeline.xml

<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">
    ...
    <valve class="com.alibaba.myapp.pipeline.MyNestableValve" p:subPipeline-ref="subPipeline" />
    ...
</services:pipeline>

6.3.3.4. 中断一个pipeline

Pipeline可以被中断。当有多级子pipeline时,你可以中断到任何一级pipeline。

例 6.14. 中断一个pipeline

pipelineContext.breakPipeline(0); // level=0,中断当前pipeline
pipelineContext.breakPipeline(1); // level=1,中断上一级pipeline

pipelineContext.breakPipeline("label"); // 中断到指定label的上级pipeline
// 以上调用相当于:
pipelineContext.breakPipeline(pipelineContext.findLabel("label"));

pipelineContext.breakPipeline(Pipeline.TOP_LABEL); // 终止所有pipelines
中断一个pipeline

图 6.6. 中断一个pipeline

6.3.3.5. 条件分支、循环

条件分支和循环其实只不过是子流程的运用而已:

条件分支 根据一定的条件,来决定是否要执行子流程、执行哪一个子流程(多条件分支)。
循环多次执行子流程。

下面的valve将子流程执行了至多10遍。如果子流程内部中断了流程,则循环终止。

例 6.15. 将子流程循环执行10次

public class Loop10 implements Valve {
    private Pipeline loopBody;

    public void setLoopBody(Pipeline loopBody) {
        this.loopBody = loopBody;
    }

    public void invoke(PipelineContext pipelineContext) throws Exception {
        PipelineInvocationHandle handle = loopBody.newInvocation(pipelineContext);

        for (int i = 0; i < 10 && !handle.isBroken(); i++) {
            handle.invoke();
        }

        pipelineContext.invokeNext();
    }
}

6.3.3.6. 存取pipeline的状态

当一个pipeline在运行时,你可以通过PipelineContext取得一些上下文信息:

例 6.16. 在valve中存取pipeline的状态

pipelineContext.index(); // 当前valve在pipeline中的序号
pipelineContext.level(); // 当前pipeline在所有子pipeline中的级别
pipelineContext.isBroken(); // 当前pipeline是否已经被中断
pipelineContext.isFinished(); // 当前pipeline的所有valves是否已经执行完

// 存取任意数据
pipelineContext.getAttribute(key);
pipelineContext.setAttribute(key, value);

6.3.3.7. 现成可用的valves

一般情况下,你并不需要写前面例子中的代码,因为Webx已经为你提供了一系列现成的valves来实现同样的功能。

无条件循环 - <loop>

例 6.17. 无条件循环

<services:pipeline>
    <loop loopCounterName="count" maxLoopCount="10"> 
        <valve />
        <break-if test="..." /> 
    </loop>
</services:pipeline>

定义循环变量loopCounterName,这个变量值将被保存在PipelineContext中,且可被其它的valve所访问。

定义maxLoopCount=10最大循环圈数,以避免循环失控。

无条件循环一定要和<break><break-if><break-unless>等valve相配合。

条件循环 - <while>

例 6.18. 条件循环

<services:pipeline>
    <while loopCounterName="count" test="count &lt;= 2"> 
        <valve />
    </while>

    <while maxLoopCount="10"> 
        <conditions:condition class="..." /> 
        <valve />
    </while>
</services:pipeline>

定义循环变量loopCounterName,这个变量值将被保存在PipelineContext中,且可被其它的valve所访问。

通过判断循环变量“count <= 2”,循环2次。

定义maxLoopCount=10,以避免循环失控。

可以自定义任意条件。

单条件分支 - <if>

例 6.19. 单条件分支

<services:pipeline>
    <if test="1 == 2"> 
        <valve />
    </if>

    <if>
        <conditions:condition class="..." /> 
        <valve />
    </if>
</services:pipeline>

JEXL条件表达式。

自定义任意条件。

多条件分支 - <choose><when><otherwise>

例 6.20. 多条件分支

<services:pipeline>
    <choose>
        <when test="1 == 2"> 
            <valve />
        </when>
        <when> 
            <conditions:condition class="..." />
            <valve />
        </when>
        <otherwise> 
            <valve />
        </otherwise>
    </choose>
</services:pipeline>

条件分支1,用JEXL表达式来判断。

条件分支2,用任意条件判断。

分支3,当所有条件均不符合时,选择该分支。

无条件中断 - <break>

例 6.21. 无条件中断

<services:pipeline>
    <loop> 
        <valve />
        <break /> 
        <valve />
    </loop>

    <loop> 
        <valve />
        <loop>
            <break levels="1" /> 
        </loop>
        <valve />
    </loop>

    <loop label="MY_LOOP"> 
        <valve />
        <loop>
            <break toLabel="MY_LOOP" /> 
        </loop>
        <valve />
    </loop>
</services:pipeline>

无条件中止当前的pipeline(即loop循环)。

无条件中止上一层(levels=1)的pipeline(即loop循环)。

无条件中止指定label的pipeline(即loop循环)。

有条件中断 - <break-if><break-unless>

有条件中断是<break><if>的组合。

例 6.22. 有条件中断

<services:pipeline>
    <loop loopCounterName="count">
        <valve />
        <break-if test="count &gt; 2" /> 
        <valve />
    </loop>

    <loop label="MY_LOOP">
        <valve />
        <break-if toLabel="MY_LOOP"> 
            <conditions:condition class="..." />  
        </break-if>
        <valve />
    </loop>

    <loop loopCounterName="count">
        <valve />
        <break-unless test="count &lt;= 2" />  
        <valve />
    </loop>
</services:pipeline>

count>2时中断。

<break-if><break-unless>均支持和<break>类似的其它选项:levelstoLabel

<if>类似,也支持任意condition。

<break-unless><break-if>的条件相反:除非count<=2,否则中断。

无条件退出整个pipeline - <exit>

退出整个pipeline,意思是结束所有的嵌套层次。

例 6.23. 无条件退出整个pipeline

<services:pipeline>
    <loop>
        <valve />
        <loop>
            <exit />
        </loop>
        <valve />
    </loop>
</services:pipeline>

对于Webx而言,<exit>还有一层特殊的含义:放弃WebxFrameworkFilter的控制权,把它交还给servlet engine。以URL http://localhost:8081/myapp/myimage.jpg为例,把控制权交还给servlet engine,意味着让servlet engine去显示myapp应用目录下的静态图片:myimage.jpg

异常捕获和finally处理 - <try-catch-finally>

类似Java中的try/catch/finally结构。

例 6.24. 异常捕获和finally处理

<services:pipeline>
    <try-catch-finally>
        <try>
            <valve />
        </try>
        <catch exceptionName="myexception"> 
            <valve />
        </catch>
        <finally>
            <valve />
        </finally>
    </try-catch-finally>
</services:pipeline>

<catch>标签可以将捕获的异常以指定名称保存在PipelineContext中,以便其它valve取得。

创建子流程 - <sub-pipeline>

单纯使用这个valve,对执行结果不会有任何影响。但可用来对较长的pipeline进行分段管理。

例 6.25. 创建子流程

<services:pipeline>
    <valve />
    <sub-pipeline label="mylabel">
        <valve />
    </sub-pipeline>
    <valve />
</services:pipeline>

6.3.3.8. 条件

在前文所述的各种条件valve(例如<if><when><while><break-if><break-unless>等)中,都用到一个共同的对象:condition。Condition是一个简单的接口。

例 6.26. Condition接口

public interface Condition {
    /**
     * 如满足条件,则返回<code>true</code>。
     */
    boolean isSatisfied(PipelineStates pipelineStates);
}

为了方便起见,Webx默认提供了一个JexlCondtion

例 6.27. 使用JexlCondition

<if>
    <conditions:jexl-condition expr="loopCount == 2" />
    <break />
</if>

以上配置可以简化为:

<if test="loopCount == 2">
    <break />
</if>

JEXL表达式是Apache的一个小项目,表达式语法详见:http://commons.apache.org/jexl/reference/syntax.html。在JEXL表达式中,你可以使用pipelineContext.getAttribute()所能取得的所有状态值。例如,loop循环时,如果你设置了loopCounterName,那么循环计数器就可以被JEXL表达式所访问。

除此之外,Webx还提供了三个组合式的条件。

<all-of>

要求所有条件均满足,相当于Java中的&&操作符。

例 6.28. 组合式的条件:<all-of>

<all-of>
    <condition1 />
    <condition2 />
    <condition3 />
</all-of>
<any-of>

只要求任一条件满足,相当于Java中的||操作符。

例 6.29. 组合式的条件:<any-of>

<any-of>
    <condition1 />
    <condition2 />
    <condition3 />
</any-of>
<none-of>

要求所有条件均不满足,相当于Java中的!操作符。

例 6.30. 组合式的条件:<none-of>

<none-of>
    <condition1 />
    <condition2 />
    <condition3 />
</none-of>

这三个组合式条件可以互相组合,以构成任意复杂的条件判断语句。

6.4. 本章总结

Request Contexts和Pipeline是Webx框架中的两个核心服务。它们分别从两个方面实现了原本需要由Filter来实现的功能 ── Request Contexts提供了包装和修改request/response的机制,而pipeline则提供了流程控制的能力。Request contexts和pipeline组合起来的功能比servlet filter机制更加强大。因为它们是基于Spring的轻量组件,其性能、配置的方便性、扩展性都优于filter。

当然,Request Contexts和Pipeline并不想取代filter。在好几种场合,filter仍然是唯一的选择:

  • 如果你既想要修改request/response,又想要控制流程;

  • 如果你希望独立于任何框架。

但在你接到一个需求,正打算用filter来实现之前,请考虑一下,是否可以采用Webx所提供的这两种机制来取代。倘若可行,必然会带来更多的好处。

第 7 章 Request Contexts功能指南

第 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()
TemplateServiceTemplateService如果指定了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显得更加优雅,因为其中大部分功能对应用程序是透明的 —— 应用程序不需要知道它们的存在,就可以享受它们所提供的功能。

第 8 章 Request Context之Session指南

Webx实现了一套session框架。Session框架建立在request contexts机制之上。建议你先阅读第 6 章 Filter、Request Contexts和Pipeline第 7 章 Request Contexts功能指南,以便了解request contexts是怎么回事。

8.1. Session概述

8.1.1. 什么是Session

HTTP协议是无状态的,但通过session机制,就能把无状态的变成有状态的。Session的功能就是保存HTTP请求之间的状态数据。有了session的支持,就很容易实现诸如用户登录、购物车等网站功能。在Servlet API中,有一个HttpSession的接口。你可以这样使用它:

例 8.1. 在Java代码中访问session

在一个请求中,保存session的状态

// 取得session对象
HttpSession session = request.getSession();

// 在session中保存用户状态
session.setAttribute("loginId", "myName");

在另一个请求中,取出session的状态:

// 得到"myName"
String myName = (String) session.getAttribute("loginId");

8.1.2. Session数据存在哪?

Session的状态数据是怎样保存的呢?

8.1.2.1. 保存在应用服务器的内存中

一般的做法,是将session对象保存在内存里。同一时间,会有很多session被保存在服务器的内存里。由于内存是有限的,较好的服务器会把session对象的数据交换到文件中,以确保内存中的session数目保持在一个合理的范围内。

为了提高系统扩展性和可用性,我们会使用集群技术 —— 就是一组独立的机器共同运行同一个应用。对用户来讲,集群相当于一台“大型服务器”。而实际上,同一用户的两次请求可能被分配到两台不同的服务器上来处理。这样一来,怎样保证两次请求中存取的session值一致呢?

一种方法是使用session复制:当session的值被改变时,将它复制到其它机器上。这个方案又有两种具体的实现,一种是广播的方式。这种方式下,任何一台服务器都保存着所有服务器所接受到的session对象。服务器之间随时保持着同步,因而所有服务器都是等同的。可想而知,当访问量增大的时候,这种方式花费在广播session上的带宽有多大,而且随着机器增加,网络负担成指数级上升,不具备高度可扩展性。

另一种方法是TCP-Ring的方式,也就是把集群中所有的服务器看成一个环,A→B→C→D→A,首尾相接。把A的session复制到B,B的session复制到C,……,以此类推,最后一台服务器的session复制到A。这样,万一A宕机,还有B可以顶上来,用户的session数据不会轻易丢失。但这种方案也有缺点:一是配置复杂;二是每增添/减少一台机器时,ring都需要重新调整,这将成为性能瓶颈;三是要求前端的Load Balancer具有相当强的智能,才能将用户请求分发到正确的机器上。

8.1.2.2. 保存在单一数据源中

也可以将session保存在单一的数据源中,这个数据源可被集群中所有的机器所共享。这样一来,就不存在复制的问题了。

然而单一数据源的性能成了问题。每个用户请求,都需要访问后端的数据源(很可能是数据库)来存取用户的数据。

这种方案的第二个问题是:缺少应用服务厂商的支持 —— 很少有应用服务器直接支持这种方案。更不用说数据源有很多种(MySQL、Oracle、Hsqldb等各种数据库、专用的session server等)了。

第三个问题是:数据源成了系统的瓶颈,一但这个数据源崩溃,所有的应用都不可能正常运行了。

8.1.2.3. 保存在客户端

把session保存在客户端。这样一来,由于不需要在服务器上保存数据,每台服务器就变得独立,能够做到线性可扩展和极高的可用性。

具体怎么做呢?目前可用的方法,恐怕就是保存在cookie中了。但需要提醒的是,cookie具有有以下限制,因此不可无节制使用该方案:

  • Cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。

  • 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。

  • 有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。

虽然有上述缺点,但是对于其优点(极高的扩展性和可用性)来说,就显得微不足道。我们可以用下面的方法来回避上述的缺点:

  • 通过良好的编程,控制保存在cookie中的session对象的大小。

  • 通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

  • 只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

  • 控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。

8.1.2.4. 将客户端、服务器端组合的方案

任何一种session方案都有其优缺点。最好的方法是把它们结合起来。这样就可以弥补各自的缺点。

将大部分session数据保存在cookie中,将小部分关键和涉及安全的数据保存在服务器上。由于我们只把少量关键的信息保存在服务端,因而服务器的压力不会非常大。

在服务器上,单一的数据源比复制session的方案,更简单可靠。我们可以使用数据库来保存这部分session,也可以使用更廉价、更简单的存储,例如Berkeley DB就是一种不错的服务器存储方案。将session数据保存在cookie和Berkeley DB(或其它类似存储技术)中,就可以解决我们的绝大部分问题。

8.1.3. 创建通用的session框架

多数应用服务器并没有留出足够的余地,来让你自定义session的存储方案。纵使某个应用服务器提供了对外扩展的接口,可以自定义session的方案,我们也不大可能使用它。为什么呢?因为我们希望保留选择应用服务器软件的自由。

因此,最好的方案,不是在应用服务器上增加什么新功能,而是在WEB应用框架上做手术。一但我们在WEB应用框架中实现了这种灵活的session框架,那么我们的应用可以跑在任何标准的JavaEE应用服务器上。

除此之外,一个好的session框架还应该做到对应用程序透明。具体表现在:

  • 使用标准的HttpSession接口,而不是增加新的API。这样任何WEB应用,都可以轻易在两种不同的session机制之间切换。

  • 应用程序不需要知道session中的对象是被保存到了cookie中还是别的什么地方。

  • Session框架可以把同一个session中的不同的对象分别保存到不同的地方去,应用程序同样不需要关心这些。例如,把一般信息放到cookie中,关键信息放到Berkeley DB中。甚至同是cookie,也有持久和临时之分,有生命期长短之分。

Webx实现了这种session框架,把它建立在Request Contexts的基础上。

8.2. Session框架

8.2.1. 最简配置

例 8.2. Session框架基本配置(/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <buffered />
    <lazy-commit />
    ...
    <session>
        <stores>
            <session-stores:simple-memory-store id="simple" /> 
        </stores>
        <store-mappings>
            <match name="*" store="simple" /> 
        </store-mappings>
    </session>
</services:request-contexts>

以上的配置,创建了一个最基本的session实现:将所有数据( name=*)保存在内存里( simple-memory-store)。

[警告]警告

最简配置只能用于开发,请不要将上述配置用在生产环境。因为simple-memory-store只是将数据保存在内存里。在生产环境中,内存有被耗尽的可能。这段配置也不支持服务器集群。

8.2.2. Session ID

Session ID唯一标识了一个session对象。把session ID保存在cookie里是最方便的。这样,凡是cookie值相同的所有的请求,就被看作是在同一个session中的请求。在servlet中,还可以把session ID编码到URL中。Session框架既支持把session ID保存在cookie中,也支持把session ID编码到URL中。

完整的session ID配置如下:

例 8.3. Session ID的配置

<session>
    <id cookieEnabled="true" urlEncodeEnabled="false">
        <cookie name="JSESSIONID" domain="" maxAge="0" path="/" httpOnly="true" secure="false" />
        <url-encode name="JSESSIONID" />
        <session-idgens:uuid-generator />
    </id>
</session>

上面这段配置包含了关于Session ID的所有配置以及默认值。如果不指定上述参数,则系统将使用默认值,其效果等同于上述配置。

表 8.1. Session ID的配置说明

配置<session><id> —— 将Session ID保存于何处?
cookieEnabled

是否把session ID保存在cookie中,如若不是,则只能保存的URL中。

默认为开启:true

urlEncodeEnabled

是否支持把session ID编码在URL中。如果为true开启,应用必须调用response.encodeURL()response.encodeRedirectURL()来将JSESSIONID编码到URL中。

默认为关闭:false

配置<session><id><cookie> —— 将Session ID存放于cookie的设置
name

Session ID cookie的名称。

默认为JSESSIONID

domain

Session ID cookie的domain。

默认为空,表示根据当前请求自动设置domain。这意味着浏览器认为你的cookie属于当前域名。如果你的应用包含多个子域名,例如:www.alibaba.comchina.alibaba.com,而你又希望它们能共享session的话,请把域名设置成“alibaba.com”。

maxAge

Session ID cookie的最长存活时间(秒)。

默认为0,表示临时cookie,随浏览器的关闭而消失。

path

Session ID cookie的path。

默认为/,表示根路径。

httpOnly

在session ID cookie上设置HttpOnly标记。

在IE6及更新版本中,可以缓解XSS攻击的危险。默认为true

secure

在session ID cookie上设置Secure标记。

这样,只有在https请求中才可访问该cookie。默认为false

配置<session><id><url-encode> —— 将Session ID编码到URL的设置
name

指定在URL中表示session ID的名字,默认也是JSESSIONID

此时,如果urlEncodeEnabledtrue的话,调用:

response.encodeURL("http://localhost:8080/test.jsp?id=1")

将得到类似这样的结果:

http://localhost:8080/test.jsp;JSESSIONID=xxxyyyzzz?id=1

配置<session><id><session-idgens:*> —— 如何生成session ID?
uuid-generator

以UUID作为新session ID的生成算法。

这是默认的session ID生成算法。

为了达到最大的兼容性,我们分两种情况来处理JSESSIONID

  1. 当一个新session到达时,假如cookie或URL中已然包含了JSESSIONID,那么我们将直接利用这个值。为什么这样做呢?因为这个JSESSIONID可能是由同一域名下的另一个不相关应用生成的。如果我们不由分说地将这个cookie覆盖掉,那么另一个应用的session就会丢失。

  2. 多数情况下,对于一个新session,应该是不包含JSESSIONID的。这时,我们需要利用SessionIDGenerator来生成一个唯一的字符串,作为JSESSIONID的值。SessionIDGenerator的默认实现UUIDGenerator

8.2.3. Session的生命期

所谓生命期,就是session从创建到失效的整个过程。其状态变迁如下图所示:

Session生命期

图 8.1. Session生命期

总结一下,其实很简单:

  1. 第一次打开浏览器时,JSESSIONID还不存在,或者存在由同一域名下的其它应用所设置的无效的JSESSIONID。这种情况下,session.isNew()返回true

  2. 随后,只要在规定的时间间隔内,以及cookie过期之前,每一次访问系统,都会使session得到更新。此时session.isNew()总是返回false。Session中的数据得到保持。

  3. 如果用户有一段时间不访问系统了,超过指定的时间,那么系统会清除所有的session内容,并将session看作是新的session。

  4. 用户可以调用session.invalidate()方法,直接清除所有的session内容。此后所有试图session.getAttribute()session.setAttribute()等操作,都会失败,得到IllegalStateException异常,直到下一个请求到来。

在session框架中,有一个