6.1 Spring远程调用概览 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

Spring远程调用概览

本章内容：

n 访问和公开RMI服务

n 使用Caucho的Hessian和Burlap协议

n 理解Spring的HTTP invoker

n 结合Web Services使用Spring

想象一下，你被困在一个荒凉的小岛上，这个听上去像是美梦成真。毕竟，谁不想在海滩上静静地独处，幸福地不用管外面世界的纷纷扰扰呢？

但是在一个荒岛上，不可能总是享受着冰镇果汁朗姆酒和日光浴。就算你享受这样宁静的隐居生活，过不了多久你就会饥饿、厌烦和孤独。在这段时间里，你只能以椰子和用叉捕的鱼为生。你最终还是需要食物、新的衣服以及其他供给。并且，如果你不和其他人联系，不久你就会以和排球说话告终。

很多你开发的应用会像被遗弃的荒岛。表面上看，它们好像能自给自足，但实际上，它们还要和其他系统合作，既有你组织内部的也有外部的。

例如，一个采购系统需要和一个厂商的供应链系统通信；可能你公司的人力资源系统需要和薪金系统整合；或者，甚至是薪金系统也要和打印、邮寄工资等外部系统交流。不管什么样的情况，你的应用需要和其他系统交流，远程访问它们的服务。

作为一个Java开发者，有好几种可以利用的远程技术，包括：

n 远程方法调用（RMI）

n Caucho的Hessian和Burlap

n Spring自己的HTTP invoker

n 企业JavaBean（EJB）

n Web Services

不管选择哪种远程技术，Spring对访问和创建远程服务提供了丰富的支持。在本章中，将学习Spring如何简化和补足这些远程服务。但首先，让我们通过远程调用如何在Spring中工作的概览，来为这一章打好基础。

6.1 Spring远程调用概览

远程调用是客户端应用和服务端之间的会话。在客户端上所需要的一些功能并不包括在该应用的职能范围内。所以应用向能提供这些功能的其他系统寻求帮助。远程的应用通过远程服务把这些功能公开出来。

例如，在Spring的培训应用里，当一名学生注册一门课程时，你希望客户能为这门课程付款（毕竟Spring培训是一项商业活动）。因此，Spring培训应用需要执行信用卡批准和付款结算任务。这个功能是超出了Spring培训应用本身的职能范围了。Spring培训应用决不能直接从学生的信用卡里扣款，甚至不知道这张信用卡能不能支付这个款项。只有发卡银行才能进行批准和结算。因此，Spring培训应用远程调用银行所公开的支付服务就非常有必要了（如图6.1所示）。

图6.1 Spring培训使用远程支付服务批准信用卡

Spring培训应用和银行之间的会话开始于一个从Spring培训应用到银行的支付服务的远程过程调用（RPC）。表面上看，RPC调用类似于调用一个本地对象的方法。它们都是同步的操作，调用代码将被堵塞，直等到被调用过程完成为止。

它们的不同也就是距离的问题，类似于人类之间的交流。假设你们在众所周知的正在工作的水冷机旁讨论周末足球比赛的结果，你们就在进行一个本地会话——也就是说，会话发生在同一房间的两个人之间。同样地，一个本地方法调用就是执行流程在同一个应用的两个代码块之间的交换。

另一方面，如果你拿起电话打给在另一个城市的客户，你们之间的对话就是通过电话网远程地进行的。类似的，RPC调用就是执行流程从一个应用通过网络传递给另一个应用，理论上，另一个应用是在远处跨越网络的一台不同的机器上。

Spring远程调用支持6种不同的RPC模式：远程方法调用（RMI）、Caucho的Hessian和Burlap、Spring自己的HTTP invoker、EJB和使用JAX-RPC 的Web Services。表6.1概括地论述了每个模式，并简略讨论它们在不同情况下的用处。

表6.1 Spring远程调用所支持的RPC模式

RPC模式 | 在何种情况下有用
远程方法调用（RMI） | 不考虑网络限制（如防火墙）时，访问/公开基于Java的服务
Hessian或 Burlap | 考虑网络限制时，通过HTTP访问/公开基于Java的服务
HTTP invoker | 考虑网络限制时，访问/公开基于Spring的服务
EJB | 访问用EJB实现的遗留的J2EE系统
JAX-RPC | 访问Web Services

不管选择哪种远程模式，你会发现Spring对每一种模式的支持中贯穿着一个共同的风格。这就意味着你一旦理解了Spring如何配置并使用其中的一种模式，当你决定使用另一种不同的模式的时候，你将拥有非常低的学习曲线。

在所有的模式中，服务可以作为Spring管理的Bean配置到你的应用中。这是用一个代理工厂Bean实现的，这个Bean使你能把远程服务当作本地对象一样置入到其他Bean的属性中。图6.2展示了它是如何工作的。

客户端发起对代理的调用，好像是代理提供了这些服务的功能一样。代理代表客户端和远程服务交流。它处理连接的具体情况，并向远程服务发起远程调用。

图6.2 在Spring里，远程服务是被代理的，所以他们能像一般的Bean那样置入到客户端的代码里

并且，如果对远程服务的调用结果是一个java.rmi.RemoteException的话，代理会处理这个异常，并把它作为一个非受查（unchecked）的org.springframework.remoting. RemoteAccessException重新抛出。远程异常通常预示着诸如网络或配置的问题，系统不能从这些问题中优雅地恢复。既然鲜有客户端能从远程异常中优雅地恢复过来，那么重新抛出一个RemoteAccessException就能让客户端来决定处理或不处理这个异常。

在服务端，你能够把任何Spring管理的Bean的功能公开成为一个远程服务，可使用在表6.1中所列的任何模式（除了EJB和JAX-RPC）。图6.3展示了远程输出器如何把Bean方法作为远程服务开放出来。

图6.3 Spring管理的Bean能通过RemoteExporter作为远程服务被开放出来

不论开发的是使用远程服务的代码，还是实现那些服务的代码，或者二者兼而有之，在Spring中，使用远程服务纯粹是个配置问题。你不用写任何Java代码来支持远程调用。你的服务Bean不必关心它们是否被卷入到RPC里（虽然任何传递给远程调用的Bean或从远程调用返回的Bean可能需要实现java.io.Serializable）。

让我们通过RMI——Java最初的远程技术，开始对Spring的远程调用支持的探索吧。


6.2 与RMI一起工作 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.2 与RMI一起工作

如果你用Java工作过，不管多长时间。毫无疑问地，你会听说过（也可能使用过）远程方法调用（RMI）。RMI——首先在JDK 1.1被引入到Java平台——提供给Java程序员一种强大的方法来实现Java程序间的交流。在RMI之前，对Java程序员来说，远程调用的惟一选择就是CORBA（在当时，需要购买一种第三方的产品，叫做Object Request Broker，或者叫做ORB），或者就是手工编写Socket程序。

但是，开发和访问RMI服务是非常沉闷的，牵涉到好几个步骤，包括程序的和手工的。Spring提供了一个代理工厂Bean，能让你把RMI服务像本地JavaBean那样置入到你的Spring应用中，大大简化了RMI模式。Spring还提供了一个远程输出器，用来简化把Spring管理的Bean转换到RMI服务的工作。

在开始学习Spring的RMI时，让我们来看看如何把一个RMI服务置入到Spring培训应用中。


6.2.1 连接RMI服务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.2.1 连接RMI服务

正如前面讲到的，Spring培训公司在学生登记一门课程时，需要通过信用卡来支付。幸运的是有这样的支付服务，能代表Spring培训公司处理这项功能。你所要做的就是把Spring培训应用挂到它上面。实际上，支付服务把它的功能公开为一个RMI服务。

访问支付服务的一种方法是写一个工厂方法，用传统的RMI方法取得对支付服务的引用。

private String payServiceUrl = "rmi:/creditswitch/PaymentService";

public PaymentService lookupPaymentService()

throws RemoteException, NotBoundException,

MalformedURLException {

PaymentService payService = (PaymentService)

Naming.lookup(payServiceUrl);

return payService;

}

这个payServiceUrl属性需要设置为RMI服务的地址。每次Spring培训应用需要一个支付服务的引用，它需要调用lookupPaymentService()方法。这种方法显然可以工作，然而却带来了两个问题：

1．传统RMI查找会导致3种异常中的任何一种（RemoteException、NotBoundException和MalformedURLException），这个异常必须被捕获或重新抛出。

2．需要支付服务的任何代码都有必须通过调用lookupPaymentService()来取得这个服务本身的一个引用。

在RMI查找过程中抛出的异常通常预示着应用中一个致命的不可恢复的情形。例如MalformedUrlException，意味着所给的这个服务的地址是无效的。要从这个异常中恢复，应用至少要被重新配置，也可能需要被重新编译。没有一个try/catch块能优雅地恢复，既然这样，为什么还要强制你的代码捕获和处理这个异常呢？

然而，更糟糕的事情是lookupPaymentService()是直接违反翻转控制原则的。这件事情的糟糕之处就在于，它意味着客户端lookupPaymentService()也要了解支付服务存在的位置以及它是一个RMI服务的事实。完美的情形下，你应该能够注入一个PaymentService对象到任何一个有需要的Bean中，而不是让Bean自己来查找服务。使用依赖注入，任何PaymentService客户端都可以忽略PaymentService是从哪里来的这一事实。

Spring的RmiProxyFactoryBean是一个工厂Bean，能创建一个指向RMI服务的代理。用RmiProxyFactoryBean来引用一个RMI PaymentService是非常简单的，只要在Spring配置文件中声明下面的<bean>：

<bean id="paymentService"

class="org.springframework.remoting.rmi.RmiProxyFactoryBean">

<property name="serviceUrl">

<value>rmi://${paymenthost}/PayService</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

RMI服务的URL是通过serviceUrl属性设置的。这里，服务被命名为PayService，并且是在一台名字用一个属性占位符配置的机器上（参考第2章的2.4.3节）。serviceInterface属性指明了这个服务所实现的接口，客户端通过这个接口调用在这个服务里的方法。

把这个支付服务定义为一个Spring管理的Bean，你就能把它作为一个合作者置入到另外的Bean上，就像你对任何非远程的Bean做的那样。举例来说，假设StudentServiceImpl需要使用这个支付服务来批准一张信用卡的支付。你就使用以下代码把RMI服务置入到StudentServiceImpl中：

<bean id="studentService"

class="com.springinaction.training.service.StudentServiceImpl">

…

<property name="paymentService">

<ref bean="paymentService"/>

</property>

…

</bean>

用这种方法访问RMI服务简直太棒了！StudentServiceImpl甚至不需要知道它处理的是一个RMI服务。它只是通过注入机制接收PaymentService对象，不必关心它是从哪里来的。此外，代理会捕获任何可能被这个服务抛出的RemoteException，并把它们作为运行时间异常重新抛出，这样，你可以安全地忽略这些异常。这也让远程服务Bean和这个服务的另外实现之间的交换成为可能——或许是不同的远程服务，或者有可能是单元测试时的一个模拟实现。

在Spring应用里，RmiProxyFactoryBean确实简化了RMI服务的功能，但这仅仅是RMI会话的一方面，让我们来看看Spring是如何支持RMI的服务端的。


6.2.2 输出RMI服务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.2.2 输出RMI服务

假设你负责写支付服务本身，而不是负责在Spring培训应用中访问支付服务的那部分工作。再一次，支付服务要作为RMI服务公开。

采用传统的RMI方法，你可能像程序清单6.1中展示的那样完成支付服务的实现。

  程序清单6.1 用传统方式（非Spring）实现作为RMI服务的支付服务

public class PaymentServiceImpl extends UnicastRemoteObject

implements PaymentService {

public PaymentServiceImpl() throws RemoteException {}

public String authorizeCreditCard(String creditCardNumber,

String cardHolderName, int expirationMonth,

int expirationYear, float amount)

throws AuthorizationException, RemoteException {

String authCode = ...;

// implement authorization

return authCode;

}

public void settlePayment(String authCode, int accountNumber,

float amount) throws SettlementException, RemoteException {

// implement settlement

}

}

n

对于PaymentServiceImpl所实现的PaymentService接口，你得确保它是从java.rmi.Remote继承的，如下所示：

public interface PaymentService extends Remote {

public String authorizeCreditCard(String cardNumber,

String cardHolderName, int expireMonth, int expireYear,

float amount) throws AuthorizationException, RemoteException;

public void settlePayment(String authCode, int merchantNumber,

float amount) throws SettlementException, RemoteException;

}

但是，只写一个服务实现类和一个接口是不够的，你得使用RMI编译器生成客户端的stub和服务端的skeleton类：

% rmic -d PaymentServiceImpl

最后，你要启动一个RMI注册表，并把这个服务绑定到注册表中。下面的代码处理了这个任务：

try {

PaymentService paymentService = new PaymentServiceImpl();

Registry registry = LocateRegistry.createRegistry(1099);

Naming.bind("PayService", paymentService);

} catch (RemoteException e) {

…

} catch (MalformedURLException e) {

…

}

哇！发布一个简单的RMI服务要做这么多工作。除了所有必需的步骤以外，你可能注意到了，相当多的RemoteException和MalformedUrlException都被抛出，即使这个异常通常意味着一个无法从捕捉块中恢复的致命的错误。明显地，没有Spring的话，发布一个RMI服务需要大量的代码和手工工作。

在Spring中配置RMI服务

幸亏，Spring提供了比较简单的发布RMI服务的方法：使用POJO。开始之前，你需要写这个服务的接口：

public interface PaymentService {

public String authorizeCreditCard(String cardNumber,

String cardHolderName, int expireMonth, int expireYear,

float amount) throws AuthorizationException;

public void settlePayment(String authCode, int merchantNumber,

float amount) throws SettlementException;

}

由于服务接口不是从java.rmi.Remote继承的，它的方法都不抛出java.rmi.RemoteException，这点让这个接口简短了很多。但更重要的是，客户端通过这个接口访问支付服务时，将不再需要捕捉那些它们可能没法处理的异常了。

下一步，你得定义服务的实现类。程序清单6.2展示了这个服务可以如何实现。

  程序清单6.2 用POJO定义支付服务

public class PaymentServiceImpl implements PaymentService {

public PaymentServiceImpl() {}

public String authorizeCreditCard(String creditCardNumber,

String cardHolderName, int expirationMonth,

int expirationYear, float amount)

throws AuthorizationException {

String authCode = ...;

// implement authorization

return authCode;

}

public void settlePayment(String authCode, int accountNumber,

float amount) throws SettlementException {

// implement settlement

}

}

n

你要做的下一件事就是在Spring的配置文件里把PaymentServiceImpl配置为一个<bean>：

<bean id="paymentService"

class="org.springframework.payment.PaymentServiceImpl">

…

</bean>

注意，这个版本的PaymentServiceImpl没有设置RMI所固有的特性。它仅仅是一个适合在Spring配置文件中声明的简单的POJO。事实上，完全有可能在非远程方式中，通过直接把它置入到客户端里，来使用这个实现。

但我们感兴趣的是远程使用这个服务。所以，最后一件要做的事情就是把PaymentServiceImpl输出成为一个RMI服务。你可以使用Spring的RmiServiceExporter，而不是使用rmic生成服务端的skeleton和客户端的stub，再手工把它添加到RMI注册表中（正如你在一般的RMI中所做的）。

RmiServiceExporter把任何Spring管理的Bean输出成一个RMI服务。它是通过把Bean包装在一个适配器类中工作的。适配器类被绑定到RMI注册表中，并且将请求代理给服务类——本例中为PaymentServiceImpl类：

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">

<property name="service">

<ref bean="paymentService"/>

</property>

<property name="serviceName">

<value>PayService</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

这里paymentService Bean被置入到service属性里，表示RmiServiceExporter将要把支付服务作为RMI服务输出。正如在6.2.1节中描述的RmiProxyFactoryBean那样，serviceName属性命名了RMI服务，而serviceInterface属性则指定这个服务所实现的接口。

RMI是和远程服务交流的一个优秀方法，但也有它的限制。首先，RMI通过防火墙工作时就有困难。那是因为RMI使用任意端口来交流——这是防火墙所不允许的。在企业内部网络环境中，这一点通常不用担心，但如果你是在这个“邪恶的互联网”上工作，用RMI可能会遇到麻烦。即使RMI提供了通过HTTP（通常防火墙都允许）走的隧道，但建立这个隧道也是不容易的事情。

另外一件要考虑的事情是RMI是基于Java的。这就意味着客户端和服务器端都必须是用Java写的。对你的应用而言，这可能是个问题，也可能不是个问题。但选择RMI做远程服务时，这是需要记在脑子里的一件事情。

Caucho Technology公司（这些人也开发了Resin应用服务器）开发了一套针对RMI限制的远程解决方案。实际上，他们采用了两种解决方法：Hessian和Burlap。让我们看一下如何在Spring中使用Hessian和Burlap来处理远程服务。


6.3 使用Hessian和Burlap的远程调用 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.3 使用Hessian和Burlap的远程调用

Hessian和Burlap是Caucho Technology（http://www.caucho.com）提供的两种解决方法，是基于HTTP的轻量级远程服务。它们都致力于通过把它们的API和通信协议变得尽可能的简单，来简化Web服务。

你可能会好奇，为什么Caucho对同一个问题有两种解决办法。事实上，Hessian和Burlap是同一个问题的两个方面，但每个都服务于略微不同的目的。Hessian，像RMI那样，使用二进制消息来建立客户端和服务端之间的交流。但与其他二进制远程技术（如RMI）不同的是，它的二进制消息可以移植到其他非Java的语言中。事实上，Caucho开发了一套对Python编程语言的Hessian实现。

Burlap是一种基于XML的远程技术，这使得它自然而然地可以移植到任何可以解析XML的语言中。正由于它的XML，比起Hessian的二进制格式来，它的可读性更强。但和其他基于XML的远程技术（例如SOAP或XML-RPC）不同，Burlap的消息结构是尽可能的简单，不需要额外的外部定义语言（如WSDL或IDL等）[1]。

从它们的大小来看，Hessian和Burlap都是轻量级的。每一个都完全包含在一个84KJAR文件里，除了Java运行时间类库以外没有额外的依赖类。这一点使它们在有内存限制的环境下使用非常理想，例如Java applet或手持设备。

你可能想知道如何在Hessian和Burlap之间做选择。很大程度上说，它们是一样的。惟一的不同就是Hessian的消息是二进制的，而Burlap的消息是XML。由于Hessian的消息是二进制的，所以它在带宽上更占优势。但如果可读性对你来说很重要的话（如出于调试的目的）或者你的应用将和没有Hessian实现（任何除了Java或Python）的语言交流，那么Burlap的XML消息会是更好的选择。

为了演示在Spring中的Hessian和Burlap服务，让我们回顾一下在6.2节中用RMI解决的支付服务问题。然而这一次，我们将看看如何用Hessian和Burlap作为远程模型来解决这个问题。


6.3.1 访问Hessian/Burlap服务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.3.1 访问Hessian/Burlap服务

回想一下6.2.1节中，StudentServiceImpl完全不知道支付服务是一个RMI服务。所有RMI的细节都包含在Spring配置文件的Bean的配置里。这样做的好处就是，由于客户端忽略了服务的实现，从一个RMI客户端转到Hessian客户端是极其简单的，不需要改变任何客户端代码。

坏处就是，如果你真地喜欢写代码的话，那么这一节就可能让你有点儿失望了。因为写基于RMI服务的客户端代码和基于Hessian服务的客户端代码惟一的不同就是你将使用Spring的HessianProxyFactoryBean来代替RmiProxyFactoryBean。客户端代码中基于Hessian的支付服务可以用以下代码声明：

<bean id="paymentService" class="org.springframework.

➥remoting.caucho.HessianProxyFactoryBean">

<property name="serviceUrl">

<value>http://${serverName}/${contextPath}/pay.service</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

就像基于RMI的服务那样，serviceInterface属性指定这个服务实现的接口。并且，如同RmiProxyFactoryBean，serviceUrl表明这个服务的URL。既然Hessian是基于HTTP的，当然应该在这里设置一个HTTP URL了（你将在下一节中看到这个URL是如何得来的）。

事实证明，写一个Burlap服务是同样无趣的。二者惟一的不同就是，你使用BurlapProxyFactoryBean代替HessianProxyFactoryBean：

<bean id="paymentService" class="org.springframework.

➥remoting.caucho.BurlapProxyFactoryBean">

<property name="serviceUrl">

<value>http://${serverName}/${contextPath}/pay.service</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

尽管我们觉得在RMI、Hessian和Burlap服务之间稍微不同的配置是很没有乐趣的，但这个单调恰恰是有好处的。它意味着你不费吹灰之力就可以在各种Spring支持的远程技术之间转换，无须去学习一个全新的模型。你一旦配置了一个对RMI服务的引用，把它重新配置为Hessian或Burlap服务也是很轻松的工作。

现在，让我们转到对话的另外一边，把Spring管理的Bean开放成为Hessian或Burlap服务。


6.3.2 用Hessian或Burlap公开bean的功能 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.3.2 用Hessian或Burlap公开Bean的功能

再一次，我们假设你的任务是要实现支付服务并把它的功能开放成为远程服务。然而这一次，你打算把它作为一个基于Hessian的服务公开。

即使没有Spring，写一个Hessian服务也是相当容易的。你只要写一个继承com.caucho.hessian.server.HessianServlet的服务类，并确保你的服务方法是public的（在Hessian里，所有public方法被视为服务方法）。

由于Hessian服务已经很容易实现了，Spring没有进一步做更多简化Hessian模式的工作。然而，和Spring一起使用的时候，Hessian服务可以在各方面利用Spring框架的优势，这是一个纯Hessian服务所没有的。包括利用Spring的AOP来向Hessian服务提供系统级服务，诸如声明式事务。

输出一个Hessian服务

在Spring里输出一个Hessian服务和在Spring里实现一个RMI服务惊人地相似。实际上，如果跟着6.2.2节中RMI的例子去做，你已经做了把支付服务公开为一个Hessian服务所需要做的大部分工作。

为把支付服务公开为RMI服务，你得在Spring配置文件中配置一个RmiServiceExporter Bean。非常类似的，把支付服务公开为Hessian服务，你也需要配置一个exporter Bean。只不过这一次用的是HessianServiceExporter：

<bean name="hessianPaymentService" class="org.springframework.

➥remoting.caucho.HessianServiceExporter">

<property name="service">

<ref bean="paymentService"/>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

HessianServiceExporter在Hessian服务中实现的功能和RmiServiceExporter在RMI服务中的功能是完全一样的。那就是说，它把一个Bean的公共方法公开为一个Hessian服务的方法。

正如RmiServiceExporter，service属性中被置入了实现这个服务的Bean的引用。这里，这个service属性绑定的是paymentService Bean的引用。serviceInterface属性用来表示PaymentService是这个服务所实现的接口。

然而，和RmiServiceExporter不同，你不需要设置serviceName属性。在RMI中，serviceName属性用来在RMI注册表中注册一个服务。Hessian没有注册表，因此就没有必要命名一个Hessian服务。

配置Hessian控制器

RmiServiceExporter和HessianServiceExporter另外一个主要的区别就是，由于Hessian是基于HTTP的，所以HessianServiceExporter被实现成Spring MVC的Controller。这就是说，为了使用输出的Hessian服务，你需要完成两个额外的配置步骤：

1．在你的Spring配置文件中配置一个URL处理器，来分发Hessian服务的URL给适当的Hessian服务Bean。

2．在web.xml中配置一个Spring的DispatcherServlet，并把你的应用部署为web应用。

将在第8章中学习Spring URL处理器和DispatcherServlet工作的细节。但在这里我们只是向你充分展示如何输出Hessian支付服务。

在6.3.1节中，你在客户端配置了serviceUrl属性，指向http://${serverName}/${contextPath}/ pay.service。这里的${serverName}和${contextPath}是通过PropertyPlaceholderConfigurer来配置的占位符。URL的最后部分，/pay.service，是我们感兴趣的地方，它就是你要映射Hessian支付服务的URL格式。

URL处理器把URL格式映射到具体的处理请求的Controller。在Hessian支付服务这个例子中，你要使用SimpleUrlHandlerMapping，把/pay.service匹配到hessianPaymentService Bean，如下所示：

<bean id="urlMapping" class="org.springframework.web.

➥servlet.handler.SimpleUrlHandlerMapping">

<property name="mappings">

<props>

<prop key="/pay.service">hessianPaymentService</prop>

</props>

</property>

</bean>

将在第8章中学习更多关于SimpleUrlHandlerMapping（8.2.2节）内容。目前，我们知道mappings属性包含了一系列键值为URL格式的属性就可以了。这里给出了一个键值为/pay.service的单个的属性，就是支付服务的URL格式。这个属性的值就是Spring Controller Bean的名字，它是用来处理到这个URL格式的请求的——在本例中就是hessianPaymentService。

因为HessianServiceExporter是在Spring MVC中作为一个controller实现的，所以你必须在web.xml中配置Spring的DispatcherServlet：

<servlet>

<servlet-name>credit</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

赋给这个Servlet的名字有很重要的意义，因为它是被DispatcherServlet用来定位Spring配置文件的。在本例中，由于这个Servlet名叫“credit”，所以配置文件必须命名为“credit-servlet.xml”。

公开Hessian服务的最后一步是建立一个Servlet映射：

<servlet-mapping>

<servlet-name>credit</servlet-name>

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

</servlet-mapping>

这样配置，任何URL以“.service”结束的请求都将被交给DispatcherServlet，它会依次把请求传交给匹配这个URL格式的Controller。因此，到“/pay.service”的请求最终将被hessianPaymentService Bean处理（它实际上仅仅是一个到PaymentServiceImpl的代理）。

输出一个Burlap服务

这一节有点虎头蛇尾，我们想你或许也愿意了解一下怎样把Spring管理的Bean作为Burlap服务输出。用Spring的BurlapServiceExporter来代替HessianServiceExporter就能完成这项任务。举例来说，下面Bean的定义展示了如何把支付服务作为一个Burlap服务公开：

<bean name="burlapPaymentService" class="org.springframework.

➥remoting.caucho.BurlapServiceExporter">

<property name="service">

<ref bean="paymentService"/>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

你会发现，除了Bean的名字（完全是任意的）和使用了BurlapServiceExporter以外，这个Bean和hessianPaymentService是一样的。配置Burlap服务和配置Hessian服务的其他方面也是一样的，这也就包括了需要建一个URL处理器和DispatcherServlet。

Hessian和Burlap解决了RMI头疼的防火墙问题。并且它们都是非常轻量级的，足以在内存或空间受限制的环境下使用，例如applet和无线设备。

但当序列化RPC消息发来的对象时，RMI就把Hessian和Burlap打败了。因为Hessian和Burlap都采用了自己私有的序列化机制，而RMI使用的是Java本身的序列化机制。如果你的数据模型非常复杂，Hessian/Burlap的序列化模型可能就不够了。

还有一个两全其美的解决方案。让我们来看一下Spring的HTTP invoker，它在HTTP之上提供了RPC（正如Hessian/Burlap），同时使用了Java的对象序列化机制（正如RMI）。


6.4 使用Http Invoker - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.4 使用HTTP invoker

Spring开发小组意识到在RMI服务和基于HTTP的服务（如Hessian和Burlap）之间的空白。一方面，RMI使用Java标准的对象序列化，但很难穿越防火墙；另一方面，Hessian/Burlap能很好地穿过防火墙工作，但使用自己私有的一套对象序列化机制。

就这样，Spring的HTTP invoker应运而生。HTTP invoker是一个新的远程调用模型，作为Spring框架的一部分，来执行基于HTTP的远程调用（让防火墙高兴的事），并使用Java的序列化机制（让程序员高兴的事）。

使用基于HTTP invoker的服务和使用基于Hessian/Burlap的服务非常相似。为了开始学习HTTP invoker，让我们再来看一下这个支付服务——这一次将作为HTTP invoker支付服务来实现。


6.4.1 通过HTTP访问服务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.4.1 通过HTTP访问服务

要访问RMI服务，得声明一个指向这个服务的RmiProxyFactoryBean；要访问Hessian服务，得声明一个HessianProxyFactoryBean.；要访问Burlap服务的话，得用BurlapProxyFactoryBean。把这样千篇一律的用法带到HTTP invoker上，应该也不会带来什么惊奇，那就是访问一个HTTP invoker服务，你需要使用HttpInvokerProxyFactoryBean。

要让支付服务作为一个HTTP invoker服务公开，得配置一个Bean，用HttpInvokerProxyFactoryBean来代理它，如下所示：

<bean id="paymentService" class= "org.springframework.remoting.

➥httpinvoker.HttpInvokerProxyFactoryBean">

<property name="serviceUrl">

<value>http://${serverName}/${contextPath}/pay.service</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

把这个Bean的定义和那些在6.2.1节、6.3.1节中的比较一下，你会发现几乎没什么变化。serviceInterface属性仍然用来表示这个支付服务所实现的接口；serviceUrl属性仍然是用来表示远程支付服务的位置。由于HTTP invoker是基于HTTP的，如同Hessian和Burlap一样，serviceUrl就能包含与Hessian和Burlap版本的Bean里一样的URL。

现在，让我们转到HTTP invoker对话的另一面，看看如何把Bean的功能作为基于HTTP invoker的服务输出。


6.4.2 把Bean作为HTTP服务公开 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.4.2 把Bean作为HTTP服务公开

你已经看到如何把PaymentServiceImpl的功能作为RMI服务公开，或者是作为Hessian服务，以及Burlap服务。接下来，我们要把这个支付服务改写为HTTP invoker服务，用Spring的HttpInvokerServiceExporter来输出这个支付服务。

为了避免听上去有违反先例的危险，我们必须告诉你，使用HttpInvokerServiceExporter把Bean的方法输出为远程方法，和你已经看到的其他远程服务的exporter做事的方法非常相像。实际上，就是一样的。举例来说，下面的Bean的定义展示了如何把paymentService Bean作为一个远程的基于HTTP invoker的服务输出：

<bean id="httpPaymentService" class="org.springframework.remoting.

➥httpinvoker.HttpInvokerServiceExporter">

<property name="service">

<ref bean="paymentService"/>

</property>

<property name="serviceInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

有点奇怪的似曾相识的感觉吗？你很难找出这个Bean的定义和那些在6.3.2节中的不同来。这里的粗体字不是帮你发现它的，而是惟一的区别就在于使用了HttpInvokerServiceExporter。否则的话，这个exporter和其他的远程服务的exporter就没什么区别了。

基于HTTP invoker的服务，顾名思义，是基于HTTP的，就像Hessian和Burlap服务一样。并且，也和HessianServiceExporter和BurlapServiceExporter那样，HttpInvokerServiceExporter也是一个Spring的Controller。这就意味着你需要建立一个URL处理器，把HTTP URL映射到服务上：

<bean id="urlMapping" class="org.springframework.web.

➥servlet.handler.SimpleUrlHandlerMapping">

<property name="mappings">

<props>

<prop key="/pay.service">httpPaymentService</prop>

</props>

</property>

</bean>

同时你也需要把这个支付服务部署到web应用中，通过在web.xml中配置Spring的DispatcherServlet：

<servlet>

<servlet-name>credit</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>credit</servlet-name>

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

</servlet-mapping>

这样配置，支付服务便能通过/pay.service获得，和它作为Hessian服务或Burlap服务公开时的URL一样。

Spring的HTTP invoker是作为一个两全其美的远程调用解决方案出现的，把HTTP交流的简单性和Java内置的对象序列化机制结合起来。这让HTTP invoker服务成为一个引人注目的对RMI或是对Hessian/Burlap的替代品。

要记住HTTP invoker有个重大的限制，它是一个只在Spring框架中提供的远程调用解决方案。这就意味着客户端和服务器端都必须是使用Spring的应用。并且，至少就目前而言，也暗示了客户端和服务器端都必须是基于Java的[2]。

到现在为止我们所讨论的所有远程技术中，没有一个受到比Enterprise JavaBean (EJB)更多的关注。甚至可以说，有关EJB的出版物比任何其他Java技术都要多。让我们看一下如何把EJB用到你的Spring应用中。


6.5 使用EJB - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.5 使用EJB

你可能会奇怪，在这本书中还有一节是如何在Spring里使用EJB的。就本书来说，大部分内容都是向你展示如何不求助于EJB来实现企业级应用。在这本书里，关于EJB的章节的可能看上去有点成为对比了。那么为什么我们现在要来讨论EJB呢？

答案就是，虽然Spring提供了大量的功能，让POJO具有EJB的能力，但你或许不能总是享受在完全没有EJB的项目上工作的奢侈。一方面，你可能会接触一些其他系统，它们的功能是通过无状态的会话EJB开放出来的。另一方面，你可能被放在一个项目中，由于正统技术（或者可能是政治）的原因，你不得不写EJB代码。

不管你的应用是EJB客户端，还是你必须写EJB本身，你都不需要为了用EJB，完全放弃Spring带来的好处。Spring有两种方法提供对EJB的支持：

n Spring能让你在Spring的配置文件里，把EJB作为Bean来声明。这样，把EJB引用置入到其他Bean的属性里就成为可能了，好像EJB就是另一个POJO。

n Spring能让你写EJB，让EJB成为Spring配置的Bean的代理的工作。

让我们探究一下Spring的EJB抽象功能，就从如何在Spring配置文件里把EJB作为Bean声明开始吧。


6.5.1 访问EJB - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.5.1 访问EJB

为了说明Spring对EJB访问的支持，让我们再回到那个支付服务。然而这一次，假设这个支付服务实现在一个遗留下来的系统上，通过一个无状态会话EJB公开它的功能。[3]

或许你还记得如何用传统的方式访问EJB。正如你所知道的，必须通过JNDI来查找home接口。可能，你会写一些像这样的代码来查找支付服务的home接口：

private PaymentServiceHome paymentServiceHome;

private PaymentServiceHome getPaymentServiceHome ()

throws javax.naming.NamingException {

if(paymentServiceHome != null)

return paymentServiceHome;

javax.naming.InitialContext ctx =

new javax.naming.InitialContext();

try {

Object objHome = ctx.lookup("paymentService");

PaymentServiceHome home =

(PaymentServiceHome) javax.rmi.PortableRemoteObject.narrow(

objHome, PaymentServiceHome.class);

return home;

} finally {

ctx.close();

}

}

一旦得到了这个home接口的引用，那么你还需要取得这个EJB的远程（或者本地）接口的引用，来调用它的业务方法。例如，下面的代码就是展示了如何调用这个支付服务EJB的authorizeCreditCard()方法：

try {

PaymentServiceHome home = getPaymentServiceHome ();

PaymentService paymentService = home.create();

String authCode =

paymentService.authorizeCreditCard(ccNumber, cardHolderName,

expMonth, expYear, amount);

} catch (javax.rmi.RemoteException e) {

throw new CreditException();

} catch (CreateException e) {

throw new CreditException();

}

哇，这么多的代码！真够烦人的，仅仅只有几行代码是有关批准信用卡的，大部分的代码都只是为了获得这个EJB的引用。这就像是做了一大堆的工作，就是为了一个EJB的authorizeCreditCard()方法的调用。

坚持住。你已经看到，贯穿整本书的，都是把你的应用需要的服务注入到应用中的Bean里。Bean不查找其他的Bean……Bean是被置入到其他Bean中的。但是，这整个通过JNDI和EJB的home接口来查找EJB的活动看上去不适合应用其他部分的结构。如果继续用传统的方式和EJB打交道，它的查找代码会把你的代码都搞乱，毫无疑问的，你的代码会和EJB结合在一起。那么，有没有一个更好的方法呢？

代理EJB

可能你已经从这个语气当中猜到了，是的，这里还有一个更好的方法。在这章的前面部分，我们向你展示了如何配置到各种远程服务的代理，包括RMI、Hessian、Burlap和HTTP invoker服务。非常类似的，Spring提供了两个代理工厂Bean，来代理EJB的访问：

n LocalStatelessSessionProxyFactoryBean——用来访问本地EJB（EJB和它的客户端在同一个容器中）。

n SimpleRemoteStatelessSessionProxyFactoryBean——用来访问远程EJB（EJB和它的客户端在独立的容器中）。

配置这些代理工厂的方法和配置RMI、Hessian、Burlap和HTTP invoker的方法有很大的不同，这打破了本章前几节中千篇一律的论调。让我们来看看，如何使用这些Bean来访问支付服务EJB。为了简单起见，假设这个EJB是一个本地EJB，有一个JNDI名字：payService。下面的XML展示了如何在Spring的配置文件中声明这个EJB：

<bean id="paymentService" class="org.springframework.ejb.

➥access.LocalStatelessSessionProxyFactoryBean"

lazy-init="true">

<property name="jndiName">

<value>payService</value>

</property>

<property name="businessInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

由于这是一个本地EJB，LocalStatelessSessionProxyFactoryBean是个适当的代理工厂Bean类。同时要把jndiName属性设置为paymentService，这样，这个代理工厂Bean就可以查找到这个EJB的home接口。

关于这个声明，一件重要的需要注意的事情就是<Bean>元素的lazy-init属性。当任何一个EJB装载代理工厂Bean在ApplicationContext中使用时，这一点是相当重要的。这是因为在Spring配置文件被装载进来的时候，ApplicationContext风格的Bean工厂预先实例化了单实例的Bean。这通常是一件好事，但它可能会导致EJB代理工厂Bean在EJB被绑定到名字服务之前，试图查找EJB的home接口。把lazy-init设置为true，确保这个“支付服务”不会试图查找home接口，直到它首次被使用为止——这将会留有足够的时间来把EJB绑定到名称服务上。

这个businessInterface属性等同与在其他远程服务代理工厂Bean中使用的serviceInterface。它再一次被设置为com.springinaction.payment.PaymentService，表示这个服务依附在PaymentService接口上。

置入EJB

现在，让我们把支付服务EJB置入到studentService Bean里：

<bean id="studentService"

class="com.springinaction.training.service.StudentServiceImpl">

…

<property name="paymentService">

<ref bean="paymentService"/>

</property>

…

</bean>

看到了吗？把支付服务EJB置入到studentService Bean里和置入一个POJO没什么两样。这个paymentService Bean （作为EJB的代理）只是简单地被注入到studentService的paymentService属性。

在使用代理工厂Bean访问这个支付服务EJB中，最精彩的事情莫过于你不需要写自己的服务定位器（Service Locator）或业务委托（Business Delegate）代码。实际上，无论哪种JNDI代码你都不需要写。同样，也不需要处理EJB的home接口（或者在本例中是本地home接口）。

此外，通过把一切隐藏在PaymentService 业务接口背后，studentService Bean甚至不需要知道所处理的是EJB。至于它所知道的，那就是它在和一个POJO合作。这一点非常有意义，因为它意味着你可以自由地将PaymentService的EJB实现替换成任何其他的实现（甚至在单元测试StudentServiceImpl时可以使用一个模拟实现）。

然后呢？

或许你想知道，这些不可思议的工作是如何进行的。你怎样才能置入一个EJB，就像其他的Bean那样呢？好，在LocalStatelessSessionProxyFactoryBean背后，有着大量的工作在进行，让这一切成为可能。

首先，在启动的时候，LocalStatelessSessionProxyFactoryBean用jndiName属性指定的JNDI名字，通过JNDI来查找EJB的本地home接口。接着它把这个以后要用到的接口缓存起来，这样以后就不需要再进行同样的JNDI查找了。

然后，每一次在PaymentService接口上的方法被调用时，代理会调用本地home接口上的create()方法来取得这个EJB的引用。最后，代理将调用这个EJB上相应的方法。

所有这些诡计都是为了制造这个支付服务是一个简单POJO的错觉，而本质上还是和EJB在打交道。

访问一个远程EJB

现在你知道如何把一个本地EJB置入到你的Spring应用中了。但是，如果是一个真实的应用，这个支付服务EJB更有可能是一个远程EJB。在这种情况下，要使用SimpleRemoteStatelessSessionProxyFactoryBean，并把它声明在Spring配置文件中，如下所示：

<bean id="paymentService" class="org.springframework.ejb.

➥access.SimpleRemoteStatelessSessionProxyFactoryBean"

lazy-init="true">

<property name="jndiName">

<value>payService</value>

</property>

<property name="businessInterface">

<value>com.springinaction.payment.PaymentService</value>

</property>

</bean>

注意，这儿惟一的区别就是选择了SimpleRemoteStatelessSessionProxyFactoryBean。除此之外，Spring使选择用本地还是远程EJB对使用EJB的代码是透明的。

但你可能会对java.rmi.RemoteException感到惊奇。如果调用远程EJB方法抛出一个RemoteException的话，本地还是远程EJB的选择怎么会是完全透明的呢？难道不需要谁来捕获这个异常吗？

这是使用Spring的EJB支持来访问EJB的另一个好处。和用RMI服务一样，任何从EJB里抛出的RemoteException都会被捕获，然后作为org.springframework.remoting. RemoteAccessException（一个非受检异常）重新抛出。这就把是否捕获异常的选择交给了EJB客户端。

现在，你已经知道如何把EJB置入到你的Spring应用里，让我们再来看看Spring对EJB开发是如何支持的。


6.5.2 用Spring开发EJB - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.5.2 用Spring开发EJB

虽然Spring提供了很多功能，让不使用EJB而实现企业级应用成为可能，但是，你可能会发现，你还是需要把你的部件作为EJB来开发。

到这里为止，你已经看到了Spring如何通过提供service exporter类，变戏法似的将POJO输出成远程服务。虽然我们非常不愿意让你失望，但不幸的是，Spring并没有提供一个能把POJO作为EJB输出的EjbServiceExporter类。（但我们都非常同意，像这样的一个exporter类将会是真正的酷毙了。）

不管怎么样，Spring提供了4种抽象支持类，让EJB开发稍微容易些：

n AbstractMessageDrivenBean——对开发从JMS之外的其他消息源接收消息的message-driven Bean有帮助（EJB 2.1规范许可这种情形）。

n AbstractJmsMessageDrivenBean——对开发从JMS源接收消息的message-driven Beans有帮助。

n AbstractStatefulSessionBean——对开发有状态EJB有用。

n AbstractStatelessSessionBean——对开发无状态EJB有用。

这些抽象类通过两种方式简化EJB开发：

n 它们提供了EJB生命周期方法的默认的空的实现（例如：ejbActivate()，ejbPassivate()，ejbRemove()）。这些方法在每个EJB规范中都需要，但是通常都是作为空方法实现的。

n 它们提供了对Spring Bean工厂的访问。这让你有可能把EJB实现成一个代理，由EJB将业务逻辑的责任委托给由Spring配置的POJO来做。

例如，假设你要把课程服务Bean公开成无状态EJB。程序清单6.3展示了如何来实现这个EJB。

  程序清单6.3 无状态EJB将业务逻辑的责任委托给POJO来做

public class CourseServiceEjb extends AbstractStatelessSessionBean

implements CourseService {

private CourseService courseService;

protected void onEjbCreate() {

courseService =

(CourseService) getBeanFactory().getBean("courseService");

}

public Course getCourse(Integer id) {

return courseService.getCourse(id);

}

public void createCourse(Course course) {

courseService.createCourse(course);

}

public Set getAllCourses() {

return courseService.getAllCourses();

}

public void enrollStudentInCourse(Course course, Student student)

throws CourseException {

courseService.enrollStudentInCourse(course, student);

}

}

n

当CourseServiceEjb被创建的时候，它的onEjbCreate()方法从Spring Bean 工厂取得到courseService Bean。然后，当这个EJB的任何方法被调用时，它们就把责任委托给courseService Bean。

注意在清单6.3中的EJB中，一个尚未解答的大问题就是，Bean工厂是从哪里来的。在典型的J2EE风格里，抽象的EJB类从JNDI取得Bean工厂。默认情况下，它们将用java:comp/env/ejb/BeanFactoryPath作为JNDI名字来查找Bean工厂。要用另外的JNDI名字查找Bean factory，必须在Bean工厂被加载之前设置beanFactoryLocatorKey属性（在构造方法中，或者在setSessionContext()方法中）。例如：

public void setSessionContext(SessionContext sessionContext) {

super.setSessionContext(sessionContext);

setBeanFactoryLocatorKey("java:comp/env/ejb/MyBeanFactory");

}

不管是好是坏，EJB在Java开发社区里已经被讨论了好几年了。但web service是一个嗡嗡作响的超越了语言和平台边界的远程调用技术。为了结束本章内容，让我们来看看Spring如何通过JAX-RPC支持web service的。


6.6 使用JAX-RPC的Web Services - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.6 使用JAX-RPC的Web Service

JAX-RPC是“基于XML的远程调用的Java API（Java APIs for XML-based remote procedure call）”的缩写。这是一个口语化的词，仅仅意味着JAX-RPC是Java程序使用XML访问远程服务的一种方式。特别地，这个服务是指用SOAP（Simple Object Access Protocol）协议公开它们的功能的web service。

有关JAX-RPC以及基于SOAP的web service的细节已经超出了本书范围。我们假定你早已掌握了SOAP和JAX-RPC的基础了。如果你想对JAX-RPC和SOAP有个初步的或最新的了解，可以参考Richard Monson-Haefel写的J2EE Web Services（AddisonWesley，2003版）。

为说明Spring对通过JAX-RPC的web service访问的支持，我们可以重新回到支付服务，但你可能会对这样的重复感到非常乏味（我们知道确实如此）。所以，对于JAX-RPC，我们想你会很高兴能从这个支付服务中休息一会儿，我们将用一个Babel Fish服务来代替它。

如果你以前读过The Hitchhiker’s Guide to the Galaxy这本书，你可能已经知道什么是Babel Fish了。对于那些不知道我们在说什么的人来说，我要告诉你，Babel Fish就是一条黄色的小鱼，当你把它放在耳朵里的时候，它会把一种口语翻译成另一种。简言之，它能让任何把鱼放在耳朵里的人理解别人所说的任何话，不管是用什么语言说的。

我们知道绝大多数的读者不可能去访问一个真的Babel Fish（就算你真的这样做，你会发现放一条鱼在耳朵里是让人毛骨悚然的）。但却有一个web service实现了类似的功能。事实上，它恰好就命名为“BabelFishService”。你可以找到Babel Fish web service的WSDL（Web Service定义语言，或Web Service Definition Language)文件，就是在这个URL中：http://www.xmethods.com/sd/2001/BabelFishService.wsdl。


6.6.1 用JAX-RPC应用一个Web Service - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.6.1 用JAX-RPC应用一个Web Service

为了要使用这个Babel Fish web service，得创建一个定义这个服务的接口。查看一下WSDL，你会发现Babel Fish web service只有一个操作叫做：BabelFish。这个操作有两个参数：一个是字符串，表示翻译模式（参考工具条）；另一个也是字符串，表示原始的尚未翻译的文本。它返回一个包含了翻译后文本的字符串。BabelFishRemote.java（程序清单6.4）展示了定义这个服务的远程接口。

  程序清单6.4 Bable Fish web service的远程服务接口

package com.springinaction.chapter06.babelfish;

import java.rmi.Remote;

import java.rmi.RemoteException;

public interface BabelFishRemote extends Remote {

public String BabelFish(String translationMode,

String sourceData) throws RemoteException;

}

n

工具条：翻译模式由两种语言代码组成，用下划线（_）分隔。一些正确的语言代码是：英语用“en”，法语用“fr”，德语用“dr”，西班牙语用“es”。在下划线之前的语言代码表示原始文本的语言，下划线之后的语言代码表示原始文本要翻译成的目标语言。例如，一个“de_en”的翻译模式将把德语文本翻译到英语文本。

BabelFishRemote接口包含了一个方法：BabelFish()。这个方法名是从WSDL的操作名中过来的。不巧的是，这个web service以一个大写的“B”开始，不像Java规范中，方法名用小写字母开始。下面的代码展示了如何使用一个传统的JAX-RPC（也就是说，没有Spring的帮助）来获得一个Babel Fish服务的引用。

String wsdlDocumentUrl =

"http://www.xmethods.com/sd/2001/BabelFishService.wsdl";

String namespaceUri =

"http://www.xmethods.net/sd/BabelFishService.wsdl";

String serviceName = "BabelFishService";

String portName = "BabelFishPort";

QName serviceQN = new QName(namespaceUri, serviceName);

QName portQN = new QName(namespaceUri, portName);

ServiceFactory sf = ServiceFactory.newInstance();

Service service =

sf.createService(new URL(wsdlDocumentUrl), serviceQN);

BabelFishRemote babelFish = (BabelFishRemote)

service.getPort(BabelFishRemote.class, portQN);

手头有了一个指向这个服务的引用，你就能用它来翻译任何你想翻译的文本。例如，把“Hello world”从英语（en）翻译到西班牙语（es）：

String translated = babelFish.BabelFish("en_es", "Hello World");

同样，可以使用下面语句把西班牙语（es）翻译为法语（fr）：

String translated = babelFish.BabelFish("es_fr", "Hola Mundo");

或者从法语（fr）到德语（de）：

String translated = babelFish.BabelFish("fr_de", "Bonjour Monde");

Babel Fish服务是很有趣的，但是标准JAX-RPC方法的一个问题就是它引入了一大堆代码，仅仅是为了查找这个babel fish服务。为了让它变得更简单些，可以用JSR-109（实现企业级Web Service，或Implementing Enterprise Web Services）建议的方法，用JNDI来检索web service：

Context ic = new InitialContext();

BabelFishService babelFishService =

(BabelFishService) ic.lookup("java:comp/env/service/BabelFish");

BabelFishRemote babelFish =

(BabelFishRemote) babelFishService.getBabelFishPort();

但是，即使这个JNDI版本比原来简练了，它仍然让客户端负责获得它自己的指向这个服务的引用。这样做的话，就违反了反转控制的精神。并且，它把处理RemoteException的重担放在客户端上了。

现在，你已经看到使用JAX-RPC访问web service的常规方法了，让我们再看看在Spring里是怎么做的。


6.6.2 在Spring里置入一个web service - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.6.2 在Spring里置入一个Web Service

和本章中讨论的其他远程技术一样，Spring提供了一个代理工厂Bean：JaxRpcPortProxyFactoryBean，能让你在应用中无缝集成一个web sevice，作为另一个Bean的协作者。在背后，JaxRpcPortProxyFactoryBean用JAX-RPC来访问远程web service。

程序清单6.5中的XML展示了如何在Spring配置文件中把Babel Fish服务声明成一个Bean：

  程序清单6.5 在Spring应用上下文中把Babel Fish服务作为一个Bean

<bean id="babelFish" class="org.springframework.remoting.

➥ jaxrpc.JaxRpcPortProxyFactoryBean">

<property name="wsdlDocumentUrl">

<value>http://www.xmethods.com/sd/2001/

➥ BabelFishService.wsdl</value>

</property>

<property name="serviceInterface">

<value>com.springinaction.chapter06.babelfish.

➥ BabelFishService</value>

</property>

<property name="portInterface">

<value>com.habuma.remoting.client.BabelFishRemote</value> w

</property>

<property name="namespaceUri">

<value>http://www.xmethods.net/sd/BabelFishService.wsdl</value> x

</property>

<property name="serviceName">

<value>BabelFishService</value> y

</property>

<property name="portName">

<value>BabelFishPort</value> z

</property>

<property name="serviceFactoryClass">

<value>org.apache.axis.client.ServiceFactory</value> {

</property>

</bean>

n

在JaxRpcPortProxyFactoryBean里设置的第一个属性是wsdlDocumentUrl u。这个属性告诉代理这个web service的WSDL的文档在哪里。

serviceInterface属性v定义了Babel Fish服务的客户端用来访问这个服务的接口。这里，它被设置为使用BabelFishService接口，如下面代码所定义的：

public interface BabelFishService {

public String BabelFish(String translationMode,

String sourceData);

}

BabelFishService接口非常像远程接口，它被设置到portInterface属性里w。它们的不同之处就是远程接口被视为一个RMI接口，必须从javax.rmi.Remote派生，而BabelFish()方法抛出javax.rmi.RemoteException。JaxRpcPortProxyFactoryBean在访问远程服务时使用BabelFishRemote接口。但是，任何RemoteException都会被抛出，代理将捕获这些异常，并把它们重新作为RemoteAccessException（运行时）抛出。这样，客户端就不用处理它们了。

接下来的3个属性被用来构造Babel Fish服务的限定名字（QName）和它的端口。namespaceUri属性x和serviceName属性y一起使用，用来构造服务的QName，同时也和portName属性z一起构造一个端口的QName。这3个字段的值都能通过查考WSDL定义中关于Babel Fish服务的内容来设置。

默认情况下，JaxRpcPortProxyFactoryBean使用javax.xml.rpc.ServiceFactory作为它的服务工厂。但你可以选择使用另外的服务工厂，比如Apache Axis的服务工厂，只要设置serviceFactoryClass属性{。

一旦在Spring配置文件中用这种方法配置好Babel Fish服务，你就可以像应用上下文中任何其他Bean一样来使用它。包括直接从应用上下文中取得它，或者把它作为一个协作者置入到另一个Bean的属性里。例如，用下面的代码把这个Bean从应用上下文中直接取出来：

ApplicationContext context =

new FileSystemXmlApplicationContext("babelFish.xml");

BabelFishService babelFish =

(BabelFishService) context.getBean(babelFish);

String translated = babelFish.BabelFish("en_es", "Hello World");

当前面这一段代码执行完后，translated变量将包含文本“Hola Mundo”，这是用西班牙语说的“Hello World”。

有趣的BABEL FISH

为了好玩，这里有些其他短语，你可以试着用Babel Fish服务把它翻译出来：

n 用“fr_en”翻译模式翻译“Qui a couple le fromage”。

n 用“es_en”翻译模式翻译“Mi perro is muy feo”。

n 用“de_en”翻译模式翻译“Ich habe eine socke voll der Zehen”。

n 用“es_en”翻译模式翻译“No me gusto a comer los cocos”。

n 用“fr_en”翻译模式翻译“Mon volleyball est mon meilleur ami”。


6.7 总结 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

6.7 小结

使用远程服务是典型的让人乏味的杂务。但Spring提供了远程服务的支持，让使用远程服务和使用常规的JavaBean一样简单。

在客户端，Spring提供了代理工厂Bean，能让你在Spring应用中配置远程服务。不管是使用RMI、Hessian、Burlap、HTTP invoker、EJB、还是Web service，你都可以把远程服务置入到你的应用里，好像它们是POJO一样。Spring甚至捕获了所有抛出的RemoteException，并在发生异常的地方重新抛出运行时RemoteAccessException，让你的代码从处理可能不可恢复的异常中解放出来。

Spring对服务端提供的支持是各种各样的。对于RMI、Hessian、Burlap、HTTP invoker服务，Spring提供了remote exporter，把Spring管理的Bean的功能作为远程服务公开出来，让另外的应用使用。尽管Spring不能让你把POJO作为EJB输出，但它提供了支撑类，让你的EJB访问Spring应用上下文成为可能。

即使Spring隐藏了远程服务的很多细节，让它们表现得好像是本地JavaBean一样，但你还是应该在脑子里牢记它们是远程服务的事实。远程服务，就它们的本质来说，通常比本地服务更低效。当在写访问远程服务的代码时，你要考虑这一点，限制远程调用，以避免性能瓶颈。

在下一章里，将学习如何使用Spring对几个企业服务的支持，包括JNDI、e-mail、调度和消息。


7.1 从JNDI中获取对象 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

本章内容：

n 访问JNDI资源

n 发送和格式化电子邮件

n 调度任务

n 与EJB集成

Spring并没有对一些企业服务提供直接的支持。它依赖其他API来提供有关服务，但对这些服务通过相应的抽象层进行了封装，因此使用起来更为方便。

你已经看到过一些Spring的抽象层了。在第4章中，你看到Spring是如何抽象JDBC和Hibernate的。除了能够避免编写一些样板式的代码，这些抽象层还使你不必捕捉受检查的异常。

在本章中，我们将会快速浏览一下Spring提供给若干企业服务的抽象层，包括Spring对以下企业服务的支持：

n Java命名和目录接口（JNDI）

n 电子邮件

n 调度

n Java消息服务（JMS）

鉴于JNDI提供了其他一些抽象层的基础，因此我们的讨论将从Spring对JNDI的支持开始。

7.1 从JNDI中获取对象

JNDI为Java应用程序提供了一个用于存储应用对象的中心仓库。例如，一个典型的J2EE应用使用JNDI来存储和获取诸如JDBC数据源和JTA事务管理器之类的对象。

但是，为什么要在JNDI而不是Spring中配置这些对象呢？当然，你可以在Spring配置文件中配置一个DataSource对象，但也许你更希望在应用服务器中配置该对象，从而可以利用服务器的数据库连接池。类似地，如果你的事务需求中要求JTA事务支持，你需要从应用服务器的JNDI仓库中获取一个JTA事务管理器。

Spring的JNDI抽象使你可以在应用的配置文件中声明JNDI对象。然后你就可以将这些对象装配到其他Bean的属性中，就如同JNDI对象与其他的POJO一样。让我们看一下怎样使用Spring的JNDI抽象来简化从JNDI中查找对象的工作。


7.1.1 使用传统的JNDI - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.1.1 使用传统的JNDI

在JNDI中查找对象是件枯燥乏味的事。例如，假设你需要从JNDI中获取一个javax.sql.DataSource对象。使用传统的JNDI API，你编写的代码也许和下面的看上去差不多：

InitialContext ctx = null;

try {

ctx = new InitialContext();

DataSource ds =

(DataSource) ctx.lookup("java:comp/env/jdbc/myDatasource");

} catch (NamingException ne) {

// handle naming exception

…

} finally {

if (ctx != null) {

try {

ctx.close();

} catch (NamingException ne) {}

}

}

初看起来，这算不上什么。但仔细观察，可以发现这段代码里有一些地方显得很笨拙：

n 你必须创建和关闭一个初始上下文，而这样做的惟一目的仅仅是查找一个DataSource对象。尽管看上去额外的代码并不多，但这些都是与你的应用代码的目标没有直接关系的辅助代码。

n 你必须捕捉，或者至少重新抛出一个javax.naming.NamingException。如果你选择捕捉这个异常，就必须恰当地处理它。如果你选择重新抛出这个异常，则会强制调用代码对它进行处理。最终在某个地方会有一个对象将不得不处理这个异常。

n 你的代码与JNDI查找紧紧地耦合在一起。其实，你只不过需要一个DataSource。它是否来自JNDI是无关紧要的。但如果你的代码中包括像之前给出的代码，你就限制自己必须从JNDI中获取DataSource了。

n 你的代码与某个特定的JNDI名字绑定了——在上面的例子中是java:comp/env/ jdbc/myDatasource。当然，你可以从某个属性文件中获取这个名字，但这样的话你就会加入更多辅助代码来从一个属性文件中获取JNDI名字。

总的说来，这种从JNDI中获取对象的传统方式的问题都来源于违背了依赖注入的原则。不是赋给你的代码一个对象，而是你的代码必须亲自去获取这个对象。这意味着你的代码所做的不是真正的工作。这也意味着你的代码不必要地与JNDI耦合了。

尽管如此，这并没有改变你有时会需要从JNDI中查找对象的事实。为了利用应用服务器的连接池，你经常在应用服务器中配置DataSource，由应用程序获取并使用它来访问数据库。如何才能既利用JNDI的所有优势，也得到依赖注入的所有好处呢？


7.1.2 代理JNDI对象 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.1.2 代理JNDI对象

Spring的JndiObjectFactoryBean允许你同时得到这两个不同领域中的好处。它是一个工厂Bean，这意味着当把它装配到一个属性上时，实际上它会创建一个其他类型的对象供装配时使用。对于JndiObjectFactoryBean，它实际装配的是一个从JNDI获取的对象。

为了演示这种工作机制，让我们回顾一下第4章（4.1.2节）的一个例子。在那里你使用JndiObjectFactoryBean从JNDI获取一个DataSource：

<bean id="dataSource"

class="org.springframework.jndi.JndiObjectFactoryBean"

singleton="true">

<property name="jndiName">

<value>java:comp/env/jdbc/myDatasource</value>

</property>

</bean>

属性jndiName指定了位于JNDI中的对象的名字。在这里JNDI全名是java:comp/ env/jdbc/myDatasource。然而，如果这个对象是一个Java资源，你可以省去java:comp/env/，从而能够以更简洁的方式指定这个名字。例如，以下jndiName属性的声明与前一个声明是等价的：

<property name="jndiName">

<value>jdbc/myDatasource</value>

</property>

当声明了dataSource Bean之后，可以把它注入到一个DataSource属性中。例如，可以使用以下方式来配置一个Hibernate会话工厂：

<bean id="sessionFactory" class="org.springframework.orm.

➥hibernate.LocalSessionFactoryBean">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

…

</bean>

当Spring装配sessionFactory Bean时，它会把从JNDI中获取的DataSource对象注入到会话工厂Bean的dataSource属性中。使用JndiObjectFactoryBean从JNDI中查找对象的最大优势在于惟一知道DataSource是从JNDI中获取的代码就是那段dataSource Bean的XML声明。sessionFactory Bean不知道（也不关心）DataSource来自哪里。这意味着如果以后你决定改从一个JDBC驱动管理器获取DataSource，只需要重新定义dataSource Bean为一个DriverManagerDataSource即可。

在本章的后续部分中我们会看到更多有关JNDI使用的内容。但首先让我们稍微转移一下话题，看一下Spring框架提供的另一个抽象层——Spring的电子邮件抽象层。


7.2 发送电子邮件 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.2 发送电子邮件

假如Spring培训的课程主任要求你每天给她发一封简要描述即将开始的课程的电子邮件，其中包括课程的座位数和参加的学生数。她希望每天早上6:00通过电子邮件发送这份报表给她，从而她上班之后就能看到这份报表。根据这份报表，她会为受欢迎的课程安排更多内容，并取消很少有人感兴趣的课程。

由于懒惰是任何程序员的伟大品质[1]，你决定自动发送这封电子邮件，从而你不必每天亲自产生这份报表。

要做到这一点，首先需要编写代码来发送电子邮件（你会在7.3节中调度它每天发送）。

首先，你需要一个邮件发送器，该邮件发送器由Spring的MailSender接口定义。邮件发送器抽象了某个特定的邮件实现。这样就使应用代码和实际使用的邮件实现之间没有耦合。Spring提供了这个接口的两个实现：

n CosMailSenderImpl——以Jason Hunter的Java Servlet Programming一书（O’Reilly，1998）中的COS（com.oreilly.servlet）实现为基础的SMTP邮件发送器的简单实现。

n JavaMailSenderImpl——一个基于JavaMail API的邮件发送器实现。允许发送MIME邮件以及非SMTP邮件（比如Lotus Notes）。

无论哪个MailSender实现都足以完成向课程主任发送报表的工作。但我们将选择JavaMailSenderImpl，因为在两者间它的功能更全面。你可以在Spring配置文件中按以下方式声明它：

<bean id="mailSender"

class="org.springframework.mail.javamail.JavaMailSenderImpl">

<property name="host">

<value>mail.springtraining.com</value>

</property>

</bean>

属性host指定了邮件服务器的主机名，在这里是Spring培训应用的SMTP服务器。在默认情况下，邮件发送器假设SMTP服务器监听25端口（标准的SMTP端口），但如果你的SMTP服务器监听在不同的端口上，可以使用JavaMailSenderImpl的port属性来指定端口。

上面的mailSender声明中显式地命名了用于发送邮件的邮件服务器。然而，如果你有一个位于JNDI中的javax.mail.MailSession对象（可能位于你的应用服务器中），则也可以选择从JNDI中获取它。只需简单地使用JndiObjectFactoryBean（如第7.1节中描述的）来获取邮件会话对象，并按照下面的方式将它装配到mailSender的mailSession属性中即可：

<bean id="mailSession"

class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName">

<value>java:comp/env/mail/Session</value>

</property>

</bean>

<bean id="mailSender"

class="org.springrframework.mail.javamail.JavaMailSenderImpl">

<property name="session"><ref bean="mailSession"/></property>

</bean>

现在邮件发送器已经设置完成，可以发送邮件了。但你也许需要声明一个邮件消息模板。

<bean id="enrollmentMailMessage"

class="org.springframework.mail.SimpleMailMessage">

<property name="to">

<value>coursedirector@springtraining.com</value>

</property>

<property name="from">

<value>system@springtraining.com</value>

</property>

<property name="subject">

<value>Course enrollment report</value>

</property>

</bean>

声明一个邮件消息模板不是必需的。你也可以通过每次创建一个SimpleMailMessage的新实例来发送邮件。但通过在Spring配置文件中声明一个模板，你可以不必在Java代码中硬编码邮件地址或主题。

接下来要做的事是CourseServiceImpl中增加一个mailSender属性，以便CourseServiceImpl能够用它来发送邮件。类似地，如果你声明了一个邮件模板，你需要增加一个message属性来保持对邮件模板Bean的引用：

public class CourseServiceImpl implements CourseService {

…

private MailSender mailSender;

public void setMailSender(MailSender mailSender) {

this.mailSender = mailSender;

}

private SimpleMailMessage mailMessage;

public void setMailMessage(SimpleMailMessage mailMessage) {

this.mailMessage = mailMessage;

}

…

}

现在CourseServiceImpl有一个MailSender和一个邮件模板的拷贝了。你可以编写用于向课程主任发送电子邮件的sendCourseEnrollmentReport()方法（见程序清单7.1）了。（不要忘了在CourseService接口中增加一个sendCourseEnrollmentReport()方法的声明）。

  程序清单7.1 发送注册报表邮件

public void sendCourseEnrollmentReport() {

Set courseList = courseDao.findAll();

SimpleMailMessage message =

new SimpleMailMessage(this.mailMessage);

StringBuffer messageText = new StringBuffer();

messageText.append(

"Current enrollment data is as follows:\n\n");

for(Iterator iter = courseList.iterator(); iter.hasNext(); ) {

Course course = (Course) iter.next();

messageText.append(course.getId() + " ");

messageText.append(course.getName() + " ");

int enrollment = courseDao.getEnrollment(course);

messageText.append(enrollment);

}

message.setText(messageText.toString());

try {

mailSender.send(message);

} catch (MailException e) {

LOGGER.error(e.getMessage());

}

}

sendCourseEnrollmentReport()方法首先使用CourseDao获取所有课程。接着，它创建一个邮件模板的工作拷贝，从而原始的邮件模板不会被修改。然后，它构造消息体，并且设置消息的正文。最后，它使用mailSender属性来发送邮件。

要做的最后一件事是将mailSender和enrollmentMailMessage Bean装配到courseService Bean中：

<bean id="courseService"

class="com.springinaction.training.service.CourseServiceImpl">

…

<property name="mailMessage">

<ref bean="enrollmentMailMessage"/>

</property>

<property name="mailSender">

<ref bean="mailSender"/>

</property>

</bean>

现在，courseService Bean已经拥有发送注册报表邮件所需的全部资源，工作已经完成了一半了。剩下的只是调度它每天向课程主任发送这份报表。如果Spring也有办法能帮助我们调度任务，那就太好了……


7.3 调度任务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.3 调度任务

并非应用系统中发生的所有事情都是由用户的动作引起的。有时候，系统自己也会发起一个动作。

例如，系统应该每天向课程主任发送注册报表邮件。要实现这一点，你有两个选择：或者你每天早上早早上班通过电子邮件人工发送报表，或者你让应用系统按照预定的计划自动发送邮件。（我们知道你会选择哪种方法。）

Java的Timer类和OpenSymphony的Quartz调度器[2]是两个流行的调度API。Spring为这两个调度器提供了一个抽象层，使你可以更容易地使用它们。让我们看一下这两个抽象层，首先从简单的Java Timer类开始。


7.3.1 使用Java Timer调度任务 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.3.1 使用Java Timer调度任务

从Java 1.3开始，Java SDK就通过java.util.Timer类提供了基本的调度功能。这个类允许你调度一个任务（通过java.util.TimerTask子类定义）按任意周期运行。

创建一个定时器任务

使用Java Timer来调度发送注册报表邮件的第一步是从java.util.TimerTask中派生出邮件任务，如程序清单7.2所示。

  程序清单7.2 一个用于发送注册报表邮件的定时器任务

public class EmailReportTask extends TimerTask {

public EmailReportTask() {}

public void run() {

courseService.sendCourseEnrollmentReport();

}

private CourseService courseService;

public void setCourseService(CourseService courseService) {

this.courseService = courseService;

}

}

n

run()方法定义了当任务运行时该做什么。在上面的例子中，它调用CourseService的sendCourseEnrollmentReport()方法（见程序清单7.1）来发送注册报表邮件。CourseService是通过依赖注入方式提供给EmailReportTask的。

按以下方式在Spring配置文件中声明EmailReportTask：

<bean id="reportTimerTask"

class="com.springinaction.training.schedule.EmailReportTask">

<property name="courseService">

<ref bean="courseService"/>

</property>

</bean>

这个声明本身只是将EmailReportTask放到应用上下文中，并在courseService属性中装配courseService Bean。在你调度它之前，它不会做任何有用的事。

调度定时器任务

Spring的ScheduledTimerTask定义了一个定时器任务的运行周期。既然课程主任要求每天向她发送注册报表，你应该以如下方式装配一个ScheduledTimerTask：

<bean id="scheduledReportTask"

class="org.springframework.scheduling.timer.ScheduledTimerTask">

<property name="timerTask">

<ref bean="reportTimerTask"/>

</property>

<property name="period">

<value>86400000</value>

</property>

</bean>

属性timerTask告诉ScheduledTimerTask运行哪个TimerTask。在这里，该属性装配了指向reportTimerTask的一个引用，它就是EmailReportTask。属性period告诉ScheduledTimerTask以怎样的频度调用TimerTask的run()方法。这个属性以毫秒作为单位，它被设置为86400000，指定这个任务应该每24小时运行一次。

启动定时器

最后一步是启动定时器。Spring的TimerFactoryBean负责启动定时任务。按以下方式在Spring配置文件中声明它：

<bean class="org.springframework.scheduling.timer.TimerFactoryBean">

<property name="scheduledTimerTasks">

<list>

<ref bean="scheduledReportTask"/>

</list>

</property>

</bean>

属性scheduledTimerTasks要求一个需要启动的定时器任务的列表。既然你现在只有一个定时器任务，这个列表中只包含一个指向scheduledReportTask Bean的引用。

遗憾的是，即使这个任务已经能够每隔24小时运行一次了，在这里你无法指定它应该在一天中的哪个时间点执行。ScheduledTimerTask有一个delay属性，允许你指定当任务第一次运行之前应该等待多久。例如，要将EmailReportTask的第一次运行延迟1小时，可以按照以下方式进行配置：

<bean id="scheduledReportTask"

class="org.springframework.scheduling.timer.ScheduledTimerTask">

<property name="timerTask">

<ref bean="reportTimerTask"/>

</property>

<property name="period">

<value>86400000</value>

</property>

<property name="delay">

<value>3600000</value>

</property>

</bean>

即使使用deplay属性，EmailReportTask的第一次运行时间仍然是相对于应用程序的启动时间的。怎样才能做到如课程主任所要求的在每天早晨6:00发送邮件（而不是在早晨5:00启动应用程序）呢？

遗憾的是，这是Java Timer的一个局限性。你可以指定任务执行的频度，但你无法精确指定它何时运行。为了能够精确指定何时发送电子邮件，你需要使用Quartz调度器。


7.3.2 使用Quartz调度器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.3.2 使用Quartz调度器

Quartz调度器为调度工作提供了更丰富的支持。和Java定时器一样，可以使用Quartz来每隔多少毫秒执行一个工作。但Quartz比Java Timer更先进之处在于它允许你调度一个工作在某个特定的时间或日期执行。

关于Quartz的更多信息，可以访问Quartz位于http://www.opensymphony.com/quartz的主页。

让我们从定义发送报表邮件的工作开始使用Quartz：

创建一个工作

定义Quartz工作的第一步是创建一个类来定义工作。要做到这一点，你需要从Spring的QuartzJobBean中派生子类，如程序清单7.3所示：

  程序清单7.3 定义一个Quartz工作

public class EmailReportJob extends QuartzJobBean {

public EmailReportJob() {}

protected void executeInternal(JobExecutionContext context)

throws JobExecutionException {

courseService.sendCourseEnrollmentReport();

}

private CourseService courseService;

public void setCourseService(CourseService courseService) {

this.courseService = courseService;

}

}

n

QuartzJobBean是Quartz中与Java的TimerTask等价的类。它实现了org.quartz.Job接口。executeInternal()方法定义了当预定的时刻来临时应该执行哪些动作。在这里，正如EmailReportTask，你只是简单地调用了courseService属性的sendCourseEnrollmentReport()方法。

在Spring配置文件中按以下方式声明这个工作：

<bean id="reportJob"

class="org.springframework.scheduling.quartz.JobDetailBean">

<property name="jobClass">

<value>com.springinaction.training.

➥schedule.EmailReportJob</value>

</property>

<property name="jobDataAsMap">

<map>

<entry key="courseService">

<ref bean="courseService"/>

</entry>

</map>

</property>

</bean>

值得注意的是，在这里你并没有直接声明一个EmailReportJob Bean，而是声明了一个JobDetailBean。这是使用Quartz时的一个特点。JobDetailBean是Quartz的org.quartz.JobDetail的子类，它要求通过jobClass属性来设置一个Job对象。

使用Quartz的JobDetail中的另一个特别之处是EmailReportJob的courseService属性是间接设置的。JobDetail的jobDataAsMap属性接受一个java.util.Map，其中包含了需要设置给jobClass的各种属性。在这里，这个map包含了一个指向courseService Bean的引用，它的键值为courseService。当JobDetailBean实例化时，它会将courseService Bean注入到EmailReportJob的courseService属性中。

调度工作

现在工作已经被定义好了，接下来你需要调度这个工作。Quartz的org.quartz.Trigger类描述了何时及以怎样的频度运行一个Quartz工作。Spring提供了两个触发器，SimpleTriggerBean和CronTriggerBean。你应该使用哪个触发器？让我们分别考察一下这两个触发器，首先从SimpleTriggerBean开始。

SimpleTriggerBean与ScheduledTimerTask类似。你可以用它来指定一个工作应该以怎样的频度运行，以及（可选地）在第一次运行工作之前应该等待多久。例如，要调度报表工作每24小时运行一次，第一次在1小时之后开始运行，可以按照以下方式进行声明：

<bean id="simpleReportTrigger"

class="org.springframework.scheduling.quartz.SimpleTriggerBean">

<property name="jobDetail">

<ref bean="reportJob"/>

</property>

<property name="startDelay">

<value>3600000</value>

</property>

<property name="repeatInterval">

<value>86400000</value>

</property>

</bean>

属性jobDetail装配了将要被调度的工作，在这个例子中是reportJob Bean。属性repeatInterval告诉触发器以怎样的频度运行这个工作（以毫秒作为单位）。这里，我们设置它为86400000，因此每隔24小时它会被触发一次。你也可以选择设置startDelay属性来延迟工作的第一次执行。我们设置它为3600000，因此在第一次触发之前它会等待1小时。

调度一个cron工作

尽管你可能认为SimpleTriggerBean适用于大多数应用，但它仍然不能满足发送注册报表邮件的需求。正如ScheduledTimerTask，你只能指定工作执行的频度，而不能准确指定它于何时运行。因此，你无法使用SimpleTriggerBean在每天早晨6:00给课程主任发送注册报表邮件。

然而，CronTriggerBean允许你更精确地控制任务的运行时间。如果你对Unix的cron工具很熟悉，则会觉得CronTriggerBean很亲切。你不是定义工作的执行频度，而是指定工作的准确运行时间（和日期）。例如，要在每天早上6:00运行报表工作，可以按照以下方式声明一个CronTriggerBean：

<bean id="cronReportTrigger"

class="org.springframework.scheduling.quartz.CronTriggerBean">

<property name="jobDetail">

<ref bean="reportJob"/>

</property>

<property name="cronExpression">

<value>0 0 6 * * ?</value>

</property>

</bean>

和SimpleTriggerBean一样，jobDetail属性告诉触发器调度哪个工作。这里我们又一次装配了一个reportJob Bean。属性cronExpression告诉触发器何时触发。如果你不熟悉cron，这个属性可能看上去有点神秘，因此让我们进一步考察一下这个属性。

一个cron表达式有至少6个（也可能是7个）由空格分隔的时间元素。从左至右，这些元素的定义如下：

1．秒（0–59）

2．分钟（0–59）

3．小时（0–23）

4．月份中的日期（1–31）

5．月份（1–12或JAN–DEC）

6．星期中的日期（1–7或SUN–SAT）

7．年份（1970–2099）

每一个元素都可以显式地规定一个值（如6），一个区间（如9-12），一个列表（如9，11，13）或一个通配符（如*）。“月份中的日期”和“星期中的日期”这两个元素是互斥的，因此应该通过设置一个问号（？）来表明你不想设置的那个字段。表7.1中显示了一些cron表达式的例子和它们的意义：

表7.1 一些cron表达式的例子

表 达 式 | 意 义
0 0 10,14,16 * * ? | 每天上午10点，下午2点和下午4点
0 0,15,30,45 * 1-10 * ? | 每月前10天每隔15分钟
30 0 0 1 1 ? 2012 | 在2012年1月1日午夜过30秒时
0 0 8-5 ? * MON-FRI | 每个工作日的工作时间

对于cronReportTrigger，我们设置cronExpression为0 0 6 * * ?可以把它读作“在任何月份任何日期（不管是星期几）的6时0分0秒执行触发器。”换句话说，这个触发器会在每天早晨6:00执行。

使用CronTriggerBean完全能够满足课程主任的期望了。现在剩下要做的只是启动这个工作了。

启动工作

Spring的SchedulerFactoryBean是Quartz中与TimerFactoryBean等价的类。按照如下方式在Spring配置文件中声明它：

<bean class="org.springframework.scheduling.

➥quartz.SchedulerFactoryBean">

<property name="triggers">

<list>

<ref bean="cronReportTrigger"/>

</list>

</property>

</bean>

属性triggers接受一组触发器。由于目前只有一个触发器，因此只需简单地装配一个包含cronReportTrigger Bean的一个引用的列表即可。

现在，你已经实现了调度发送注册报表邮件的需求。但在这个过程中，你做了一些额外的工作。在开始新的话题之前，首先让我们看一下如何通过更简单一些的方式调度报表邮件。


7.3.3 按调度计划调用方法 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.3.3 按调度计划调用方法

为了调度报表邮件，你不得不编写EmailReportJob Bean（或者在使用定时器任务的情况下，是EmailReportTask Bean）。但这个Bean只是简单地调用了一次CourseService的sendCourseEnrollmentReport()方法。以这一点而论，EmailReportTask和EmailReportJob看上去都有些空洞。如果你不用写这个额外的类就能指定调用sendCourseEnrollmentReport()方法，是不是很棒？

好消息！你能够不用编写一个单独的TimerTask或QuartzJobBean类就可以调度单次方法调用。要达到这一目的，Spring提供了MethodInvokingTimerTaskFactoryBean和MethodInvokingJobDetailFactoryBean，可以分别使用Java的定时器支持或Quartz调度器对方法调用进行调度。

例如，要使用Java的定时器服务调度一个对sendCourseEnrollmentReport()方法的调用，可以按照以下方式重新声明scheduledReportTask Bean：

<bean id="scheduledReportTask"

class="org.springframework.scheduling.timer.

➥MethodInvokingTimerTaskFactoryBean">

<property name="targetObject">

<ref bean="courseService"/>

</property>

<property name="targetMethod">

<value>sendCourseEnrollmentReport</value>

</property>

</bean>

在幕后，MethodInvokingTimerTaskFactoryBean创建了一个TimerTask来对targetObject属性指定的对象调用targetMethod属性指定的方法。这与EmailReportTask的效果是一样的。

使用根据以上方式声明的scheduledReportTask，你现在能够去掉EmailReportTask类以及声明它的reportTimerTask Bean了。

当使用ScheduledTimerTask时，MethodInvokingTimerTaskFactoryBean可以很好地完成简单的单次方法调用。但你现在是使用Quartz的CronTriggerBean在每天早晨6:00发送报表邮件。因此，你不是使用MethodInvokingTimerTaskFactoryBean，而是按以下方式重新声明reportJob Bean：

<bean id="courseServiceInvokingJobDetail"

class="org.springframework.scheduling.quartz.

MethodInvokingJobDetailFactoryBean">

<property name="targetObject">

<ref bean="courseService"/>

</property>

<property name="targetMethod">

<value>sendCourseEnrollmentReport</value>

</property>

</bean>

MethodInvokingJobDetailFactoryBean是Quartz中与MethodInvokingTimerTaskFactoryBean等价的类。在幕后，它是通过创建一个Quartz JobDetail对象来调用一次由targetObject和targetMethod属性指定的对象和方法。以这种方式使用MethodInvokingJobDetailFactoryBean，你就能去掉空洞的EmailReportJob类了。


7.4 使用JMS发送消息 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.4 使用JMS发送消息

软件系统中的大多数操作是同步进行的。换句话说，当一个子程序被调用时，程序的控制流被转交给那个子程序来完成它的功能。完成之后，控制流返回到调用者子程序并且程序继续。图7.1展示了这一点。

图7.1 同步程序控制流

但在某些时候，没有必要（甚至不希望）等待被调用的子程序完成处理。例如，如果子程序很慢，更理想的做法是向子程序发一个消息，然后只是假设那个子程序会处理该消息，或者在以后的某个时间去检查它的进度。

当你向一个子程序发送消息而且不等待结果返回，这称为异步。异步程序控制流在图7.2中显示。

图7.2 异步程序控制流

Java消息服务（JMS）是一个用于异步处理的Java API。JMS支持两种类型的消息：点对点和发布-订阅。

一个点对点的消息被消息生产者放入一个消息队列中，并在以后某个时刻被消息消费者从队列中取出。一旦消息从队列中取走，对于其他监听在这个队列上的消息消费者来说它就不存在了。这意味着尽管多个消费者可能同时观察一个队列，但每个点对点消息只会被一个消费者使用。

发布-订阅模型会让你联想起杂志发行商将发行物的拷贝送到多个订阅者手中。事实上，这是发布-订阅消息工作机制的一个很好的类比。多个消费者订阅一个消息主题。当一个消息生产者向该主题发布一个消息时，所有的订阅者都能够获得该消息并得到一个处理它的机会。

Spring提供了JMS的一个抽象层，使得访问一个消息队列或主题（抽象地称为一个目标）并向目标发布消息成为一件简单的事。而且，Spring以非检查的org.springframework.jms. JmsException的形式重新抛出JMS异常，使你的应用不必处理javax.jms.JMSException。

让我们看一下如何应用Spring的JMS抽象层。


7.4.1 使用JMS模板发送消息 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.4.1 使用JMS模板发送消息

在第6章中，你学到了如何使用Spring对远程调用的支持来针对Spring培训支付服务进行信用卡授权。现在你将要进行账户结算和接受支付了。

当对支付进行授权时，必须等待信用卡处理器的响应，因为你需要知道信用卡的发卡行是否授权进行支付。但当已经拥有恰当的授权之后，支付的结算可以以异步的方式进行。在这种情况下，不需要等待响应——你可以安全地假设支付将会被结算。

信用卡处理系统接受一个通过JMS发送的异步消息用于支付的结算。它接受的消息是一个javax.jms.MapMessage，其中包含了以下字段：

n authCode——从信息卡处理器得到的授权码；

n creditCardNumber——信用卡号；

n customerName——信用卡持有人的姓名；

n expirationMonth——信用卡过期的月份；

n expirationYear——信用卡过期的年份。

Spring采用回调方式处理JMS消息。这种回调方式让人回想起第4章中描述的JDBC回调机制。回调机制由两部分组成：一个消息创建器负责构造一个JMS消息（javax.jms.Message）和一个真正发送消息的JMS模板。

使用模板

要做的第一件事是给PaymentServiceImpl类配上一个JmsTemplate属性：

private JmsTemplate jmsTemplate;

public void setJmsTemplate(JmsTemplate jmsTemplate) {

this.jmsTemplate = jmsTemplate;

}

jmsTemplate属性会通过setter注入方式装配一个org.springframework.jms.core. JmsTemplate的实例。我们稍候将向你演示如何装配该实例。首先，让我们实现发送结算消息的服务层方法。

PaymentServiceImpl需要一个sendSettlementMessage()方法来向信用卡处理器发送结算消息。程序清单7.4显示了sendSettlementMessage()方法如何使用JmsTemplate来发送该消息。（PaySettlement参数是一个简单的JavaBean，包含了消息需要的所有字段。）

  程序清单7.4 使用JMS回调类来发送一个支付结算消息

public void sendSettlementMessage(final PaySettlement settlement) {

jmsTemplate.send(

new MessageCreator() {

public Message createMessage(Session session)

throws JMSException {

MapMessage message = session.createMapMessage();

message.setString("authCode",

settlement.getAuthCode());

message.setString("customerName",

settlement.getCustomerName());

message.setString("creditCardNumber",

settlement.getCreditCardNumber());

message.setInt("expirationMonth",

settlement.getExpirationMonth());

message.setInt("expirationYear",

settlement.getExpirationYear());

return message;

}

}

);

}

n

sendSettlementMessage()方法使用JmsTemplate的send()方法来发送消息。这个方法接受一个org.springframework.jms.core.MessageCreator的实例，在这里是作为一个匿名的内部类来定义的，其中构造了将被发送的消息。在这种情况下，消息是一个javax.jms.MapMessage。要构造一个消息，MessageCreator从PaySettlement Bean的属性中取得值，并使用这些值来设置MapMessage的相应字段。

装配模板

现在你必须将一个JmsTemplate装配到PaymentServiceImpl中。下面一段来自Spring配置文件的XML将做到这一点：

<bean id="paymentService"

class="com.springinaction.training.service.PaymentServiceImpl">

…

<property name="jmsTemplate">

<ref bean="jmsTemplate"/>

</property>

<bean>

jmsTemplate Bean的声明如下：

<bean id="jmsTemplate"

class="org.springframework.jms.core.JmsTemplate">

<property name="connectionFactory">

<ref bean="jmsConnectionFactory"/>

</property>

<property name="defaultDestination">

<ref bean="destination"/>

</property>

</bean>

注意，jmsTemplate Bean装配了一个JMS连接工厂和一个默认的目标。属性connectionFactory是一个必需的属性，因为JmsTemplate是通过它从JMS提供者那里得到一个连接的。在Spring培训应用中，连接工厂是从JNDI中取得的，如下面的connectionFactory Bean的声明所示：

<bean id="jmsConnectionFactory"

class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName">

<value>connectionFactory</value>

</property>

</bean>

通过以这种方式装配，Spring会使用JndiObjectFactoryBean（见第7.1节）从JNDI中查找一个名为java:comp/env/connectionFactory的连接工厂对象。（当然，这里假设你有一个在JNDI中注册了JMSConnectionFactory实例的JMS实现。）

属性defaultDestination定义了默认的用于消息发布的JMS目标（一个javax.jms.Destination的实例）。在这个例子中，它被装配了一个destination Bean的引用。正如connectionFactory Bean，destination Bean也是使用JndiObjectFactoryBean从JNDI中获取的：

<bean id="destination"

class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName">

<value>creditCardQueue</value>

</property>

</bean>

属性defaultDestination不是必需的。但既然只有一个用于信用卡消息的目标，为方便起见就在这里设置这个目标了。如果你没有设置默认的目标，则必须在调用JmsTemplate的send()方法时传递一个Destination的实例或者一个Destination的JNDI名字。例如，可以使用以下方式在调用send()方法时指定一个JMS目标的JNDI名字：

jmsTemplate.send(

"creditCardQueue", new MessageCreator() { … });

使用JMS 1.0.2

直到现在，jmsTemplate Bean都是作为一个JmsTemplate的实例声明的。尽管看上去并不明显，这里包括的一个隐含要求是JMS提供者的实现必须符合JMS规范1.1版。如果你的JMS实现是符合1.0.2版而非1.1版规范的，则你应该使用JmsTemplate102而不是JmsTemplate。

JmsTemplate和JmsTemplate102最大的不同之处在于JmsTemplate102需要知道你使用的是点对点还是发布-订阅消息。默认地，JmsTemplate102假设你使用点对点消息，但你可以通过设置pubSubDomain属性为true来指定使用发布-订阅消息：

<bean id="jmsTemplate"

class="org.springframework.jms.core.JmsTemplate">

…

<property name="pubSubDomain">

<value>true</value>

</property>

</bean>

除了这一点不同之外，你使用JmsTemplate102的方式与使用JmsTemplate完全一样。

处理JMS异常

需要注意的关于使用JmsTemplate的一个重要之处是你不必捕捉javax.jms.JMSException异常。JmsTemplate的大多数方法（包括send()）捕捉任何抛出的JMSException，并将它转换为一个非检查的运行时间异常org.springframework.jms.JmsException。


7.4.2 消费消息 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.4.2 消费消息

现在假设你在编写结算过程的接收端代码。你需要接收消息，将它转换成一个PaySettlement对象，然后把它传递到处理过程中。幸运的是，JmsTemplate既可以用于发送消息，也可以用于接收消息。

程序清单7.5演示了如何使用JmsTemplate来接收一个结算消息。

  程序清单7.5 接收一个PaySettlement消息

public PaySettlement processSettlementMessages() {

Message msg = jmsTemplate.receive("creditCardQueue");

try {

MapMessage mapMessage = (MapMessage) msg;

PaySettlement paySettlement = new PaySettlement();

paySettlement.setAuthCode(mapMessage.getString("authCode"));

paySettlement.setCreditCardNumber(

mapMessage.getString("creditCardNumber"));

paySettlement.setCustomerName(

mapMessage.getString("customerName"));

paySettlement.setExpirationMonth(

mapMessage.getInt("expirationMonth"));

paySettlement.setExpirationYear(

mapMessage.getInt("expirationYear"));

return paySettlement;

} catch (JMSException e) {

throw JmsUtils.convertJmsAccessException(e);

}

}

n

JmsTemplate的receive()方法尝试从指定的目标接受一个消息。根据之前在Spring配置文件中的声明，receive()方法会试图从一个JNDI名字为creditCardQueue的目标中接收一个消息。

一旦接收到消息，它会将它强制转换成一个MapMessage，并使用MapMessage中字段的值来初始化一个PaySettlement对象。

在默认情况下，receive()方法会无限期地等待消息。然而，你可能不希望让你的应用在等待接收消息时无限期地阻塞。最好能够设置一个超时间隔，使receive()方法在等待一定时间后放弃。幸运的是，可以通过设置jmsTemplate Bean的receiveTimeout属性来指定这个超时。例如：

<bean id="jmsTemplate"

class="org.springframework.jms.core.JmsTemplate">

<property name="receiveTimeout">

<value>10000</value>

</property>

</bean>

属性receiveTimeout以一个消息等待时间的毫秒数作为参数。设置它为10000，这规定了receive()方法会在10秒后放弃。如果在10秒中内没有收到任何消息，JmsTemplate会抛出一个非检查的JmsException（你可以选择捕捉或忽略它）。


7.4.3 转换消息 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.4.3 转换消息

在程序清单7.4中，MessageCreator实例负责将PaySettlement的属性映射到MapMessage的字段中。程序清单7.5的processSettlement()方法进行逆向转换，将一个消息转换为一个PaySettlement对象。这种方式工作得很好，但是会造成大量的映射代码，并且这些映射代码最终可能重复出现在所有你需要发送和接收PaySettlement消息的地方。

为了避免这种重复，并使发送和接收代码看起来更清晰，理想的做法是将这段映射代码提取出来放到一个单独的工具对象中。

转换PaySettlement消息

尽管你可以编写自己的工具对象来处理消息转换，Spring的org.springframework.jms.support. converter.MessageConverter接口定义了一个公共的机制在JMS Message对象和对象间进行相互转换。

为了演示这一点，PaySettlementConverter（程序清单7.6）实现了MessageConverter，使得在PaySettlement对象与JMS Message对象间的相互转换更方便。

  程序清单7.6 在JMS Message和PaySettlement间相互转换

public class PaySettlementConverter implements MessageConverter {

public PaySettlementConverter() {}

public Object fromMessage(Message message)

throws MessageConversionException {

MapMessage mapMessage = (MapMessage) message;

PaySettlement settlement = new PaySettlement();

try {

settlement.setAuthCode(mapMessage.getString("authCode"));

settlement.setCreditCardNumber(

mapMessage.getString("creditCardNumber"));

settlement.setCustomerName(

mapMessage.getString("customerName"));

settlement.setExpirationMonth(

mapMessage.getInt("expirationMonth"));

settlement.setExpirationYear(

mapMessage.getInt("expirationYear"));

} catch (JMSException e) {

throw new MessageConversionException(e.getMessage());

}

return settlement;

}

public Message toMessage(Object object, Session session)

throws JMSException, MessageConversionException {

PaySettlement settlement = (PaySettlement) object;

MapMessage message = session.createMapMessage();

message.setString("authCode", settlement.getAuthCode());

message.setString("customerName",

settlement.getCustomerName());

message.setString("creditCardNumber",

settlement.getCreditCardNumber());

message.setInt("expirationMonth",

settlement.getExpirationMonth());

message.setInt("expirationYear",

settlement.getExpirationYear());

return message;

}

}

n

由名字可知，fromMessage()方法接受一个Message对象，并将它转换为某个其他对象。在我们的例子中，通过从MapMessage中取出所有的字段并设置到PaySettlement对象的属性中，将Message对象转换为一个PaySettlement对象。

上述转换过程在toMessage()方法中被逆向执行。该方法接受一个对象（在这个例子中假设为一个PaySettlement Bean），并且使用对象的属性来设置MapMessage中的元素。

装配一个消息转换器

要使用消息转换器，首先必须在Spring配置文件中将它作为一个Bean加以声明：

<bean id="settlementConverter" class="com.springinaction.

➥training.service.PaySettlementConverter">

…

</bean>

接着需要让JmsTemplate知道消息转换器。你是通过将PaySettlementConverter装配到JmsTemplate的messageConverter属性中让JmsTemplate知道它的：

<bean id="jmsTemplate"

class="org.springframework.jms.core.JmsTemplate">

…

<property name="messageConverter">

<ref bean="settlementConverter"/>

</property>

</bean>

现在，JmsTemplate知道PaySettlementConverter了，你已经准备好发送由PaySettlement对象转换成的消息了。

发送和接收已转换的消息

在JmsTemplate中装配了一个消息转换器之后，sendSettlementMessage()方法的实现被大大简化了：

public void sendSettlementMessage(PaySettlement settlement) {

jmsTemplate.convertAndSend(settlement);

}

在这里，不是调用JmsTemplate的send()方法并使用MessageCreator来构造一个Message对象，而是简单地调用JmsTemplate的convertAndSend()方法并传入一个PaySettlement对象。在幕后，convertAndSend()方法创建自己的MessageCreator实例，使用PaySettlementConverter从PaySettlement对象创建一个Message对象。

类似地，要接收转换后的消息，你调用JmsTemplate的receiveAndConvert()方法（而不是receive()方法），并传入JMS消息队列的名字：

PaySettlement settlement = (PaySettlement)

jmsTemplate.receiveAndConvert("creditCardQueue");

除了自动将Message对象转换为一个应用对象之外，receiveAndConvert()的语义和receive()完全一样。

使用SimpleMessageConverter

Spring提供了一个现成的MessageConverter接口的实现。SimpleMessageConverter在MapMessage、TextMessage以及ByteMessage和java.util.Map集合对象、String以及byte数组之间分别进行相互转换。

要使用SimpleMessageConverter在PaySettlement对象和JMS Message间来回转换，可以使用以下声明来替换原先的settlementConverter Bean声明：

<bean id="settlementConverter" class="org.springframework.jms.

➥support.converter.SimpleMessageConverter">

…

</bean>

尽管这个转换器的功能相当简单，当你的消息非常简单而且和应用领域内的对象没有直接对应关系时，它将是十分有用的。


7.5 小结 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

7.5 小结

尽管Spring提供的功能免去了大多数的EJB使用需求，仍有很多企业服务在Spring中没有直接的替代实现。在那些情况下，Spring提供了各种抽象层，使你能够容易地将这些企业服务装配到你的Spring应用程序中。

在本章中，你已经看到如何取得保存在JNDI中的对象的引用。这些引用可以像本地定义的Bean一样装配到Bean的属性中。本章中处处体现了这种方式的实用性，比如使用Spring的JNDI抽象层来查询诸如邮件会话和JMS连接工厂等等JNDI对象。

你也看到如何通过Spring的电子邮件抽象层来发送电子邮件，并使用Java Timer或者OpenSymphony的Quartz调度器来调度任务。

最后，你看到如何通过Spring的JMS抽象层来发送和接收异步消息。

在下一章中，我们会将关注的焦点转移到应用的表示层，学习如何使用Spring的MVC框架来开发Web应用程序。


9.1 使用Velocity模板 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

本章内容：

n 使用Velocity模板

n 与FreeMarker集成

n 使用Jakarta Tiles

n 生成PDF和Excel文件

在1908年10月，Henry Ford推出了“巅峰之车”：T型福特。它的标价为950美元。为了加快组装速度，所有的T型福特都漆成黑色，因为黑色的漆干起来最快。传言说Henry Ford曾说过“任何顾客都能获取一辆他想要的任何颜色的轿车，只要它是黑色的”。[1]

从1908年至今，汽车已经进步了许多。除了令人目眩的车型之外，你也能够有其他许多选择，包括音响类型，是否有电动车窗和门锁，以及布或真皮的内饰。而且，现在任何顾客都能真正拥有他想要的颜色，包括但不仅限于黑色。

在第8章，我们已经向你展示了如何使用Spring MVC和JSP在你的应用之上创建Web层。当然，Spring MVC和JSP是构建Web应用的强大组合和坚固平台。但这是否是Spring开发人员可以拥有的惟一选择？

尽管JSP通常用于生成基于Java的Web应用系统的视图，但JSP并不是每个人的选择。早在JSP刚推出的时候，就有许多开发人员在JSP无法满足他们的期望时，转而采用其他模板解决方案，比如Jakarta Velocity和FreeMarker。尽管JSP在最近几年通过增加对定制标签库的支持而得到成长，并且实质上消除了对脚本代码的依赖，但许多过去曾避之唯恐不及的开发者仍旧偏爱其他的选择。

JSP也有它的局限性。JSP主要是为Web应用系统生成HTML和XML输出的。但另一方面，Velocity和FreeMarker能够生成足够灵活的内容，实质上可以生成任何类型的文本文件。JSP无法生成诸如微软Excel工作表、Adobe PDF文档或图片等二进制内容。

即使你喜欢JSP，你也可能想将JSP页面放到一个布局框架中，比如Jakarta Tiles，从而使你的页面看起来更美观。

你将会发现，Spring MVC能够灵活地生成各种内容。如果你不是JSP的狂热爱好者，你也许会乐于知道Spring提供了各种视图解析器，使你能够使用Velocity和FreeMarker。如果你需要生成动态创建的二进制内容，Spring支持在Spring MVC中生成Excel工作表、PDF文档。

在本章中，我们会向你展示如何配置Spring MVC，可以

n 使用Velocity或FreeMarker模板，而不是JSP；

n 使用Jakarta Tiles来布局你的应用页面；

n 生成动态创建的二进制Excel工作表、PDF文档以及图片。

让我们开始考察如何将JSP换成其他视图层语言，从Velocity开始吧。

9.1 使用Velocity模板

Velocity是一种针对Java应用的易用的模板语言。Velocity模板中没有任何Java代码，这使得它能够同时被非开发人员和开发人员轻松地理解。Velocity的用户手册上是这么说的：“Velocity将Java代码从Web页面中分离出来，使用Web站点从长远看更容易维护，并且提供了一种可行的JavaServer Pages替代解决方案。”

除了JSP，Velocity可能是用于Web应用的最流行的模板语言。因此很可能你会想采用Velocity作为视图层技术开发基于Spring的应用。幸运地是，Spring支持将Velocity作为Spring MVC的视图层模板语言。

让我们通过基于Velocity重新实现Spring培训应用中的视图层来看一下如何在Spring MVC中使用Velocity。


9.1.1 定义Velocity视图 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.1 定义Velocity视图

假设你已经选择使用Velocity而不是JSP来创建Spring培训应用的视图。你需要使用Velocity模板编写的页面之一是显示可用课程列表的页面。程序清单 9.1 显示了courseList.vm，一个和courseList.jsp等价的用于显示课程列表的Velocity模板。

  程序清单9.1 基于Velocity的课程列表

<html>

<head>

<title>Course List</title>

</head>

<body>

<h2>COURSE LIST</h2>

<table width="600" border="1" cellspacing="1" cellpadding="1">

<tr bgcolor="#999999">

<td>Course ID</td>

<td>Name</td>

<td>Instructor</td>

<td>Start</td>

<td>End</td>

</tr>

#foreach($course in $courses)

<tr>

<td>

<a href="displayCourse.htm?id=${course.id}">

${course.id}

</a>

</td>

<td>${course.name}</td>

<td>${course.instructor.lastName}</td>

<td>${course.startDate}</td>

<td>${course.endDate}</td>

</tr>

#end // 在所有课程中循环

</table>

</body>

</html>

n

可能你首先注意到的是这个模板中没有任何模板标签。这是因为Velocity不是基于与JSP类似的标签的，而是采用了它自己的语言——称为Velocity模板语言（VTL）——用于流程控制和其他指令。在courseList.vm中，#foreach指令用于循环处理一个课程列表，显示每个课程的明细。除了这个Velocity和JSP的基本区别之外，你会发现Velocity的表达式语言和JSP很相似。事实上，当JSP使用${}作为它自己的表达式语言时，它不过是模仿Velocity的做法而已。这个模板仅仅演示了很少一部分你可以使用Velocity所做的事情。

如果想知道更多，可以访问Velocity位于http://jakarta.apache.org/velocity的主页。注意当完成模板之后，你需要配置Spring使它可以在MVC应用中使用Velocity模板作为视图。


9.1.2 配置Velocity引擎 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.2 配置Velocity引擎

首先需要配置的是Velocity引擎自己。要做到这点，可以通过以下方式在Spring配置文件中声明一个VelocityConfigurer Bean：

<bean id="velocityConfigurer" class="org.springframework.

➥web.servlet.view.velocity.VelocityConfigurer">

<property name="resourceLoaderPath">

<value>WEB-INF/velocity/</value>

</property>

</bean>

VelocityConfigurer负责在Spring中设置Velocity引擎。这里，我们通过属性resourceLoaderPath告诉Velocity到哪里寻找它的模板。我们建议将模板放到WEB-INF的某个子目录下面，这样可以保证这些模板不能被直接访问。也可以通过velocityProperties属性来设置其他Velocity的配置细节。例如下面的VelocityConfigurer配置：

<bean id="velocityConfigurer" class="org.springframework.

➥web.servlet.view.velocity.VelocityConfigurer">

<property name="resourceLoaderPath">

<value>WEB-INF/velocity/</value>

</property>

<property name="velocityProperties">

<props>

<prop key="directive.foreach.counter.name">loopCounter</prop>

<prop key="directive.foreach.counter.initial.value">0</prop>

</props>

</property>

</bean>

可以注意到velocityProperties属性使用一个<props>元素来设置多个属性。在这里可以设置的属性与一个典型的Velocity应用中通过“velocity.properties”文件设置的属性是一样的。缺省地，Velocity的#foreach循环维护一个名为$velocityCount的循环计数器，该计数器在第一轮循环开始时从1开始计数。但这里我们设置属性directive.foreach.counter.name为loopCounter，因此将使用$loopCounter来引用循环计数器。我们也通过设置属性directive.foreach.counter.initial.value为0使循环计数器由零开始计数。（想知道关于Velocity配置属性的信息，请参考Velocity开发者指南http://jakarta.apache.org/velocity/developer-guide.html。）


9.1.3 解析Velocity视图 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.3 解析Velocity视图

要使用Velocity模板视图，你必须做的最后一件事情是配置一个视图解析器。具体地说，需要以如下方式在Spring上下文配置中声明一个VelocityViewResolver Bean：

<bean id="viewResolver" class="org.springframework.

web.servlet.view.velocity.VelocityViewResolver">

<property name="suffix"><value>.vm</value></property>

</bean>

VelocityViewResolver和Velocity的关系与InternalResourceViewResolver和JSP的关系相似。正如InternalResourceViewResolver，它使用prefix属性和suffix属性由视图的逻辑名构造出模板文件的路径。这里我们仅仅设置suffix属性为“.vm”扩展名。由于模板目录的路径已经通过VelocityConfigurer的resourceLoaderPath属性配置好了，因此这里不需要设置前缀。

注意：这里把Bean的ID设置为viewResolver。这一点很重要，因为我们并没有配置DispatcherServlet检测所有的视图解析器。如果要同时使用多个视图解析器，则你很可能需要将这个ID改成某个更合适的名字（并且是惟一的），比如velocityViewResolver。

现在，你的应用系统已经可以渲染基于Velocity模板的视图了。你只需要在返回的ModelAndView对象中通过逻辑名引用所需的视图。以ListCourseController为例，不需要做其他事情，因为它已经返回如下的ModelAndView对象：

return new ModelAndView("courseList", "courses", allCourses);

视图的逻辑名为“courseList”。当解析这个视图时，“courseList”加上后缀“.vm”构成了一个模板名“courseList.vm”。VelocityViewResolver会在WEB-INF/velocity路径下寻找这个模板。

至于“courses”模型对象，它会作为一个Velocity属性暴露给Velocity模板使用。在程序清单9.1中，它就是在#foreach指令中使用的集合对象。


9.1.4 格式化日期和数字 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.4 格式化日期和数字

尽管应用已经配置成可以渲染Velocity视图了，但我们还有一些杂七杂八的问题需要解决。当你比较程序清单9.1中的couseList.vm和courseList.jsp时，会注意到courseList.vm没有像courseList.jsp一样对课程的ID、开始日期和结束日期进行格式化。在courseList.jsp中，课程ID显示为一个6位定长的前面以零补齐的数字，而所有的日期以完整格式显示。为了完成courseList.vm，你需要对它作进一步的调整，对ID和日期属性进行格式化。

VTL并不直接支持日期和数字的格式化，而是通过提供日期和时间的工具类来支持格式化。为了允许使用这些工具，你需要告诉VelocityViewResolver在模板中暴露它们时使用的属性名。这些属性名是通过VelocityViewResolver的dateToolAttribute和numberToolAttribute属性来规定的：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.velocity.VelocityViewResolver">

…

<property name="dateToolAttribute">

<value>dateTool</value>

</property>

<property name="numberToolAttribute">

<value>numberTool</value>

</property>

</bean>

在这里，我们规定数字工具通过numberTool属性暴露给Velocity使用。因此，要格式化课程ID，你只需要通过数字工具的format()方法来处理课程ID即可，如下：

$numberTool.format("000000", course.id)

方法format()的第一个参数是模式字符串，在这里我们规定课程ID显示为6个数字的域，必要时在前面补零。模式字符串的语法和java.text.DecimalFormat一致。请参考Velocity关于NumberTool的文档来获取更多关于该工具功能的信息。

类似地，我们分配日期工具使用dateTool属性。为了格式化课程的开始和结束日期，只需使用日期工具的format()方法：

$dateTool.format("FULL", course.startDate)

$dateTool.format("FULL", course.endDate)

与数字工具的format()方法一样，第一个参数也是模式字符串。模式字符串的语法与java.text.SimpleDateFormat一致。另外，也可以设置模式字符串为FULL、LONG、MEDIUM、SHORT或DEFAULT中的一个，以使用标准的java.text.DateFormat模式。这里我们设置模式字符串为FULL来表示完整的日期格式。请参考Velocity关于DateTool的文档来获得更多关于该工具功能的信息。


9.1.5 暴露请求和会话属性 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.5 暴露请求和会话属性

尽管需要在Velocity模板中显示的大多数数据都可以通过ModelAndView对象的模型Map传递给视图，有时候也会需要显示servlet请求或会话中的属性。比如，当用户登录到应用系统时，用户的信息可能存放在servlet会话中。

如果在每一个控制器中都将请求或会话的属性复制到模型Map中，这会是非常笨拙的。幸运的是，VelocityViewResolver会帮你将这些属性复制到模型中。属性exposeRequestAttributes和exposeSessionAttributes告诉VelocityViewResolver是否需要将servlet请求和会话中的属性复制到模型中。比如：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.velocity.VelocityViewResolver">

…

<property name="exposeRequestAttributes">

<value>true</value>

</property>

<property name="exposeSessionAttributes">

<value>true</value>

</property>

</bean>

这两个属性的默认值都为false。但在这里我们把这两个属性都设置为true，从而请求和会话的属性都会被复制到模型中，并且在Velocity模板中可见。


9.1.6 在Velocity中绑定表单域 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.1.6 在Velocity中绑定表单域

在第8章中，我们看到如何使用Spring的<spring:bind>JSP标签将表单域绑定到一个命令对象中。这个标签在向用户显示表单域相关的错误时也是非常有用的。

幸运的是，当你使用Velocity而不是JSP时，不必放弃<spring:bind>提供的功能。Spring提供了若干个Velocity宏来模仿<spring:bind>标签的功能。

例如，假设Spring培训应用的学生注册表单是用Velocity模板编写的。程序清单9.2显示了registerStudent.vm中的一段，演示如何使用#springBind宏：

  程序清单9.2 在Velocity模板中使用#springBind

#springBind("command.phone")

phone: <input type="text"

name="${status.expression}"

value="$!status.value">

<font color="#FF0000">${status.errorMessage}</font><br>

#springBind("command.email")

email: <input type="text"

name="${status.expression}"

value="$!status.value">

<font color="#FF0000">${status.errorMessage}</font><br>

n

#springBind宏的参数是被绑定表单域的引用路径。它在模板中设置了一个名为status的变量用于保存表单域的名称、值以及可能出现的任何错误信息（可能来自一个验证器）。

如果错误信息中包含在HTML中有特殊意义的字符（比如：<，>，&），你可能需要对错误信息进行转义以正确显示在Web浏览器中。在这种情况下，你需要使用宏#springBindEscaped而不是#springBind：

#springBindEscaped("command.email", true)

除了域的引用路径之外，#springBindEscaped宏接受一个boolean参数，表明是否需要对错误信息中的HTML特殊字符进行转义。如果该参数为false，则宏#springBindEscaped和#springBind的行为完全一样，HTML特殊字符不会被转义。

为了在模板中使用Spring的宏，你需要通过VelocityViewResolver的exposeSpringMacroHelpers来使用这些宏：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.velocity.VelocityViewResolver">

…

<property name="exposeSpringMacroHelpers">

<value>true</value>

</property>

</bean>

通过把exposeSpringMacroHelpers属性设为true，你就能在Velocity模板中使用#springBind和#springBindEscaped宏。

尽管Velocity是一种广泛使用的JSP的替代技术，它不是惟一可以使用的替代模板技术。FreeMarker是另一种广为人知的用于在MVC应用的视图层中替代JSP的模板语言。接下来让我们看一下如何在Spring MVC应用中使用FreeMarker。


9.2 使用FreeMarker - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.2 使用FreeMarker

FreeMarker比Velocity复杂一点点，但这只是它比Velocity的功能强大一点点的结果。FreeMarker提供了对一些常用任务的内置支持，比如日期和数字的格式化，以及去除空白字符。在Velocity中，这些特性只有通过附加工具才能实现。很快你将看到在Spring MVC中使用FreeMarker与在Spring MVC中使用Velocity没有太大不同。但重要的事情优先——让我们从写一个用于Spring培训应用的FreeMarker模板开始。


9.2.1 构造一个FreeMarker视图 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.2.1 构造一个FreeMarker视图

假设经过深思熟虑之后，你认为FreeMarker模板比Velocity更合乎自己的品味。因此，你不是使用Velocity开发Spring培训应用的视图层，而是在Spring MVC中使用FreeMarker。再次回到课程列表页面，这次你写了一个courseList.ftl （程序清单9.3）来显示一个可供选择课程列表。

  程序清单9.3 使用FreeMarker的模板语言列举课程

<html>

<head>

<title>Course List</title>

</head>

<body>

<h2>COURSE LIST</h2>

<table width="600" border="1" cellspacing="1" cellpadding="1">

<tr bgcolor="#999999">

<td>Course ID</td>

<td>Name</td>

<td>Instructor</td>

<td>Start</td>

<td>End</td>

</tr>

<#list courses as course>

<tr>

<td>

<a href="displayCourse.htm?id=${course.id}">

${course.id?string("000000")}

</a>

</td>

<td>${course.name}</td>

<td>${course.instructor.lastName}</td>

<td>${course.startDate?string.long}</td>

<td>${course.endDate?string.long}</td>

</tr>

</#list>

</table>

</body>

</html>

n

你可能已经注意到课程列表页面的FreeMarker版和程序清单9.1中给出的Velocity版没有很大不同。和Velocity（或JSP）一样， ${}记号仍作为显示属性值的表达式语言。但你可能也注意到了课程ID和日期属性有额外的参数用于格式化。FreeMarker和Velocity不一样，它有内置的数字和日期格式化支持。

这个courseList.ftl模板仅仅触及了FreeMarker功能的表面。更多FreeMarker的信息可以通过访问FreeMarker主页 http://freemarker.sourceforge.net 得到。


9.2.2 配置FreeMarker引擎 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.2.2 配置FreeMarker引擎

和Velocity一样，要在Spring MVC中使用FreeMarker渲染视图，首先必须配置FreeMarker引擎。在Spring上下文配置文件中可以按如下方式声明一个FreeMarkerConfigurer：

<bean id="freemarkerConfig" class="org.springframework.

➥web.servlet.view.freemarker.FreeMarkerConfigurer">

<property name="templateLoaderPath">

<value>WEB-INF/freemarker/</value>

</property>

</bean>

FreeMarkerConfigurer和FreeMarker的关系与VelocityConfigurer和Velocity的关系一样。可以用它来配置FreeMarker引擎。你必须至少告诉FreeMarker到哪里寻找模板。这是通过设置templateLoaderPath属性做到的。

通过freemarkerSettings属性，也能够设置其他FreeMarker的配置。例如，FreeMarker默认每隔5秒检查模板是否被更新，如果已经更新了，就会重新加载并分析模板。但经常检查模板是否更新可能比较耗时。如果你的应用运行在生产模式下，而且你预期模板不会经常更新，则可以将更新的延迟时间延长至一个小时或者更久。

可以通过为freemarkerSettings属性设置template_update_delay达到这一目的。例如：

<bean id="freemarkerConfig" class="org.springframework.

➥web.servlet.view.freemarker.FreeMarkerConfigurer">

…

<property name="freemarkerSettings">

<props>

<prop key="template_update_delay">3600</prop>

</props>

</property>

</bean>

注意，和VelocityConfigurer的velocityProperties属性一样，freemarkerSettings属性要求一个<props>元素。上面惟一的一个<prop>是设置template_update_delay为3600（秒），因此FreeMarker只有在1个小时之后才检查模板是否更新。


9.2.3 解析FreeMarker视图 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.2.3 解析FreeMarker视图

接下来需要做的一件事是声明一个针对FreeMarker的视图解析器：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.freemarker.FreeMarkerViewResolver">

<property name="suffix"><value>.ftl</value></property>

</bean>

FreeMarkerViewResolver和VelocityViewResolver或InternalResourceViewResolver的工作机制相同。模板资源是通过在视图的逻辑名上增加prefix属性的值作为前缀，以及增加suffix属性的值作为后缀进行解析的。和VelocityViewResolver一样，在这里我们又一次只设置suffix属性，因为模板的路径已经在FreeMarkerConfigurer的templateLoaderPath属性中定义了。

暴露请求和会话属性

在第9.1.3节中，你看到如何告诉VelocityViewResolver将请求和会话属性复制到模型map中，从而它们能够在模板中作为变量使用。采用同样的方式配置FreeMarkerViewResolver，可以将请求和会话属性作为变量暴露给FreeMarker模板使用。要做到这一点，可以设置exposeRequestAttributes或者exposeSessionAttributes为true：

<bean id="viewResolver" class="org.springframework.

web.servlet.view.freemarker.FreeMarkerViewResolver">

…

<property name="exposeRequestAttributes">

<value>true</value>

</property>

<property name="exposeSessionAttributes">

<value>true</value>

</property>

</bean>

这里，两个属性都被设置为true。结果是请求和会话属性都被复制到模板的属性集中，可以使用FreeMarker的表达式语言来访问并显示。


9.2.4 在FreeMarker中绑定表单域 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.2.4 在FreeMarker中绑定表单域

你想使用FreeMarker做的最后一件事是将表单域绑定到命令属性中。在第8章中，你使用JSP的 <spring:bind> 标签，而在第9.1.6节中，你是使用#springBind Velocity宏来实现这一点的。类似地，Spring提供了一组FreeMarker宏来进行这种绑定。

等价的FreeMarker宏是<@spring.bind>和<@spring.bindEscaped>。例如，程序清单9.4节选了registerStudent.ftl中的一段，演示了如何使用<@spring.bind>指令将status信息绑定到表单中。

  程序清单9.4 在FreeMarker模板中使用<@spring.bind>

<@spring.bind "command.phone" />

phone: <input type="text"

name="${spring.status.expression}"

value="${spring.status.value}">

<font color="#FF0000">${spring.status.errorMessage}</font><br>

<@spring.bind "command.email" />

email: <input type="text"

name="${spring.status.expression}"

value="${spring.status.value}">

<font color="#FF0000">${spring.status.errorMessage}</font><br>

n

你可能已经注意到程序清单9.4和程序清单9.2非常相像。但这里有两个不同。首先，FreeMarker版中不是使用Velocity的#springBind宏，而是使用<@spring.bind>指令。其次，<@spring.bind>将状态信息绑定到${spring.status}而不是${status}。

正如Sping的Velocity宏，为了使用这些宏，必须设置FreeMarkerViewResolver的exposeMacroHelpers属性为true：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.freemarker.FreeMarkerViewResolver">

…

<property name="exposeSpringMacroHelpers">

<value>true</value>

</property>

</bean>

最后，你还需要做一件事才能使用FreeMarker宏。在所有需要使用<@spring.bind>和<@spring.bindEscaped>的FreeMarker模板的顶部增加以下一行：

<#import "/spring.ftl" as spring />

这一行会在模板中导入Spring的FreeMarker宏。


9.3 使用Tiles设计页面布局 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.3 使用Tile设计页面布局

直到现在，Spring培训应用的界面看上去还是非常平凡的。我们只是专注于如何编写基于Spring的Web应用，而很少考虑美观性。但一个应用的外观经常决定它是否成功。为了使Spring培训应用看上去更吸引人一些，我们需要用一个包含悦目图片的模板来装饰这些普通的页面。

Jakarta Tiles是一个用于在模板中对页面块进行布局的框架。尽管它最初是作为Jakarta Struts的一部分创建的，Tiles既可以用在Struts中，也可以在Struts之外独立使用。我们打算和Spring的MVC框架一起使用Tiles。

尽管接下来会看到关于如何使用Tiles的简短介绍，我们还是建议你阅读 Struts in Action（Manning，2002）的第11章来获取更多关于使用Tiles的信息。


9.3.1 Tile视图 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.3.1 Tile视图

Spring培训应用的模板是比较简单的。它有一个页首用于显示公司的logo和标语，一个页脚用于显示联系信息和版权信息，以及一个大的中间区域用于显示页面的主要内容。图9.1显示了关于模板如何布局的框图。

图9.1 Spring培训应用模板的布局

模板mainTemplate.jsp（程序清单9.5）定义了这个布局。它使用HTML来定义基本的页面布局，并且使用<tiles:getAsString>和<tiles:insert>标签作为占位符，每个具体的页面会在这个位置填入实际内容。

  程序清单9.5 Spring培训应用的模板mainTemplate.jsp

<%@ taglib prefix="tiles"

uri="http://jakarta.apache.org/struts/tags-tiles" %>

<html>

<head>

<title><tiles:getAsString name="title"/></title>

</head>

<body

<table width="100%" border="0">

<tr>

<td><tiles:insert name="header"/></td>

</tr>

<tr>

<td valign="top" align="left">

<tiles:insert name="content"/>

</td>

</tr>

<tr>

<td>

<tiles:insert name="footer"/>

</td>

</tr>

</table>

</body>

</html>

n

写完这个模板之后，接下来是创建一个Tiles定义文件。一个Tiles定义文件是一个描述如何填充模板的XML文件。这个文件的命名是任意的，但对于Spring培训应用而言，“training-defs.xml”看上去挺合适。

以下一段节选自training-defs.xml，它描述了主模板（称为“template”），并为其中每个组件提供了缺省值：

<tiles-definitions>

<definition name="template" page="/tiles/mainTemplate.jsp">

<put name="title" value="Default title"/>

<put name="header" value="/tiles/header.jsp"/>

<put name="content" value="/tiles/defaultContentPage.jsp"/>

<put name="footer" value="/tiles/footer.jsp"/>

</definition>

…

</tiles-definitions>

这里指定了页首和页脚组件对应的JSP文件的路径，它们定义了页首和页脚的模样。当Tiles构造一个页面时，它会用header.jsp和footer.jsp的输出结果分别替换名为header和footer的<tiles:insert>标签。对于title和content组件，它们被赋予的仅仅是假值。由于它仅仅是一个模板，你不会直接看到这个模板页面。当你查看另一个基于该模板的具体页面时，title和content中的假值会被真正的值覆盖。

在应用中的以模板为基础的所有页面中，课程明细页面是一个典型的例子。在training-defs.xml中它是以如下方式定义的：

<definition name="courseDetail" extends="template">

<put name="title" value="Course Detail" />

<put name="content" value="/tiles/courseDetail.jsp"/>

</definition>

由上可见，courseDetail页面从template扩展，确保该页面能够继承template中的所有组件定义。然而，它选择了重载template的title组件的值为Course Detail，使得该页面能够在浏览器的标题栏中有一个合适的标题。而且，它的主内容页面被重载为courseDetail.jsp。

至此，这只不过是一个典型的基于Tiles的应用。你还没有看到任何Spring特有的东西。但现在是时候通过以下两步将Tiles集成到Spring MVC中了：

n 配置一个TilesConfigurer来装载Tiles定义文件；

n 声明一个Spring MVC视图解析器将逻辑视图名解析为Tiles定义。

配置Tiles

将Tiles和Spring MVC集成的第一步是告诉Spring加载Tiles配置文件。Spring提供了TilesConfigurer，一个装载Tiles配置文件并使之在渲染Tiles视图时可用的Bean。要加载Tiles配置至Spring，以如下方式声明一个TilesConfigurer实例：

<bean id="tilesConfigurer" class="org.springframework.

➥web.servlet.view.tiles.TilesConfigurer">

<property name="definitions">

<list>

<value>/WEB-INF/tiledefs/training-defs.xml</value>

</list>

</property>

</bean>

属性definitions被赋予了一个需要加载的Tiles定义文件的列表。但对于Spring培训应用，只有一个定义文件：training-defs.xml。

解析Tiles视图

将Tiles和Spring MVC集成的最后一步是配置一个视图解析器，将用户送往一个由Tiles定义的页面。InternalResourceViewResolver负责实现这一点：

<bean id="viewResolver" class="org.springframework.

➥web.servlet.view.InternalResourceViewResolver">

<property name="viewClass">

<value>org.springframework.web.

➥servlet.view.tiles.TilesView</value>

</property>

</bean>

通常，InternalResourceViewResolver通过Web应用中的资源（典型地为JSP）来解析逻辑视图。但对于Tiles，需要它将视图解析为Tiles定义文件中的定义。为此，viewClass属性设置为TilesView。实际上，使用Tiles时有两个视图类可供选择：TilesView和TilesJstlView。两者的区别在于TilesJstlView会把本地化信息放到对JSTL页面的请求中。即使我们使用JSTL，我们也没有利用JSTL的国际化支持。因此，在这里我们只使用TilesView。

当InternalResourceViewResolver配置了TilesView（或TilesJstlView）时，它会试图通过在Tiles定义文件中查找一个与逻辑视图名有相同名字的定义来解析视图。例如，考虑对DisplayCourseController的执行结果会发生什么。当执行结束后，该控制器会返回以下的ModelAndView对象：

return new ModelAndView("courseDetail", "course", course);

逻辑视图名是courseDetail，因此TilesView会在Tiles配置中查找视图定义。在这种情况下，它找到了一个名为courseDetail的定义。由于courseDetail是基于template定义的，作为结果的HTML页面会有类似mainTemplate.jsp（程序清单9.5）的结构，但标题设为Course Detail，并且内容是由/tiles/courseDetail.jsp得到的。

Spring培训应用的控制器类根本不需要针对Tiles支持作任何改变。这是因为training-defs.xml中的页面定义被巧妙地命名为与所有控制器返回的逻辑视图名 同名。


9.3.2 Tile控制器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.3.2 Tile控制器

假设你想让Spring培训应用看上去更个性化一些，在页首对登录用户显示一个问候语。这个问候语中包含学生的名字，并且显示他们已经完成的课程的数量。

实现这一点的途径之一是在每个控制器中增加以下代码：

Student student =

(Student) request.getSession().getAttribute("student");

if(student != null) {

int courseCount =

studentService.getCompletedCourses(student).size();

modelMap.add("courseCount", courseCount);

modelMap.add("studentName", student.getFirstName());

}

这会将学生的姓名和已完成课程的数量放到模型中，从而它们可以在header.jsp中以如下方式显示：

Hello ${studentName}, you have completed ${courseCount} courses.

但如果要对所有页面都实现这一点，必须在应用的所有控制器代码中重复这段学生查找代码。有其他方法可以消除这些冗余代码，包括将查找代码放到一个控制器基类中，或者在一个工具类中。但所有这些选择都增加了你本想避免的复杂性。

Tiles的一个独特功能是页面的每一个组件都可以有它自己的控制器。这是一个Tiles特定的控制器，不要把它与Spring MVC的控制器混淆。组件控制器可以与Tiles组件关联，因此每个组件都可以完成该组件特有的功能。为了在Spring培训应用的每个页面包含一个个性化消息，你需要为header组件创建一个控制器。HeaderTilesController（程序清单9.6）获取学生已完成课程的数量，并将这一信息放到组件的上下文中供显示在页首上：

  程序清单9.6 使用Tiles控制器获取课程数量

public class HeaderTileController

extends ComponentControllerSupport {

protected void doPerform(ComponentContext componentContext,

HttpServletRequest request, HttpServletResponse response)

throws Exception {

ApplicationContext context = getApplicationContext();

StudentService studentService =

(StudentService) context.getBean("studentService");

Student student =

(Student) request.getSession().getAttribute("student");

int courseCount =

studentService.getCompletedCourses(student).size();

componentContext.putAttribute("courseCount",

new Integer(courseCount));

componentContext.putAttribute("studentname",

student.getFirstName());

}

}

n

HeaderTileController扩展了ComponentControllerSupport，一个Spring提供的针对Tiles的ControllerSupport类的特定扩展。ComponentControllerSupport使控制器可以通过getApplicaionContext()方法获取并使用Spring应用上下文。HeaderTileController调用getApplicationContext()获得Spring应用上下文，并从应用上下文中找到对studentService Bean的引用，从而它能够查出某学生已经完成了多少课程。当控制器获取该信息之后，会把它放到Tiles组件的上下文中，因此header组件能够显示这一信息。

剩下惟一要做的就是将这个组件控制器和header组件关联。回到training-defs.xml中的header定义，在header定义中设置它的controllerClass属性指向HeaderTileController：

<definition name=".header" page="/tiles/header.jsp"

controllerClass="com.springinaction.training.

➥tiles.HeaderTileController"/>

<definition name="template" page="/tiles/mainTemplate.jsp">

<put name="title" value="Default title"/>

<put name="header" value=".header"/>

<put name="content" value="/tiles/defaultContentPage.jsp"/>

<put name="footer" value="/tiles/footer.jsp"/>

</definition>

现在，当构造这一页面时，Tiles在显示header组件之前会使用HeaderTileController来设置组件的上下文。


9.4 产生非HTML输出 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.4 产生非HTML输出

直到现在，Spring培训应用的Web层产生的视图都是基于HTML的。的确，HTML是典型的在Web上显示信息的方式。但HTML对要提供的信息而言并不总是合适的。

比如，你需要提供的数据是表格形式的，则Excel工作表也许是更好的表示这一信息的形式。如果你想让用户能够自己处理由你的应用提供的数据，Excel工作表也是很有用的。

再比如你需要对文档的格式进行精确的控制。对HTML文档进行精确的格式化几乎是不可能的，对需要在多种浏览器上进行查看的文档而言更是如此。但Adobe的可移植文档格式（PDF）已经成为产生在各种平台上都能以精确的格式进行查看的文档的事实标准。

Excel工作表和PDF文件通常都是静态文件。但Spring提供了基于应用数据动态创建Excel工作表和PDF文档的视图类。

我们将探讨Spring对非HTML视图的支持。首先从动态产生Excel工作表开始。


9.4.1 产生Excel工作表 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.4.1 产生Excel工作表

比方说Spring培训的课程主任要求你产生一个针对所有课程的Excel工作表，其中包含每个课程参加的学生人数。最终，她可能经常向你要这个报表。因此，你决定自动产生这个报表，并且可以在Web上请求该报表，使得她能够在任何需要的时候通过Web下载该报表。

你可以回想起我们已经创建的ListCourseController（程序清单8.1），它能够获取一个课程列表并送到名为courseList的视图供渲染。在第8章，我们假设courseList视图是一个JSP。但事实上，ListCourseController并没有特别规定courseList必须和一个JSP相关联。这就意味着我们只需将courseList关联到一个用于产生Excel工作表的视图即可。

你真幸运。Spring提供了org.springframework.web.servlet.view.document.AbstractExcelView，一个用于在Spring MVC中产生Excel工作表视图的抽象视图实现。你只需要从AbstractExcelView中派生子类，并实现它的buildExcelDocument()方法。程序清单9.7显示了CourseListExcelView，一个能够以Excel工作表的形式产生课程列表的AbstractExcelView的子类：

  程序清单9.7 一个产生课程列表Excel工作表的视图

public class CourseListExcelView extends AbstractExcelView {

protected void buildExcelDocument(Map model, HSSFWorkbook wb,

HttpServletRequest request, HttpServletResponse response)

throws Exception {

Set courses = (Set) model.get("courses");

HSSFSheet sheet = wb.createSheet("Courses");

HSSFRow header = sheet.createRow(0);

header.createCell((short)0).setCellValue("ID");

header.createCell((short)1).setCellValue("Name");

header.createCell((short)2).setCellValue("Instructor");

header.createCell((short)3).setCellValue("Start Date");

header.createCell((short)4).setCellValue("End Date");

header.createCell((short)5).setCellValue("Students");

HSSFCellStyle cellStyle = wb.createCellStyle();

cellStyle.setDataFormat(

HSSFDataFormat.getBuiltinFormat("m/d/yy h:mm"));

int rowNum = 1;

for (Iterator iter = courses.iterator(); iter.hasNext();) {

Course course = (Course) iter.next();

HSSFRow row = sheet.createRow(rowNum++);

row.createCell((short)0).setCellValue(

course.getId().toString());

row.createCell((short)1).setCellValue(course.getName());

row.createCell((short)2).setCellValue(

course.getInstructor().getLastName());

row.createCell((short)3).setCellValue(course.getStartDate());

row.getCell((short)3).setCellStyle(cellStyle);

row.createCell((short)4).setCellValue(course.getEndDate());

row.getCell((short)4).setCellStyle(cellStyle);

row.createCell((short)5).setCellValue(

course.getStudents().size());

}

HSSFRow row = sheet.createRow(rowNum);

row.createCell((short)0).setCellValue("TOTAL:");

String formula = "SUM(F2:F"+rowNum+")";

row.createCell((short)5).setCellFormula(formula);

}

}

n

AbstractExcelView以Jakarta POI（http://jakarta.apache.org/poi）为基础，一个用于产生许多微软Office应用程序支持的文档的API，其中也包括Excel工作表。方法buildExcelDocument()要求一个java.util.Map参数，其中可以包含任何构造视图所需要的模型对象，以及一个空的HSSFWorkbook[2]用于放置需要创建的工作表。

CourseListExcelView首先从模型Map中获取一个包含课程对象的集合。然后它使用课程集合中的数据构造Excel工作表。

如果你曾用过servlet或基于servlet的框架产生过Excel工作表（或者任何非HTML的内容），则会知道必须设置HTTP响应的内容类型，以使浏览器能够知道如何显示文档。对于Excel工作表，你很可能会用以下方式设置内容类型：

response.setContentType("application/vnd.ms-excel");

但使用CourseListExcelView，你不必担心内容类型的设置。CourseListView的默认构造器会帮你处理这一点。余下惟一要做的事就是在CourseListExcelView和courseList的逻辑视图名之间建立关联。最简单的做法是使用一个BeanNameViewResolver（见8.4.2节）并且以courseList作为ID声明CourseListExcelView Bean：

<bean id="courseList"

class="com.springinaction.training.mvc.CourseListExcelView"/>

另一种做法是使用ResourceBundleViewResolver。在这种情况下，你是通过在views.properties文件中放入下面一行在courseList这个逻辑名和CourseListExcelView之间建立关联的：

courseList.class=com.springinaction.training.mvc.CourseListExcelView

请记住，对于ListCourseController来说，产生Excel工作表没有任何特别之处。视图对象完全负责决定所产生的文档的类型。控制器类与用于渲染输出的视图机制之间完全没有耦合。这一点很重要，因为这意味着你可以换一个不同的视图对象来产生一个不同类型的文档。事实上，那就是我们接下来要做的——为ListCourseController装配一个不同的视图，以PDF文件的形式产生一个课程清单。


9.4.2 产生PDF文档 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.4.2 产生PDF文档

假设你需要产生的不是一个Excel工作表，而是一个显示课程清单的PDF文件。和刚才产生Excel工作表一样，首先你需要创建一个用于产生PDF文档的视图实现。

Spring的org.springframework.web.servlet.view.document.AbstractPdfView是一个视图的抽象实现类，支持作为Spring MVC中的视图创建PDF文档。与AbstractExcelView很相似，你需要派生一个AbstractPdfView的子类，并且实现buildPdfDocument()方法来构造一个PDF文档。

CourseListPdfView（程序清单9.8）是一个AbstractPdfView的子类，能够产生包含一个课程清单表格的PDF文档。

  程序清单9.8 产生一个PDF文档视图

public class CourseListPdfView extends AbstractPdfView {

protected void buildPdfDocument(Map model, Document pdfDoc,

PdfWriter writer, HttpServletRequest request,

HttpServletResponse response) throws Exception {

Set courseList = (Set) model.get("courses");

Table courseTable = new Table(5);

courseTable.setWidth(90);

courseTable.setBorderWidth(1);

courseTable.addCell("ID");

courseTable.addCell("Name");

courseTable.addCell("Instructor");

courseTable.addCell("Start Date");

courseTable.addCell("EndDate");

for (Iterator iter = courseList.iterator(); iter.hasNext();) {

Course course = (Course) iter.next();

courseTable.addCell(course.getId().toString());

courseTable.addCell(course.getName());

courseTable.addCell(course.getInstructor().getLastName());

courseTable.addCell(course.getStartDate().toString());

courseTable.addCell(course.getEndDate().toString());

}

pdfDoc.add(courseTable);

}

}

n

PDF文档是在buildPdfDocument()方法中创建的。除了其他一些参数外，这个方法要求一个java.util.Map和一个com.lowagie.text.Document参数。和AbstractExcelView中的buildExcelDocument()一样，传递给buildPdfDocument()方法的Map包含了可以用于生成视图的模型数据。

AbstractPdfView基于iText，一个用于处理PDF文档的API。传递给buildPdfDocument()方法的Document对象是一个空的iText文档，可以在其中填写内容。（想知道关于iText的更多信息，可以访问iText的主页http://www.lowagie.com/iText。）

在CourseListPdfView中，buildPdfDocument()方法首先从模型Map中获得所有课程。接着，它构造一个每行一个课程的表格。表格构造完之后，它将表格添加到Document对象中。

正如AbstractExcelView，AbstractPdfView帮你处理内容类型的设置（在这种情况下，内容类型被设置为application/pdf）。

接下来，你必须将CourseListPdfView关联到逻辑视图名courseList上。也和CourseListExcelView一样，你有两个选择。最容易的一种方式是使用一个BeanNameViewResolver，并且在应用上下文配置文件中按如下方式声明一个CourseListPdfView Bean：

<bean id="courseList"

class="com.springinaction.training.mvc.CourseListPdfView"/>

或者也可以使用一个ResourceBundleViewResolver，在views.properties中按以下方式声明这个视图：

courseList.class=com.springinaction.training.mvc.CourseListPdfView

改变页面尺寸

传递给buildPdfDocument()方法的Document对象默认配置为纵向A4尺寸（210×297mm）。但你也许想把这个配置改变为不同的尺寸和方向。要做到这一点，必须重载AbstractPdfView的getDocument()方法，返回一个由你自己选择的Document对象。

例如，下面给出的getDocument()实现返回一个使用legal-size（216×356mm）页面的文档对象：

protected Document getDocument() {

return new Document(PageSize.LEGAL);

}

要将页面的方向从纵向改为横向，只需要调用PageSize对象的rotate()方法即可。例如，使用以下代码将legal-size尺寸的文档从纵向改为横向：

protected Document getDocument() {

return new Document(PageSize.LEGAL.rotate());

}

现在你已经知道如何通过定制视图的实现来产生Excel和PDF文档了。但如果你的应用需要用到一种Spring没有提供直接支持的不同类型的文档呢？


9.4.3 产生其他非HTML文件 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.4.3 产生其他非HTML文件

AbstractExcelView和AbstractPdfView都实现了View接口。但如果你需要的不是Excel工作表和PDF文档，那就得亲自创建一个View接口的实现。View接口只要求你实现一个方法，即render()方法。它的方法签名如下：

void render(Map model,

HttpServletRequest request,

HttpServletResponse response) throws Exception;

我们假设你的应用需要动态产生JPEG格式的图像。由于你将来可能需要创建一些互不相同的JPEG图像，因此明智的做法是创建一个View的抽象实现，封装渲染JPEG视图所需要的公共代码。程序清单9.9显示了一个AbstractJpegView，一个参照AbstractPdfView和AbstractExcelView的风格编写的用于渲染JPEG的抽象View。

  程序清单9.9 一个渲染JPEG图像的抽象视图

public abstract class AbstractJpegView implements View {

public AbstractJpegView() {}

public int getImageWidth() { return 100; }

public int getImageHeight() { return 100; }

protected int getImageType() {

return BufferedImage.TYPE_INT_RGB;

}

public void render(Map model, HttpServletRequest request,

HttpServletResponse response) throws Exception {

response.setContentType("image/jpeg");

BufferedImage image = new BufferedImage(

getImageWidth(), getImageHeight(), getImageType());

buildImage(model, image, request, response);

ServletOutputStream out = response.getOutputStream();

JPEGImageEncoder encoder = new JPEGImageEncoderImpl(out);

encoder.encode(image);

out.flush();

}

protected abstract void buildImage(Map model, BufferedImage image,

HttpServletRequest request, HttpServletResponse response)

throws Exception;

}

n

要使用AbstractJpegView，你必须扩展它并实现buildImage()方法。例如，如果你只想简单地画一个圆，则CircleJpegView（程序清单 9.10）就能做到这一点。

  程序清单9.10 一个绘制圆形的视图

public class CircleJpegView extends AbstractJpegView {

public CircleJpegView() {}

protected void buildImage(Map model, BufferedImage image,

HttpServletRequest request, HttpServletResponse response)

throws Exception {

Graphics g = image.getGraphics();

g.drawOval(0,0,getImageWidth(), getImageHeight());

}

}

n

你可以自己扩展AbstractJpegView去绘制更多有趣的图形。或许以CircleJpegView为起点，你可以进一步画出一个饼图。


9.5 小结 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

9.5 小结

尽管JSP是在Spring MVC应用中产生视图的最可能的选择，但它不是惟一的选择。通过切换视图解析器和视图实现，你的应用可以使用其他视图层技术产生Web页面，甚至可以产生非HTML的输出。

在本章中，你学会了如何在Spring MVC应用中使用Velocity或FreeMarker来代替JSP。以类似的方式，你也看到如何将Jakarta Tiles集成到你的Spring MVC应用中，使应用的外观可用性更高，也更美观。

最后，你看到如何创建定制的视图实现，能够产生动态生成的二进制内容，比如Excel工作表、PDF文档以及图像。

虽然本章在应用的视图层为你提供了若干种选择，但所有你看到的实现都是在Spring MVC应用中完成的。那么，如果你更偏爱别的MVC框架呢？

在下一章中，我们会把选择的范围扩展到其他MVC框架，从而你可以同时使用Spring以及你自己选择的MVC框架。


11.1 Acegi安全系统介绍 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

本章内容：

n Acegi安全系统介绍

n 使用Servlet过滤器保护Web应用系统

n 基于数据库或LDAP进行身份认证

n 透明地对方法调用进行保护

你是否曾注意到在电视连续剧中大多数人是不锁门的？这是司空见惯的。在情景喜剧Seinfeld中，Krammer常常到Jerry的房间里自己从冰箱里拿东西吃。在Friends中，各种各样的剧中人经常不敲门就不假思索地进入别人的房间。甚至有一次在伦敦，Ross突然进入Chandler的旅馆房间，差点儿撞见Chandler和Ross的妹妹的私情。

在20世纪50年代Leave It to Beaver热播的时代，并不值得为人们不锁门这一现象大惊小怪。但在如今这个隐私和安全极受重视的时代，看到电视剧中的角色允许他人大摇大摆地进入自己的家中或房间中实在让人觉得难以置信。

类似地，在软件系统中，允许任何人可以访问敏感或者私密的信息是不明智的。应用系统必须通过质询来验证用户身份，据此决定是允许还是拒绝他访问受限制的信息。无论你是通过用户名/密码来保护一个电子邮件账号，还是基于交易个人身份号码来保护一个佣金账户，安全性都是大多数应用系统的一个重要切面。

我们有意选择“切面”这个词来描述应用系统的安全性。安全性是超越应用系统功能特性的一个关注点。应用系统的绝大部分不应该亲自参与到与安全相关的处理中。尽管你能够把与安全相关的处理直接编码到应用系统中（这种情况并不少见），但更好的做法还是将安全有关的关注点与应用系统的关注点分开。

如果你想到这听上去好像安全性是通过 “面向切面”技术实现的，那你猜对了。在本章中，我们将向你介绍Acegi安全系统，并探讨使用Spring AOP和Servlet过滤器[1]来保护应用系统的各种手段。

11.1 Acegi安全系统介绍

Acegi是一个能够为基于Spring的应用系统提供描述性安全保护的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean，充分利用了Spring对依赖注入和面向切面编程的支持。

当保护Web应用系统时，Acegi使用Servlet过滤器来拦截Servlet请求，以实施身份认证并强制安全性。并且，在第11.4.1节你将会看到，Acegi采取了一种独特的机制来声明Servlet过滤器，使你可以使用Spring IoC注入它所依赖的其他对象。

Acegi也能够通过保护方法调用在更底层的级别上强制安全性。使用Spring AOP，Acegi代理对象，将“切面”应用于对象，以确保用户只有在拥有恰当授权时才能调用受保护的方法。

无论你正在保护一个Web应用程序还是需要方法调用级别的安全性，Acegi都是使用如图11.1所示的4个主要组件来实施安全性。

图11.1 Acegi安全的基本组件

通过本章，我们会揭示每一个组件的细节。但在开始考察Acegi安全机制的本质之前，首先让我们居高临下地考察一下每个组件扮演的角色。


11.1.1 安全拦截器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.1.1 安全拦截器

为了释放锁舌并打开门，你必须先把钥匙插到锁孔中，并恰当地拨动锁的制栓。如果钥匙和锁不匹配，就无法拨动制栓并释放锁舌。但如果你有正确的钥匙，所有的制栓就会接受这把钥匙，锁舌就会释放，从而允许你把门打开。

在Acegi中，可以认为安全拦截器像一把锁的锁舌，能够阻止对应用系统中受保护资源的访问。为了释放“锁舌”并通过安全拦截器，你必须向系统提供“钥匙”（通常是一对用户名和密码）。“钥匙”会尝试拨开安全拦截器的“制栓”，从而允许你访问受保护的资源。


11.1.2 认证管理器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.1.2 认证管理器

第一道必须打开的安全拦截器的制栓是认证管理器。认证管理器负责决定你是谁。它是通过考虑你的主体（通常是一个用户名）和你的凭证（通常是一个密码）做到这点的。

你的主体定义了你是谁，你的凭证是确认你身份的证据。如果你的凭证足以使认证管理器相信你的主体可以标识你的身份，Acegi就能知道它在和谁打交道。


11.1.3 访问决策管理器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.1.3 访问决策管理器

一旦Acegi决定了你是谁，它就必须决定你是否拥有访问受保护的资源的恰当授权。访问决策管理器是Acegi锁中第二道必须被打开的制栓。访问决策管理器进行授权，通过考虑你的身份认证信息和与受保护资源关联的安全属性决定是否让你进入。

例如，安全规则也许规定只有主管才允许访问某个受保护资源。如果你被授予主管权限，则第二道也是最后一道制栓——访问决策管理器——会被打开，并且安全拦截器会给你让路，让你取得受保护资源的访问权。


11.1.4 运行身份管理器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.1.4 运行身份管理器

当你通过认证管理器和访问决策管理器，安全拦截器会被开启，门已经可以被打开。但在你转动门把手进入之前，安全拦截器也许还有一件事要做。

即使你已经通过身份认证并且已经获得了访问被保护资源的授权，门后也许还有更多的安全限制在等着你。比如，你也许已被授权访问查看某个Web页面，但用于创建该页面的对象也许和页面本身有不同的安全需求。一个运行身份管理器可以用另一个身份替换你的身份，从而允许你访问应用系统内部更深处的受保护对象。

运行身份管理器的用处在大多数应用系统中是有限的。幸运的是，当你使用Acegi保护应用系统时可以不必使用甚至不必完全理解运行身份管理器。因此，我们认为运行身份管理器是一个高级课题，在下文中不再深入地探讨它。

现在，你已经看到了Acegi安全性的全貌，让我们回过头来看一下如何配置Acegi安全系统的每一个部分，首先由认证管理器开始。


11.2 管理身份验证 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.2 管理身份验证

决定是否允许用户访问受保护资源的第一步是判断用户的身份。在大多数应用系统中，这意味着用户在一个登录屏上提供用户名和密码。用户名（或者主体）告诉应用系统用户声明自己是谁。为了确证用户的身份，用户需要同时提供一个密码（或凭证）。如果应用系统的安全机制确认密码是正确的，则系统假设用户的实际身份与他声明的身份相同。

在Acegi中，是由认证管理器负责确定用户身份的。一个认证管理器由接口net.sf.acegisecurity.AuthenticationManager定义：

public interface AuthenticationManager {

public Authentication (Authentication authentication)

throws AuthenticationException;

}

认证管理器的()方法需要一个net.sf.acegisecurity.Authentication对象（其中可能只包括用户名和密码）作为参数，它会尝试验证用户身份。如果认证成功，()方法返回一个完整的Authentication对象，其中包括用户已被授予的权限（将由授权管理器使用）。如果认证失败，则它会抛出一个AuthenticationException。

正如你所见到的，AuthenticationManager接口非常简单，而且你可以相当容易地实现自己的AuthenticationManager。但是Acegi提供了ProviderManager，作为AuthenticationManager的一个适用于大多数情形的实现。所以，我们不讨论开发自己的认证管理器，而是看一下如何使用ProviderManager。


11.2.1 配置ProviderManager - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.2.1 配置ProviderManager

ProviderManager是认证管理器的一个实现，它将验证身份的责任委托给一个或多个认证提供者，如图11.2所示。

图11.2 ProviderManager将身份验证的职责委托给一个或多个认证提供者

ProviderManager的思路是使你能够根据多个身份管理源来认证用户。它不是依靠自己实现身份验证，而是逐一遍历一个认证提供者的集合，直到某一个认证提供者能够成功地验证该用户的身份（或者已经尝试完了该集合中所有的认证提供者）。

你可以在Spring配置文件中按如下方式配置一个ProviderManager：

<bean id="authenticationManager"

class="net.sf.acegisecurity.providers.ProviderManager">

<property name="providers">

<list>

<ref bean="daoAuthenticationProvider"/>

<ref bean="passwordDaoProvider"/>

</list>

</property>

</bean>

通过providers属性可以为ProviderManager提供一个认证提供者的列表。通常你只需要一个认证提供者，但在某些情况下，提供由若干个认证提供者组成的列表是有用的。在这种情况下，如果一个认证提供者验证身份失败，可以尝试另一个认证提供者。一个认证提供者是由net.sf.acegisecurity.provider.AuthenticationProvider接口定义的。Spring提供了若干个AuthenticationProvider的有用实现，如下表所列：

表11.1 Acegi选择的认证提供者

认证提供者 | 目 的
net.sf.acegisecurity.adapters. AuthByAdapterProvider | 使用容器的适配器验证身份。
net.sf.acegisecurity.providers.cas. CasAuthenticationProvider | 根据Yale中心认证服务验证身份。
net.sf.acegisecurity.providers.dao. DaoAuthenticationProvider | 从数据库中获取用户信息，包括用户名和密码。
net.sf.acegisecurity.providers.jaas. JaasAuthenticationProvider | 从JAAS登录配置中获取用户信息。
net.sf.acegisecurity.providers.dao. PasswordDaoAuthenticationProvider | 从数据库中获取用户信息，但让底层的数据源完成实际的身份验证。
net.sf.acegisecurity.providers.rcp. RemoteAuthenticationProvider | 根据远程服务验证用户身份。
net.sf.acegisecurity.runas. RunAsImplAuthenticationProvider | 针对身份已经被运行身份管理器替换的用户进行认证。
net.sf.acegisecurity.providers. TestingAuthenticationProvider | 用于单元测试。自动认为一个TestingAuthenticationToken是有效的。不应用于生产环境。

你可以认为一个AuthenticationProvider是一个下属的AuthenticationManager。事实上，AuthenticationProvider接口也有一个authenticate()方法，该方法的签名与AuthenticationManager的authenticate()方法完全一样。

在本节中，我们关注表11.1中列出的三个最常用的认证提供者。首先从使用DaoAuthenticationProvider进行简单的基于数据库验证身份开始。


11.2.2 根据数据库验证身份 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.2.2 根据数据库验证身份

大多数应用系统将包括用户名和密码在内的用户信息保存在数据库中。如果这和你的情况相符，则你会发现Acegi提供的以下两个认证提供者是有用的：

n DaoAuthenticationProvider；

n PasswordDaoAuthenticationProvider。

这两个认证提供者都能使你通过将用户的主体和密码与数据库记录进行比较来验证用户身份。两者的不同之处在于真正的身份验证是在哪里进行的。DaoAuthenticationProvider使用Dao来获取用户名和密码，并使用它们来验证用户身份。而PasswordDaoAuthenticationProvider将身份验证的责任推给Dao自己完成。这是一个重要的区别，等到我们在11.2.3节中讨论PasswordDaoAuthenticationProvider时，这个区别会变得更清楚。

在本节中，我们看一下如何使用DaoAuthenticationProvider根据保存在某个数据源（通常是关系数据库）中的用户信息进行简单的身份验证。在下一节中你将看到如何使用PasswordDaoAuthenticationProvider根据一个LDAP（轻型目录访问协议）用户库进行身份验证。

声明一个DAO认证提供者

一个DaoAuthenticationProvider是一个简单的认证提供者，它使用DAO来从数据库中获取用户信息（包括用户的密码）。

取得了用户名和密码之后，DaoAuthenticationProvider通过比较从数据库中获取的用户名和密码以及来自认证管理器的通过Authentication对象中传入的主体和凭证完成身份验证（见图11.3）。如果用户名和密码与主体和凭证匹配，则用户通过身份验证，同时返回给认证管理器一个已完全填充的Authentication对象。否则会抛出一个AuthenticationException，表明身份验证失败。

图11.3 DaoAuthenticationProvider通过从数据库中获取用户信息帮助认证管理器进行身份验证

配置一个DaoAuthenticationProvider再简单不过了。下一段XML摘要显示了如何声明一个DaoAuthenticationProvider Bean，并且装配上它所依赖的DAO。

<bean id="authenticationProvider" class="net.sf.acegisecurity.

➥ providers.dao.DaoAuthenticationProvider">

<property name="authenticationDao">

<ref bean="authenticationDao"/>

</property>

</bean>

属性authenticationDao指定了一个用于从数据库中获取用户信息的Bean。这个属性期望赋予一个net.sf.acegisecurity.providers.dao.AuthenticationDao的实例。接下来的问题就是该如何配置authenticationDao Bean了。

Acegi提供了两个可供选择的AuthenticationDao的实例：InMemoryDaoImpl和JdbcDaoImpl。我们首先配置一个InMemoryDaoImpl作为authenticationDao Bean的实例，然后再使用更实用的JdbcDaoImpl替换它。

使用内存DAO

尽管假定AuthenticationDao对象总是通过查询关系数据库获取用户信息是一种自然的想法，事实情形却不必总是如此。如果你的应用系统的身份验证需求是微不足道的，或者是为了开发期间方便起见，也许更简单的做法是在Spring配置文件中直接配置你的用户信息。

为此，Acegi提供了InMemoryDaoImpl，一个从Spring配置文件中获取用户信息的AuthenticationDao。你能够在Spring配置文件中通过以下方式配置一个InMemoryDaoImpl：

<bean id="authenticationDao" class="net.sf.acegisecurity.

➥ providers.dao.memory.InMemoryDaoImpl">

<property name="userMap">

<value>

palmerd=4moreyears,ROLE_PRESIDENT

bauerj=ineedsleep,ROLE_FIELD_OPS,ROLE_DIRECTOR

myersn=traitor,disabled,ROLE_CENTRAL_OPS

</value>

</property>

</bean>

属性userMap使用一个net.sf.acegisecurity.providers.dao.memory.UserMap对象来定义一组用户名、密码和权限。幸运的是，当装配一个InMemoryDaoImpl时，你不必为配置一个UserMap实例而操心，因为Acegi提供了一个属性编辑器，它能够帮你把一个字符串转化为一个UserMap对象。

userMap字符串的每一行都是一个名字—值对，其中名字是用户名，值是一个由逗号分隔的列表，它以用户密码开头，后面跟着一个或多个赋予该用户的权限的名字（可以将权限看作角色）。

Myersn=traitor, disabled,ROLE_CENTRAL_OPS

以上的authenticationDao声明中定义了三个用户：palmerd、bauerj和myersn。这三个用户的密码分别是4moreyears、ineedsleep、和traitor。用户palmerd被定义为拥有权限ROLE_PRESIDENT，bauerj被赋予权限ROLE_FIELD_OPS和ROLE_DIRECTOR，并且用户myersn被给予ROLE_CENTRAL_OPS授权。

注意用户myersn的密码后面有disabled这个单词。这是一个特殊的标志，表明该用户已被禁用。

InMemoryDaoImpl有明显的局限性。最主要的一点是，对安全性进行管理时要求你重新编辑Spring的配置文件并且重新部署应用。虽然在开发环境下这是可以接受的（而且可能还是有帮助的），但对于生产用途而言这种做法就太笨拙了。因此，我们强烈反对在生产环境下使用InMemoryDaoImpl，而是应该考虑使用JdbcDaoImpl。

声明一个JDBC DAO

JdbcDaoImpl是一个简单而灵活的认证DAO。以它最简单的形式，只需要一个javax.sql.DataSource对象的引用，可以通过以下方式在Spring配置文件中进行声明：

<bean id="authenticationDao"

class="net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

</bean>

JdbcDaoImpl假设你在数据库中已经建立了某些用于存放用户信息的表。特别地，它假设有一张“Users”表和一张“授权”表，如图11.4所示。

图11.4 JdbcDaoImpl假设的数据库表

当JdbcDaoImpl查找用户信息时，它会使用“SELECT username，password，enabled FROM users WHERE username = ?”作为查询语句。类似地，当查找授权时，它会使用“SELECT username，authority FROM authorities WHERE username = ?”。

尽管JdbcDaoImpl假定的表结构非常直接，它们很可能与你已经为应用系统建立的表结构不一致。比如，在Spring培训应用中，Student表保存用户名（在login列中）和密码。是否这意味着你无法在Spring培训应用中使用JdbcDaoImpl来验证学生的身份？

当然不是。但你必须通过设置usersByUserNameQuery属性告诉JdbcDaoImpl如何找到用户信息。下面是对authenticationDao Bean的调整使它更适合Spring培训应用：

<bean id="authenticationDao"

class="net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

<property name="usersByUserNameQuery">

<value>SELECT login, password

FROM student WHERE login=?</value>

</property>

</bean>

现在JdbcDaoImpl知道如何在Student表中查找用户的认证信息了。但是，还有一件事遗漏了。Student表中没有标志表明用户是使能的还是禁用的。事实上，我们一直假设所有的学生都是使能的。但我们如何告诉JdbcDaoImpl做同样的假设？

JdbcDaoImpl还有一个usersByUserNameMapping属性，它引用一个MappingSqlQuery实例。正如你回忆起第4章中所介绍的，MappingSqlQuery的mapRow()方法将一个ResultSet中的字段映射为一个领域对象。对于JdbcDaoImpl，提供给usersByUserNameMapping属性的MappingSqlQuery对象要求能够将一个ResultSet（通过执行用户查询获得）转换为一个net.sf.acegisecurity.UserDetails对象。

UsersByUserNameMapping（程序清单11.1）显示了一个MappingSqlQuery的实现，它适合将学生用户表的一个查询结果转换为一个UserDetails对象。它从ResultSet中抽取出username和password，但总是设置enabled属性为true。

  程序清单11.1 将学生查询的结果映射为一个UserDetails对象

public class UsersByUsernameMapping extends MappingSqlQuery {

protected UsersByUsernameMapping(DataSource dataSource) {

super(dataSource, usersByUsernameQuery);

declareParameter(new SqlParameter(Types.VARCHAR));

compile();

}

protected Object mapRow(ResultSet rs, int rownum)

throws SQLException {

String username = rs.getString(1);

String password = rs.getString(2);

UserDetails user = new User(username, password, true,

new GrantedAuthority[]

{new GrantedAuthorityImpl("HOLDER")});

return user;

}

}

n

剩下惟一需要做的事就是声明一个UsersByUsernameMapping Bean，并将它装配到usersByUserNameMapping属性中。以下的authenticationDao Bean的声明将一个内部Bean装配至usersByUserNameMapping属性中，从而可以应用新的用户映射：

<bean id="authenticationDao"

class="net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

<property name="usersByUserNameQuery">

<value>SELECT login, password

FROM student WHERE login=?</value>

</property>

<property name="usersByUserNameMapping">

<bean class=

"com.springinaction.training.security.UsersByUsernameMapping"/>

</property>

</bean>

你也能改变JdbcDaoImpl查询用户权限的方式。与属性usersByUserNameQuery和usersByUserNameMapping定义JdbcDaoImpl如何查询用户认证信息相同，属性authoritiesByUserNameQuery和authoritiesByUserNameMapping告诉JdbcDaoImpl如何查询用户的权限：例如，你可以使用以下代码从user_privileges表中查询已授予一个用户的权限。

<bean id="authenticationDao"

class="net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

<property name="usersByUserNameQuery">

<value>SELECT login, password

FROM student WHERE login=?</value>

</property>

<property name="usersByUserNameMapping">

<bean class="com.springinaction.training.

➥ security.UsersByUsernameMapping"/>

</property>

<property name="authoritiesByUserNameQuery">

<value>SELECT login, privilege

FROM user_privileges where login=?</value>

</property>

</bean>

你也可以将属性authoritiesByUserNameMapping设置成一个定制的MappingSqlQuery对象，从而可以定制权限查询的结果如何映射为一个net.sf.acegisecurity.GrantedAuthority对象。但是，由于默认的MappingSqlQuery对上面给出的查询来说已经足矣，我们就不再画蛇添足了。

使用加密的密码

默认地，DaoAuthenticationProvider假设用户的密码是以明文方式（未加密的方式）存储的。但在与从数据库中取出的密码进行比较之前，可以使用一个密码编码器加密用户输入的明文密码。Acegi提供了三个密码编码器：

n PlaintextPasswordEncoder（默认）——不对密码进行编码，直接返回未经改变的密码；

n Md5PasswordEncoder ——对密码进行消息摘要（MD5）编码；

n ShaPasswordEncoder ——对密码进行安全哈希算法（SHA）编码。

你可以通过设置DaoAuthenticationProvider的passwordEncoder属性改变它的密码编码器。例如，要使用MD5编码可以用以下代码：

<property name="passwordEncoder">

<bean class=

"net.sf.acegisecurity.providers.encoding.Md5PasswordEncoder"/>

</property>

你也需要设置编码器的种子源（salt source）。一个种子源为编码提供种子（salt），或者称编码的密钥。Acegi提供两个种子源：

n ReflectionSaltSource——使用用户的User对象中某个指定的属性来获取种子；

n SystemWideSaltSource——对系统中所有用户使用相同的种子。

SystemWideSaltSource适用于大多数情形。以下一段XML将一个SystemWideSaltSource装配到DaoAuthenticationProvider的saltSource属性中：

<property name="saltSource">

<bean class=

"net.sf.acegisecurity.providers.dao.SystemWideSaltSource">

<property name="systemWideSalt">

<value>123XYZ</value>

</property>

</bean>

</property>

ReflectionSaltSource使用用户对象的某个特定属性作为用户密码的编码种子。由于这意味着每个用户的密码都会以不同的方式编码，因此更安全。若要装配一个ReflectionSaltSource，可以通过如下方式将它装配到saltSource属性中：

<property name="saltSource">

<bean class="net.sf.acegisecurity.

➥providers.dao.ReflectionSaltSource">

<property name="userPropertyToUse">

<value>userName</value>

</property>

</bean>

</property>

在这里，用户的userName属性被用作种子来加密用户的密码。要特别重视的是必须保证种子是静态的，永远不会改变；否则，一旦种子改变，就再也不可能对用户身份进行验证了。

缓存用户信息

每次当请求一个受保护的资源时，认证管理器就被调用以获取用户的安全信息。但如果获取用户信息涉及到查询数据库，每次都查询相同的数据可能在性能上表现得很糟糕。注意到用户信息不会频繁改变，也许更好的做法是在第一次查询时缓存用户信息，并在后续的查询中直接从缓存中获取用户信息。

DaoAuthenticationProvider通过net.sf.acegisecurity.providers.dao.UserCache接口的实现类支持对用户信息进行缓存。

public interface UserCache {

public UserDetails getUserFromCache(String username);

public void putUserInCache(UserDetails user);

public void removeUserFromCache(String username);

}

顾名思义，接口UserCache中方法提供了向缓存中放入、取得和删除用户明细信息的功能。写一个你自己的UserCache实现类是相当简单的。然而，在你考虑开发自己的UserCache实现类之前，应该首先考虑Acegi提供的两个方便的UserCache实现类：

n net.sf.acegisecurity.providers.dao.cache.NullUserCache

n net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache

NullUserCache事实上不进行任何缓存。任何时候调用它的getUserFromCache方法，得到的返回值都是null。这是DaoAuthenticationProvider使用的默认UserCache实现。

EhCacheBasedUserCache是一个更实用的缓存实现。类如其名，它是基于开源项目ehcache实现的。ehcache是一个简单快速的针对Java的缓存解决方案，同时也是Hibernate默认的和推荐的缓存方案。（关于ehcache的更多信息，请访问ehcache的网站 http://ehcache.sourceforge.net）

在DaoAuthenticationProvider中使用ehcache是很简单的，只需要简单地声明一个EhCacheBasedUserCase Bean即可：

<bean id="userCache"

➥class="net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">

<property name="minutesToIdle">15</property>

</bean>

属性minutesToIdle告诉缓存器一条用户信息在没有访问的情况下应该在缓存中保存多久。这里，我们设定在15分钟的非活动期后删除该条用户信息。

声明了userCache Bean之后，下面惟一要做的事就是把它装配到DaoAuthenticationProvider的userCache属性中：

<bean id="authenticationProvider"

➥class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">

<property name="userCache">

<ref bean="userCache"/>

</property>

</bean>


11.2.3 根据LDAP仓库进行身份验证 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.2.3 根据LDAP仓库进行身份验证

DaoAuthenticationProvider的工作方式是从数据库中获取用户的主体和凭据，并且与用户在登录时输入的主体和凭据进行比较。如果你希望由认证提供者最终负责认证决策，这是一种很好的方式。但也许你更希望将认证的职责委托给一个第三方的系统。

例如，根据LDAP服务器进行认证很常见。在这种情况下，是由LDAP服务器帮助应用系统完成身份验证的工作。应用系统自已甚至看不到用户存储的凭据。

PasswordDaoAuthenticationProvider与DaoAuthenticationProvider有相似的目标，惟一的不同之处在于它只负责获取用户明细信息，实际的认证决策是委托给DAO完成的。而且，正如你将要看到的，在使用LDAP的情况下，DAO进一步把身份验证委托给了LDAP服务器。

要使用PasswordDaoAuthenticationProvider，你需要在Spring配置文件中以如下方式声明它：

<bean id="authenticationProvider" class="net.sf.acegisecurity.

➥ providers.dao.PasswordDaoAuthenticationProvider">

<property name="passwordAuthenticationDao">

<ref bean="passwordAuthenticationDao"/>

</property>

</bean>

属性passwordAuthenticationDao装配了一个同名的Bean的引用。装配到这个属性的Bean是负责获取用户信息并进行身份验证的DAO。这个Bean实现类必须实现net.sf.acegisecurity.providers.dao.PasswordAuthenticationDao接口：

public interface PasswordAuthenticationDao {

public UserDetails loadUserByUsernameAndPassword(String username,

String password) throws DataAccessException,

BadCredentialsException;

}

这个接口与AuthenticationDao接口类似，惟一的不同之处在于，由于我们期望这个DAO不但要获取用户信息，还需要完成实际的身份验证，它的loadUserByUsernameAndPassword()方法需要一个String类型的password参数，并且在身份验证失败时会抛出一个BadCredentialsException异常。

与你在本章中将要看到的其他大多数Acegi接口不同的是，Acegi的最新版本(0.6.1版)没有提供PasswordAuthenticationDao接口的任何实用的实现。但是，你不需要付出很多努力去找一个实现类。在我们写作本章的时候，Acegi的CVS沙箱[2]中包含了LdapPasswordAuthenticationDao，一个提供LDAP身份验证的PasswordAuthenticationDao的实现类。它目前还不是Acegi的官方组成部分，但如果你想将它从沙箱中取出来并让它开始工作，只需要按照如下方式声明一个passwordAuthenticationDao Bean即可：

<bean id="passwordAuthenticationDao" class="net.sf.acegisecurity.

➥providers.dao.ldap.LdapPasswordAuthenticationDao">

<property name="host">

<value>security.springinaction.com</value>

</property>

<property name="port">

<value>389</value>

</property>

<property name="rootContext">

<value>DC=springtraining,DC=com</value>

</property>

<property name="userContext">

<value>CN=user</value>

</property>

<property name="rolesAttributes">

<list>

<value>memberOf</value>

<value>roles</value>

</list>

</property>

</bean>

LdapPasswordAuthenticationDao有一些用于指导它根据LDAP服务器进行身份验证的属性。其中，惟一必须指定的属性是host，它指定了LDAP服务器的主机名。但你很可能需要调整其他一个或多个属性。

属性port指定了LDAP服务器监听的端口。该属性的默认值是389（LDAP的著名端口），但为了更好地说明port属性的含义，我们在这里显式地设置了端口的值。

属性rootContext代表LDAP的根上下文。该属性的默认值为空，所以你很可能需要重载它。下图显示了如何使用rootContext属性（以及host和port属性）来构造一个LDAP服务器的提供者URL。

Ldap://security.springinaction.com:389/DC=springtraining,DC=com

属性userContext规定了保存用户信息的LDAP上下文。该属性的默认值是CN=Users，但我们在这里用CN=User重载了这个值。rootContext、userContext和userName共同构成用户的主体。

CN=bauerj, CN=user, DC=springtraining, DC=com

最后，属性rolesAttributes允许你列出一个或多个与LDAP条目关联的用于保存用户角色的属性。默认地，该属性只包括一项memberOf，但我们在列表中增加了roles属性。

关于roles属性要注意的一个重要之处是当LdapPasswordAuthenticationDao从LDAP中获取属性时，它会自动为属性值加上ROLE_前缀。当我们在第11.3.2节中讨论角色投票者时，你将会看到这个前缀的用途。


11.2.4 基于Acegi和Yale CAS实现单次登录 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.2.4 基于Acegi和Yale CAS实现单次登录

你有多少个密码？如果你和大多数人一样，你很可能使用十余个乃至更多的密码来登录日常访问的各种系统。保管所有这些密码是一个挑战，而被迫登录多个系统则令人厌烦。如果能够只登录一次就自动登录了所有需要使用的系统，那该有多好。

单次登录（Single Sign On，SSO）是一个热门的安全话题。这个名字就已经表达了一切：一次登录，访问一切。耶鲁大学技术和规划组已经创建了一个名为中心认证服务（Central Authentication Service，CAS）的优秀SSO解决方案，它可以和Acegi共同工作。

关于设置和使用CAS的细节远远超出了本书的范围。但我们将讨论CAS采用的基本身份验证方式，并探讨如何与CAS一起使用Acegi。如果想知道关于CAS的更多信息，我们强烈推荐你访问CAS的主页http://tp.its.yale.edu/tiki/tiki-index.php?page= CentralAuthenticationService。

为了理解Acegi在一个CAS身份验证应用中的作用，重要的是先理解一个典型的CAS身份验证场景是如何工作的。考虑一个请求受保护服务的流程，如图11.5所示。

图11.5 使用Yale CAS保护一个应用

当Web浏览器请求一个服务时①，服务通过在请求中寻找一个CAS票据来判断用户身份是否已通过验证。如果未找到票据，则意味着该用户尚未通过身份验证。作为结果，用户被重定向到CAS的登录页面②。

在CAS的登录页面，用户输入他/她的用户名和密码。如果CAS成功认证了该用户，则创建一个与请求的服务相关联的票据。接着，CAS服务器将用户重定向到用户原先请求的服务（此时请求中已经有票据了）③。

服务再次在请求中寻找票据。这一次它找到了票据，并与CAS服务器联系以确认票据是有效的④。如果CAS的响应表明票据对当前请求的服务而言是有效的，服务就会允许用户访问应用。

以后，当用户请求访问另一个支持CAS的应用系统时，那个应用仍会与CAS联系。由于用户之前已经认证，CAS会返回一个对该新应用有效的服务票据，而不会提示用户再次登录。

你应该理解的关于CAS的关键概念之一是：被保护的应用从不处理用户的凭证。当用户被提示登录应用系统时，他们实际上是登录到了CAS服务器。应用系统自已从来没有看到过用户的凭证。应用系统使用的惟一安全形式是通过询问CAS服务器来验证用户的票据。这意味着只有一个应用（即CAS服务器）负责处理用户认证，因此是非常好的解决方案。

当Acegi与CAS共同使用时，它承担着帮助应用系统向CAS服务器验证CAS票据的任务。这就使应用系统本身从CAS认证过程中解脱了出来。

Acegi通过使用CasAuthenticationProvider来完成这一任务。这是一个不关心用户名和密码的认证提供者，它只接受CAS票据作为凭证。你可以在Spring配置文件中按如下方式配置一个CasAuthenticationProvider Bean：

<bean id="casAuthenticationProvider"

➥class="net.sf.acegisecurity.providers.cas.CasAuthenticationProvider">

<property name="ticketValidator">

<ref bean="ticketValidator"/>

</property>

<property name="casProxyDecider">

<ref bean="casProxyDecider"/>

</property>

<property name="statelessTicketCache">

<ref bean="statelessTicketCache"/>

</property>

<property name="casAuthoritiesPopulator">

<ref bean="casAuthoritiesPopulator"/>

</property>

<property name="key">

<value>some_unique_key</value>

</property>

</bean>

正如你所看到的，CasAuthenticationProvider是通过和其他若干个Bean相互合作来完成它的工作的。这些Bean中的第一个是ticketValidator Bean，它被装配到ticketValidator属性中。在Spring配置文件中它是这样声明的：

<bean id="ticketValidator" class="net.sf.acegisecurity.

➥providers.cas. ticketvalidator.CasProxyTicketValidator">

<property name="casValidate">

<value>https://localhost:8443/cas/proxyValidate</value>

</property>

<property name="serviceProperties">

<ref bean="serviceProperties"/>

</property>

</bean>

CasProxyTicketValidator通过联系CAS服务器来验证CAS服务票据。属性casValidate指定了CAS服务器处理验证请求的URL。

配置中引用的serviceProperities Bean中包含了与CAS相关的Bean的重要配置信息：

<bean id="serviceProperties"

class="net.sf.acegisecurity.ui.cas.ServiceProperties">

<property name="service">

<value>https://localhost:8443/training/

➥j_acegi_cas_security_check</value>

</property>

</bean>

属性service指定了一个URL，CAS在用户成功登录之后应该将用户重定向至该URL。以后，在第11.4.3节中，你会看到该URL是如何被服务的。

回到casAuthenticationProvider Bean，属性casProxyDecider装配了一个指向casProxyDecider Bean的引用，即一个到类型为net.sf.acegisecurity.providers.cas. CasProxyDecider的Bean的引用。为了理解casProxyDecider Bean的作用，你必须理解CAS如何支持代理服务。

CAS支持代理服务的概念，代理服务帮助另一个应用程序实现用户的身份验证。一个典型的代理服务的例子是门户，门户帮助由它所代表的portlet应用程序完成用户身份验证。当用户登录到一个门户时，门户通过代理票据也确保用户隐含地登录到它包含的应用系统中。

CAS如何处理代理票据是一个高级话题。我们建议你查阅CAS的文档（http://tp.its.yale.edu/tiki/tiki-index.php?page=CasTwoOverview）获取关于代理票据的更详细的信息。在这里，我们只需要指出CasProxyDecider负责决定是否接受代理票据。Acegi提供了CasProxyDecider的三个实现类：

n AcceptAnyCasProxy——接受来自任何服务的代理请求；

n NamedCasProxyDecider——接受来自一个已命名服务的列表的代理请求；

n RejectProxyTickets——拒绝任何代理请求。

为简单起见，让我们假设你的应用系统不涉及代理服务。在这种情况下，RejectProxyTicket就成为对casProxyDecider Bean来说最合适的CasProxyDecider：

<bean id="casProxyDecider"class="net.sf.acegisecurity.

➥ providers.cas.proxy.RejectProxyTickets"/>

属性statelessTicketCache用于支持无状态的客户端（比如远程服务的客户端），它们无法在HttpSession中存储CAS票据。不幸的是，即使没有无状态客户端要访问你的应用系统，statelessTicketCache属性也是必不可少的。Acegi仅仅提供了一个实现，所以声明一个statelessTicketCache相当简单：

<bean id="statelessTicketCache"class="net.sf.acegisecurity.

➥ providers.cas.cache.EhCacheBasedTicketCache">

<property name="minutesToIdle"><value>20</value></property>

</bean>

最后一个与CasAuthenticationProvider协同工作的Bean是casAuthoritiesPopulator Bean。作为一个SSO实现，CAS只负责身份验证——它不关心权限是如何分配给用户的。为了弥补这一差距，你需要一个net.sf.acegisecurity.providers.cas.CasAuthoritiesPopulator Bean。

Acegi只提供了一个CasAuthoritiesPopulator的实现。DaoCasAuthoritiesPopulator使用一个认证DAO（如11.2.2节中讨论的）从数据库中加载用户明细信息。可以这样声明一个casAuthoritiesPopulator Bean：

<bean id="casAuthoritiesPopulator" class="net.sf.acegisecurity.

➥providers.cas.populator.DaoCasAuthoritiesPopulator">

<property name="authenticationDao">

<ref bean="inMemoryDaoImpl"/>

</property>

</bean>

最后，CasAuthenticationManager的key属性指定了一个 String 值，认证管理器使用该字符串来识别之前已经认证的标志。你可以把这个属性设为任意值。

关于使用CAS和Acegi实现SSO还有比CasAuthenticationManager更多的内容。我们仅仅讨论了CasAuthenticationProvider是如何进行身份验证的。在第11.4.3节中，你将看到当CasAuthenticationManager验证用户身份失败时，是如何将用户重定向到CAS登录页面的。

但就现在而言，让我们先看一下Acegi是如何判断一个己通过身份验证的用户是否拥有访问受保护资源所需的恰当权限的。


11.3 控制访问 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.3 控制访问

身份验证只是Acegi安全保护机制的第一步。一旦Acegi知道用户的身份，它必须决定是否允许用户访问由它保护的资源。这就引出了访问决策管理器。

正如认证管理器负责确定用户的身份，访问决策管理器负责决定用户是否有恰当的权限访问受保护的资源。一个访问决策管理器是由net.sf.acegisecurity.AccessDecisionManager接口定义的：

public interface AccessDecisionManager {

public void decide(Authentication authentication, Object object,

ConfigAttributeDefinition config)

throws AccessDeniedException;

public boolean supports(ConfigAttribute attribute);

public boolean supports(Class clazz);

}

supports()方法根据受保护资源的类以及它的配置属性（受保护资源的访问需求）判断该访问管理器是否能够做出针对该资源的访问决策。decide()方法是完成最终决策的地方。如果它没有抛出AccessDeniedException而返回，则允许访问受保护的资源。否则，访问被拒绝。


11.3.1 访问决策投票 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.3.1 访问决策投票

编写一个你自己的AccessDecisionManager看上去是非常简单的。但是，为什么做那些你本来用不着亲自做的事？Acegi提供了适用于大多数情形的AccessDecisionManager的三个实现类：

n net.sf.acegisecurity.vote.AffirmativeBased

n net.sf.acegisecurity.vote.ConsensusBased

n net.sf.acegisecurity.vote.UnanimousBased

这三个访问决策管理器的名字都相当奇怪，但当你考察过Acegi的授权策略之后就会明白它们的意思。

Acegi的访问决策管理器负责最终决定一个通过身份验证的用户是否拥有访问权限。然而，它们不是完全靠自己而完成这一决策的，而是通过征询一个或多个对某用户是否有权访问受保护资源进行投票的对象。一旦获得所有的投票结果，决策管理器统计得票情况，并完成最终决策。

区分不同的访问决策管理器的是它们如何计算出最终的决策。表11.2描述了每一个认证决策管理器是如何决定是否允许访问的。

表11.2 Acegi的访问决策管理器如何计票

访问决策管理器 | 如 何 决 策
AffirmativeBased | 当至少有一个投票者投允许访问票时允许访问
ConsensusBased | 当所有投票者都投允许访问票时允许访问
UnanimousBased | 当没有投票者投拒绝访问票时允许访问

在Spring配置文件中，所有的访问决策管理器都是以相同的方式进行配置的。例如，以下一段XML摘要配置了一个UnanimousBased访问决策管理器：

<bean id="accessDecisionManager"

class="net.sf.acegisecurity.vote.UnanimousBased">

<property name="decisionVoters">

<list>

<ref bean="roleVoter"/>

</list>

</property>

</bean>

你可以通过decisionVoters属性为访问决策管理器提供一组投票者。在上述情况中，只有一个投票者，它引用了一个名为roleVoter的Bean。让我们看一下roleVoter是如何配置的。


11.3.2 决定如何投票 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.3.2 决定如何投票

尽管访问决策投票者对是否授权访问某个受保护资源没有最终发言权，它们在访问决策过程中扮演了重要的角色。一个访问决策投票者的工作是同时考虑用户已拥有的授权和受保护资源的配置属性中要求的授权。基于这一信息，访问决策投票者通过投票为访问决策管理器做出决策提供支持。

一个访问决策投票者是任何实现了net.sf.acegisecurity.vote.AccessDecisionVoter接口的对象：

public interface AccessDecisionVoter {

public static final int ACCESS_GRANTED = 1;

public static final int ACCESS_ABSTAIN = 0;

public static final int ACCESS_DENIED = -1;

public boolean supports(ConfigAttribute attribute);

public boolean supports(Class clazz);

public int vote(Authentication authentication, Object object,

ConfigAttributeDefinition config);

}

可以看到，AccessDecisionVoter接口和AccessDecisionManager接口非常相似。最大的区别在于，AccessDecisionVoter没有AccessDecisionManager接口的返回类型为void的decide()方法，而是有一个返回int的vote()方法。这是因为访问决策投票者并不决定是否允许访问，它仅仅就是否允许访问投出它自己的一票。

在获得投票机会时，访问决策投票者能够以下面三种方式进行投票：

n ACCESS_GRANTED——投票者希望允许访问受保护的资源

n ACCESS_DENIED——投票者希望拒绝对受保护资源的访问

n ACCESS_ABSTAIN——投票者不关心

与大多数Acegi的组件相同，你可以自由地编写自己的AccessDecisionVoter的实现类。然而，Acegi提供了一个很实用的投票者实现类RoleVoter，它当受保护资源的配置属性代表一个角色时进行投票。说得更具体一些，RoleVoter当受保护资源有一个名字由ROLE_开始的配置属性时参与投票。

RoleVoter决定投票结果的方式是简单地将受保护资源的所有配置属性（以ROLE_作为前缀）与认证用户的所有授权进行比较。如果RoleVoter发现其中有一个是匹配的，则它投ACCESS_GRANTED票。否则，它将投ACCESS_DENIED票。

RoleVoter只在访问所需的授权不是以ROLE_为前缀时放弃投票。例如，如果受保护的资源仅仅需要一个非角色的授权（诸如CREATE_USER），则RoleVoter将放弃投票。

你可以在Spring配置文件通过以下方式配置一个RoleVoter：

<bean id="roleVoter"

class="net.sf.acegisecurity.vote.RoleVoter"/>

如前所述，RoleVoter只在受保护资源有以ROLE_为前缀的配置属性才进行投票。然而，ROLE_前缀只是默认值。你可以选择通过设置rolePrefix属性来重载这个默认前缀：

<bean id="roleVoter"

class="net.sf.acegisecurity.vote.RoleVoter">

<property name="rolePrefix">

<value>GROUP_</value>

</property>

</bean>

在这里，默认的前缀被重载为GROUP_。因此，这个RoleVoter现在将只针对以GROUP_为前缀的权限进行授权投票。


11.3.3 处理投票弃权 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.3.3 处理投票弃权

在知道了任何一个投票者都可能投允许、拒绝或弃权票之后，现在你可能会问：如果所有投票者都投弃权票，会发生什么？用户究竟会被授权还是拒绝访问？

默认地，当全部投票者都投弃权票时，所有的访问决策管理者都将拒绝访问资源。然而，你可以通过设置访问决策管理者的allowIfAllAbstain属性为true来重载这个默认行为：

<bean id="accessDecisionManager"

class="net.sf.acegisecurity.vote.UnanimousBased">

<property name="decisionVoters">

<list>

<ref bean="roleVoter"/>

</list>

</property>

<property name="allowIfAllAbstain">

<value>true</value>

</property>

</bean>

通过设置allowIfAllAbstain为true，你建立了一个“沉默即同意”的策略。换句话说，如果所有的投票者都放弃投票，则如同它们都投赞成票一样，访问被授权。

现在，你已经看到Acegi的身份验证和访问控制管理器是如何工作的了，让我们把它们投入使用。在下一节，你将看到如何使用Acegi的一组Servlet过滤器来保护一个Web应用程序。再接下来，在第11.5节，我们将深入到一个应用系统内部，考察如何使用Spring AOP在方法调用的级别上实施保护。


11.4 保护Web应用程序 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4 保护Web应用程序

Acegi对Web安全性的支持大量地依赖Servlet过滤器。这些过滤器拦截对应用系统的请求，并且在应用系统真正处理请求之前进行某些安全处理。Acegi提供了若干个过滤器，能够拦截Servlet请求，并将这些请求转给认证和访问决策管理器处理，从而强制安全性。根据你的需要，你可以使用多达6个的过滤器来保护你的应用系统。表11.3描述了Acegi的每个过滤器。

表11.3 Acegi的Servlet过滤器

过 滤 器 | 目 的
通道处理过滤器 | 确保请求是在安全通道（比如HTTPS）之上传输的
认证处理过滤器 | 接受认证请求，并将它们转交给认证管理器进行身份验证
CAS处理过滤器 | 接受CAS服务票据，验证Yale CAS是否已经对用户进行了认证
HTTP基本授权过滤器 | 处理使用HTTP基本认证的身份验证请求
集成过滤器 | 处理认证信息在请求间的存储（比如在HTTP会话中）
安全强制过滤器 | 确保用户己经认证，并且满足访问一个受保护Web资源的权限需求

当一个请求被提交到一个由Acegi保护的Web应用时，它是按照如下顺序（参见图11.6）逐一通过Acegi过滤器的：

1．如果配置了一个通道处理过滤器，则由它首先处理请求。通道处理过滤器将检查请求的传输通道（通常为HTTP或HTTPS）并判断通道是否满足安全需求。如果不满足，则请求被重定向到同样的URL，但修改通道以使之与安全需求相符。

2．接下来，某个认证处理过滤器（其中包括CAS处理过滤器和HTTP基本授权过滤器）将决定该请求是否是一个认证请求。如果是，则它会从请求中取得恰当的用户信息（通常为用户名/密码），并转交给认证管理器来决定用户的身份。如果这不是一个认证请求，则请求会继续沿着过滤器链向下移动。

图11.6 流经每一个Acegi过滤器的请求

3．集成过滤器试图从某个由它在请求间保持的位置（通常为HTTP会话）中获取用户的认证信息。如果能够找到用户的认证信息，则该信息被放置到一个Context-Holder对象中（基本上就是一个ThreadLocal），以方便所有Acegi组件获取。

4．最后，安全强制过滤器最终决定是否授权用户访问被保护的资源。首先，安全强制过滤器询问认证管理器。如果用户还没有认证成功，则安全强制过滤器会将用户重定向到认证入口点（例如一个登录页面）。下一步，安全强制过滤器将询问访问决策管理器，以决定用户是否拥有合适的可以访问被保护资源的权限。如果没有，则返回一个HTTP 403（禁止访问）消息至用户浏览器。

5．如果用户成功通过了安全强制过滤器的检查，则他/她将可以访问被保护的Web资源。

我们将逐一仔细考察这些过滤器。但在你能够开始使用它们之前，你需要首先学习Acegi是如何对Servlet过滤器使用一个与Spring类似的技巧。


11.4.1 代理Acegi的过滤器 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.1 代理Acegi的过滤器

如果你曾经使用过Servlet过滤器，你知道要让它们生效，必须在Web应用的web.xml文件中使用<filter>和<filter-mapping>元素配置它们。虽然这样做未尝不可，但它与Spring的通过依赖注入的方式配置组件的方式是不一致的。

例如，假设你在web.xml文件中声明了以下过滤器：

<filter>

<filter-name>Foo</filter-name>

<filter-class>FooFilter</filter-class>

</filter>

现在，假设FooFilter需要引用一个Bar Bean来完成它的工作。你该如何向FooFilter中注入Bar的一个实例呢？简单的回答是：你无法做到这一点。web.xml文件没有依赖注入的概念，也没有直接的方式可以从Spring应用上下文中获取Bean并装配到Servlet过滤器中。惟一可以选择的办法是使用Spring的WebApplicationContextUtils从Spring上下文中获取“bar”Bean：

ApplicationContext ctx =

WebApplicationContextUtils.getWebApplicationContext(servletContext);

Bar bar = (Bar) ctx.getBean("bar");

但这种方法的问题是你必须在Servlet过滤器中编写Spring特定的代码。此外，你最终在程序中硬编码了bar Bean的名字。

好在Acegi通过FilterToBeanProxy提供了一种更好的方式。FilterToBeanProxy是一个特殊的Servlet过滤器，它本身做的事并不多，而是将自己的工作委托给Spring应用上下文中的另一个Bean来完成。被委托的Bean和其他的Servlet过滤器一样，实现了javax.servlet.Filter接口，但它是通过Spring配置文件而不是web.xml配置的。

通过使用FilterToBeanProxy，你能够在Spring中配置真正的过滤器，充分利用Spring依赖注入的优势。如图11.7所示，web.xml文件只包含FilterToBeanProxy的<filter>声明。真正的FooFilter是在Spring配置文件中配置的，并且它使用了setter注入将一个Bar Bean的引用设置到了bar属性中。

图11.7 FilterToBeanProxy代理过滤器处理对Spring应用上下文中的一个委托过滤器Bean

要使用FilterToBeanProxy，必须在web.xml文件中建立一个<filter>条目。例如，如果要使用FilterToBeanProxy配置一个FooFilter，你可能使用以下代码：

<filter>

<filter-name>Foo</filter-name>

<filter-class>net.sf.acegisecurity.util.

➥FilterToBeanProxy</filter-class>

<init-param>

<param-name>targetClass</param-name>

<param-value>

FooFilter

</param-value>

</init-param>

</filter>

在这里，targetClass初始化参数被设置为被委托的过滤器Bean的完整类名。当这个FilterToBeanProxy过滤器被初始化时，它会在Spring上下文中找一个类型为FooFilter的Bean，并且把自已的过滤工作委托给在Spring上下文中找到的FooFilter Bean来完成：

<bean id="fooFilter"

class="FooFilter">

<property name="bar">

<ref bean="bar"/>

</property>

</bean>

如果找不到FooFilter Bean，则会抛出一个异常。如果找到了多于一个的相匹配的Bean，则会使用第一个Bean。

可选地，你也可以通过设置targetBean初始化参数而不是targetClass从Spring上下文中选取一个特定的Bean。例如，通过以下方式设置targetBean属性，你可以根据名字选取fooFilter Bean：

<filter>

<filter-name>Foo</filter-name>

<filter-class>net.sf.acegisecurity.

➥util.FilterToBeanProxy</filter-class>

<init-param>

<param-name>targetBean</param-name>

<param-value>fooFilter</param-value>

</init-param>

</filter>

targetBean初始化参数使你能够更具体地指定将过滤工作委托给哪个Bean来完成，但需要你精确匹配web.xml和Spring配置文件中受委托的Bean的名字。当你决定重命名Bean时，这会带来额外的工作量。出于这个原因，使用targetClass而不是targetBean很可能是更好的选择。

无论你选择targetClass还是targetBean，FilterToBeanProxyx必须能够访问Spring应用上下文。这就意味着必须使用Spring的ContetLoaderListener或ContextLoaderServlet（参见第8章）加载Spring上下文。

最后，你可能需要将过滤器与一个URL模式相关联。下面的<filter-mapping>将FilterToBeanProxy的Acegi-Authentication实例与URL模式/*相关联，从而所有的请求都将被该过滤器处理：

<filter-mapping>

<filter-name>Acegi-Authentication</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

/*是所有Acegi过滤器推荐的URL模式。这样做的意图是Acegi应该拦截所有请求，并由其依赖的安全管理器来决定是否和如何保护该请求。

注意：值得注意的是，FilterToBeanProxy中并没有任何与Acegi或与保护Web应用程序相关的特定部分。你会发现当配置你自己的Servlet过滤器时，FilterToBeanProxy也是非常有用的。事实上，由于它是如此有用，在Spring开发者邮件列表的一些讨论中，有人提议可以把FilterToBeanProxy从Acegi中移出，并放到核心Spring项目的某个后续版本中。

现在你已经知道如何使用FilterToBeanProxy了，你已经准备就绪，可以开始用它来建立与Acegi安全相关的Web组件了。让我们从Acegi安全的主要过滤器——安全强制过滤器——开始。


11.4.2 强制Web安全性 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.2 强制Web安全性

每当用户请求你的Web应用中的一个页面时，这个页面可能是受保护的，也可能不是。在Acegi中，一个安全强制过滤器负责拦截请求，判断请求是否安全，并且给予认证和访问决策管理器一个机会来验证用户的身份和权限。在Spring配置文件中，安全强制过滤器是通过以下方式在Spring配置文件中声明的：

<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.

➥intercept.web.SecurityEnforcementFilter">

<property name="securityInterceptor">

<ref bean="securityInterceptor"/>

</property>

<property name="authenticationEntryPoint">

<ref bean="authenticationEntryPoint"/>

</property>

</bean>

在这里，SecurityEnforcementFilter装配了另外两个Bean的引用：authenticationEntryPoint和securityInterceptor。我们将在稍候更详细地讨论authenticationEntryPoint属性。现在，让我们首先关注securityInterceptor属性。

使用一个过滤器安全拦截器

属性securityInterceptor装配了一个指向同名的Bean的引用。如果你回想起本章开始时的门锁比喻，安全拦截器就如同开门前必须被释放的锁舌。正是它使认证管理器、访问决策管理器和运行身份管理器能够协同工作。

为了保证Web安全，Acegi的FilterSecurityInterceptor类负责执行安全拦截器的工作。它是通过以下方式在Spring配置文件中声明的：

<bean id="securityInterceptor" class="net.sf.acegisecurity.

➥intercept.web.FilterSecurityInterceptor">

<property name="authenticationManager">

<ref bean="authenticationManager"/>

</property>

<property name="accessDecisionManager">

<ref bean="accessDecisionManager"/>

</property>

<property name="objectDefinitionSource">

<value>

CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON

\A/admin/.*\Z=ROLE_ADMIN

\A/student/.*\Z=ROLE_STUDENT,ROLE_ALUMNI

\A/instruct/.*\Z=ROLE_INSTRUCTOR

</value>

</property>

</bean>

在这里装配的前两个属性是指向本章之前定义的认证管理器和访问决策管理器Bean的引用。安全拦截器使用认证管理器来决定用户是否已登录，并获取用户已拥有的授权。它使用访问决策管理器来判断用户是否拥用合适的授权来访问受保护的资源。

属性objectDefinitionSource告诉安全拦截器被拦截的各种请求所需要的授权是什么。该属性有一个对应的属性编辑器，因此可以方便地作为一个字符串值进行配置。该字符串包括若干行，每一行可以是一个指令，或者一个从URL到授权的映射。

从上面的XML摘要中可以看到，属性objectDefinitionSource的值的第一行是一个指令，表明在比较请求的URL在和紧跟其后定义的模式之前必须首先正规化为小写字母。该属性的其余几行将URL模式映射为允许用户访问这些URL时必须授予用户的权限。可以看到，URL模式是以正则表达式的形式描述的。因此，securityInterceptor Bean的objectDefinitionSource属性定义了：

n /admin/reports.htm 要求用户拥有 ROLE_ADMIN 权限；

n /student/manageSchedule.htm 要求用户拥有 ROLE_STUDENT 或 ROLE_ALUMNI 权限；

n /instruct/postCourseNotes.htm 要求用户拥有 ROLE_INSTUCTOR 权限。

如果愿意，你也可以使用与Ant相似的URL模式而不是正则表达式，只需在objectDefinitionSource属性中增加一条PATTERN_TYPE_APACHE_ANT指令即可。例如，以下的objectDefinitionSource属性定义与上面给出的那个定义是等价的：

<property name="objectDefinitionSource">

<value>

CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON

PATTERN_TYPE_APACHE_ANT

/admin/**=ROLE_ADMIN

/student/**=ROLE_STUDENT,ROLE_ALUMNI

/instruct/**=ROLE_INSTRUCTOR

</value>

</property>

与所有Acegi的过滤器一样，安全强制过滤器是一个由FilterToBeanProxy作为前置的过滤委托Bean。这意味着配置一个安全强制过滤器的第一步是在应用的web.xml文件中为FilterToBeanProxy增加<filter>和<filter-mapping>元素：

<filter>

<filter-name>Acegi-Security</filter-name>

<filter-class>net.sf.acegisecurity.util.

➥FilterToBeanProxy</filter-class>

<init-param>

<param-name>targetBean</param-name>

<param-value>securityEnforcementFilter</param-value>

</init-param>

</filter>

…

<filter-mapping>

<filter-name>Acegi-Security</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

值得注意的是，<filter-mapping>的<url-pattern>将所有请求映射到安全强制过滤器之上。这是Acegi过滤器配置的典型做法。这样做的意图是对所有的请求进行过滤，由安全强制过滤器通过对象定义源属性来决定是否需要进行实际的处理。

注意：本节由始至终，你会在web.xml中增加若干个<filter>和<filter-mapping>元素，它们都使用FilterToBeanProxy。尽管过滤器类都是一样的，但不要认为它们之间可以相互替代。尽管这些过滤器都使用同一个FilterToBeanProxy类，它们服务于不同的目的，委托Spring应用上下文中不同的Bean来完成各自的工作。除非特别说明，它们都是Acegi的安全保护机制中不可或缺的部分。

假设用户提交了一个到指定受到保护的页面的请求。如果用户已经通过身份验证并且拥有恰当的权限，则安全强制过滤器会允许用户访问该页面。但如果用户尚未通过身份验证呢？


11.4.3 处理登录 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.3 处理登录

回想一下，上一节的安全强制过滤器装配了一个指向authenticationEntryPoint的引用。当安全强制过滤器判断出一个用户尚未通过身份验证时，会将控制转交到一个认证入口点。

认证入口点的主要目的是提示用户登录。Acegi提供了三个现成的认证入口点：

n BasicProcessingFilterEntryPoint——通过向浏览器发送一个HTTP 401（未授权）消息，由浏览器弹出登录对话框，提示用户登录；

n AuthenticationProcessingFilterEntryPoint——将用户重定向到一个基于HTML表单的登录页面；

n CasProcessingFilterEntryPoint——将用户重定向至一个Yale CAS登录页面。

无论使用哪种认证入口点，都会提示用户需要提供用户名/密码来标识他/她自己的身份。当用户提交用户名和密码之后，Acegi需要通过某种方式给予认证管理器一个验证用户身份的机会。

处理认证请求的工作由认证处理过滤器负责完成。Acegi提供了三个认证处理过滤器：

n BasicProcessingFilter——处理HTTP基本身份验证请求；

n AuthenticationProcessingFilter——处理基于表单的身份验证请求；

n CasProcessingFilter——基于CAS服务票据的存在性和有效性验证用户身份。

可以看出，三种认证处理过滤器和三种认证入口点是一一对应的。事实上，每一种认证入口点都需要和对应的认证处理过滤器配合才能完成身份验证的完整过程。图11.8展示了这一协作过程。

图11.8 认证入口点和认证处理过滤器协同工作以验证Web用户的身份

认证入口点通过提示用户登录启动身份验证过程。当用户提交认证所需的信息，认证处理过滤器试图验证该用户的身份（在认证管理器的帮助下）。

让我们更深入地考察一下Acegi中提供的三种身份验证方式是如何工作的。首先由基本身份验证开始。

基本身份验证

基于Web的各种身份验证方式中最简单的形式称为基本身份验证。基本身份验证的工作机制是服务器发送一个HTTP 401（未授权）响应至Web浏览器。当浏览器看到这个响应，它就可以知道服务器需要用户进行登录。于是，浏览器弹出一个对话框提示用户输入用户名和密码。

当用户提交登录请求时，浏览器将请求送回至服务器进行身份验证。如果验证成功，用户会被送往他期望访问的目标URL。否则，服务器会再回送一个HTTP 401响应，并且浏览器会再次提示用户登录。

在Acegi中使用基本身份验证，需要先配置一个BasicProcessingFilterEntryPoint Bean：

<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.

➥ ui.basicauth.BasicProcessingFilterEntryPoint">

<property name="realmName">

<value>Spring Training</value>

</property>

</bean>

BasicProcessingFilterEntryPoint只有一个属性。属性realmName指定了一个任意字符串，该字符串会显示在登录对话框上，用于提示用户为什么需要登录。

当用户点击登录对话框上的“确定”按钮，用户名和密码会通过HTTP头提交回服务器端。这时BasicProcessFilter就可以取得用户名和密码并进行处理。

<bean id="basicProcessingFilter" class="net.sf.acegisecurity.

➥ui.basicauth.BasicProcessingFilter">

<property name="authenticationManager">

<ref bean="authenticationManager"/>

</property>

<property name="authenticationEntryPoint">

<ref bean="authenticationEntryPoint"/>

</property>

</bean>

BasicProcessingFilter从HTTP头中取得用户名和密码，将它们转交给通过authenticationManager属性装配的认证管理器。如果认证成功，会将一个Authentication对象放到会话中供以后引用。否则，如果认证失败，会将控制转交给认证入口点（通过authenticationEntryPoint属性装配），给予用户一个再次登录的机会。

与Acegi的所有过滤器一样，BasicProcessingFilter需要一个在web.xml中配置的FilterToBeanProxy：

<filter>

<filter-name>Acegi-Authentication</filter-name>

<filter-class>net.sf.acegisecurity.

➥util.FilterToBeanProxy</filter-class>

<init-param>

<param-name>targetBean</param-name>

<param-value>

net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter

</param-value>

</init-param>

</filter>

…

<filter-mapping>

<filter-name>Acegi-Authentication</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

基于表单的身份验证

尽管基本身份验证对简单的应用而言很不错，但它有一些局限性。主要原因在于浏览器弹出的对话框对用户而言既不友好，又不美观。基于表单的身份验证克服了这个局限，对大多数应用而言更加合适。用户不用面对弹出式的登录对话框，而是可以通过一个基于Web的表单进行登录。

Acegi的AuthenticationProcessingFilterEntryPoint是一个提供给用户基于HTML的登录表单的认证入口点。你可以在Spring配置文件中用以下方式来配置它：

<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.

➥ui.webapp. AuthenticationProcessingFilterEntryPoint">

<property name="loginFormUrl">

<value>/jsp/login.jsp</value>

</property>

<property name="forceHttps"><value>true</value></property>

</bean>

属性loginFromUrl配置了一个登录表单的URL。当需要用户登录时，AuthenticationProcessingFilterEntryPoint会将用户重定向到该URL。根据上述配置中，它会重定向到一个JSP文件，JSP中可能包含下面的HTML表单：

<form method="POST" action="j_acegi_security_check">

<input type="text" name="j_username"><br>

<input type="password" name="j_password"><br>

<input type="submit">

</form>

这个登录表单中必须有两个名为j_username和j_password的域，供用户输入用户名和密码。表单的action属性被设置为j_acegi_security_check，这会被AuthenticationProcessingFilter拦截。

AuthenticationProcessingFilter是处理基于表单身份验证的过滤器。在Spring配置文件中它是以如下方式配置的：

<bean id="authenticationProcessingFilter"

class="net.sf.acegisecurity.

➥ ui.webapp.AuthenticationProcessingFilter">

<property name="filterProcessesUrl">

<value>/j_acegi_security_check</value>

</property>

<property name="authenticationFailureUrl">

<value>/jsp/login.jsp?failed=true</value>

</property>

<property name="defaultTargetUrl">

<value>/</value>

</property>

<property name="authenticationManager">

<ref bean="authenticationManager"/>

</property>

</bean>

属性filterProcessesUrl告诉AuthenticationProcessingFilter应该拦截哪个URL。这个URL与登录表单中action属性的值一样。它的默认值为/j_acegi_security_check，但我们在这里显式定义了该值，用于说明你可以根据需要改变这个值。

属性authenticationFailureUrl指定当身份验证失败时用户应该被送往哪里。对于上述配置，我们会将用户送回到登录页面，同时传递一个参数用于表明认证失败（因此可以在登录页面上显示一个错误消息）。

正常情况下，当身份验证成功后，AuthenticationProcessingFilter会在HTTP会话中放置一个Authentication对象，并且将用户重定向到他们期望访问的页面。因为SecurityEnforcementFilter在把控制转交给认证入口点之前，已经把原先的目标URL放到HTTP会话中了，所以AuthenticationProcessingFilter知道用户期望访问的页面是什么。当AuthenticationProcessingFilter 成功验证用户身份之后，它会从HTTP会话中取得目标URL并将用户重定向到该URL。

属性defaultTargetUrl定义了当出现目标URL不在HTTP会话中的异常情况时将发生什么。这可能发生在用户通过浏览器书签或其他方式而不是通过SecurityEnforcementFilter到达登录页面的情况下。

在Spring中定义了AuthenticationProcessingFilter之后，最后一件需要做的事就是配置一个FilterToBeanProxy，它能够将过滤处理委托给authenticationProcessingFilter Bean：

<filter>

<filter-name>Acegi-Authentication</filter-name>

<filter-class>net.sf.acegisecurity.

➥ util.FilterToBeanProxy</filter-class>

<init-param>

<param-name>targetClass</param-name>

<param-value>

net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter

</param-value>

</init-param>

</filter>

…

<filter-mapping>

<filter-name>Acegi-Authentication</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

CAS身份验证

在第11.2.4节中，你已经看到如何配置一个CasAuthenticationManager，从而可以根据CAS服务器来认证CAS的服务票据。但那一节还遗留着一个有待回答的大问题：如何首先将用户送到CAS登录页面。

Acegi的CasProcessingFilterEntryPoint是一个能够将用户送到CAS服务器进行登录的认证入口点。你可以在Spring配置文件中按以下方式声明它：

<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.

➥ui.cas.CasProcessingFilterEntryPoint">

<property name="loginUrl">

<value>https://localhost:8443/cas/login</value>

</property>

<property name="serviceProperties">

<ref bean="serviceProperties"/>

</property>

</bean>

CasProcessingFitlerEntryPoint中两个属性的意思是不言自明的。属性loginUrl指定了CAS登录页面的URL，而属性serviceProperties是指向在11.2.4节中声明的serviceProperties Bean的引用。

无论用户是否成功登录CAS，你必须首先明确一点，CasAuthenticationManager总是会在允许用户访问受保护资源之前尝试验证CAS票据的真实性。CasProcessingFilter是一个认证处理过滤器，它会拦截来自CAS服务器的包含待验证票据的请求。

<bean id="authenticationProcessingFilter"

class="net.sf.acegisecurity.ui.cas.CasProcessingFilter">

<property name="filterProcessesUrl">

<value>/j_acegi_cas_security_check</value>

</property>

<property name="authenticationManager">

<ref bean="authenticationManager"/>

</property>

<property name="authenticationFailureUrl">

<value>/authenticationfailed.jsp</value>

</property>

<property name="defaultTargetUrl">

<value>/</value>

</property>

</bean>

CasProcessingFilter和AuthenticationProcessingFilter有相同的属性集。但需要特别注意filterProcessesUrl属性。在这里，它被设置为/j_acegi_cas_security_check。在第11.2.4节中，我们设置serviceProperties Bean的service属性为一个以同样模式结尾的URL。当用户在CAS服务器上登录成功之后，CAS会将用户重定向到一个服务URL。在非Acegi应用系统中，它可以是受保护应用的任意URL。但如果使用Acegi保护一个应用系统，你必须确保首先调用CasAuthenticationManager来处理Acegi这边的身份验证并检查用户的授权。

在CAS服务器那端，serviceProperties Bean的service属性告诉CAS当登录成功之后转向哪里。在客户端，filterProcessesUrl属性确保有一个CasProcessingFilter回答该请求，并将CAS票据转交给CasAuthenticationManager进行验证。


11.4.4 设置一个安全上下文 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.4 设置一个安全上下文

在处理请求的过程中，用户的认证信息是通过一个ContextHolder（实质上是一个ThreadLocal）进行传递的。Acegi过滤器链中的每一个过滤器都通过ContextHolder获取用户的认证信息。

但是，ThreadLocal不能跨越多个请求存在。因此，Acegi必须找到一个方便的位置存放用户的认证信息，从而当下一个请求到来时该信息仍然可用。这就是Acegi的集成过滤器要做的事。

一个集成过滤器的生命周期开始于从一个知名位置查找用户的Authentication对象——通常是HTTP会话。然后，它构造一个新的ContextHolder对象，并且将Authentication对象放到其中。

当请求处理完成之后，集成过滤器从ContextHolder中取得Authentication对象，并将它放回到知名位置中，供下一个请求使用。

Acegi提供了若干个集成过滤器，其中HttpSessionIntegrationFilter适用于大多数情形。它将Authentication对象保存在HTTP会话中，使之能够跨越多个请求。你可以在Spring配置文件中按以下方式配置它：

<bean id="integrationFilter" class="net.sf.acegisecurity.

➥ ui.webapp.HttpSessionIntegrationFilter"/>

最后，你需要在web.xml中配置一个FilterToBeanProxy过滤器，它负责将过滤处理委托给integrationFilter Bean执行：

<filter>

<filter-name>Acegi-Integration</filter-name>

<filter-class>net.sf.acegisecurity.util.FilterToBeanProxy

➥ </filter-class>

<init-param>

<param-name>targetClass</param-name>

<param-value>net.sf.acegisecurity.ui.AutoIntegrationFilter

➥ </param-value>

</init-param>

</filter>

…

<filter-mapping>

<filter-name>Acegi-Integration</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

要把集成过滤器的<filter-mapping>配置项放到所有其他Acegi过滤器的<filter-mapping>的后面，这点很重要。


11.4.5 确保通道安全性 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.5 确保通道安全性

受保护的Web应用系统中的一些页面可能带有敏感信息。如果这些信息是通过一个非安全的通道传输的（比如HTTP），则存在被目无法纪的黑客截取数据并用于破坏性目的的风险。

这方面的常见例子包括登录页面或者输入或显示用户的信用卡信息的页面。万一这些信息被泄露，则某人的私人数据将被用于购买物品，或者用于假冒该用户的身份。要让用户感觉到他们的信息机密性是有保障的，这一点是非常重要的，否则他们会不再使用你的站点。或者更糟糕地，他们会诉诸法律来确保你赔偿了他们的损失。

HTTPS对服务器和浏览器之间传输的数据进行加密，有助于阻止高科技罪犯从互联网上截取敏感数据。一般说来，使用HTTPS就像在URL中使用“https://”而不是“http://”一样简单。然而，这需要你记住每次链接到一个显示敏感数据的页面时都加上“s”。虽然看上去非常简单，但在我们自己的经验中，忘记加上“s”的次数也是数不胜数的。

Acegi通过ChannelProcessingFilter提供了一个解决方案。ChannelProcessingFilter确保Web应用的页面是通过合适的通道（HTTP或HTTPS）传输的——无论你是否记得在链接的URL前加了“https://”。

要使用ChannelProcessingFilter，你必须在Web应用的web.xml文件中再增加一个FilterToBeanProxy配置：

<filter>

<filter-name>Acegi-Channel</filter-name>

<filter-class>net.sf.acegisecurity.util.FilterToBeanProxy

➥ </filter-class>

<init-param>

<param-name>targetClass</param-name>

<param-value>

net.sf.acegisecurity.securechannel.ChannelProcessingFilter

</param-value>

</init-param>

</filter>

…

<filter-mapping>

<filter-name>Acegi-Channel</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

ChannelProcessingFilter的<filter-mapping>定义必须出现在任何其他的<filter-mapping>之前，这点非常重要。这是因为ChannelProcessingFilter需要在允许其他过滤器处理之前确保请求是经过合适的通道传输的。

当你在web.xml中配置了FilterToBeanProxy之后，就需要在Spring配置文件中声明受委托的过滤器Bean：

<bean id="channelProcessingFilter" class="net.sf.acegisecurity.

➥ securechannel.ChannelProcessingFilter">

<property name="filterInvocationDefinitionSource">

<value>

CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON

\A/secure/.*\Z=REQUIRES_SECURE_CHANNEL

\A/login.jsp.*\Z=REQUIRES_SECURE_CHANNEL

\A/j_acegi_security_check.*\Z=REQUIRES_SECURE_CHANNEL

\A.*\Z=REQUIRES_INSECURE_CHANNEL

</value>

</property>

<property name="channelDecisionManager">

<ref bean="channelDecisionManager"/>

</property>

</bean>

属性filterInvocationDefinitionSource用于定义哪个页面是安全的或者是非安全的。和FilterSecurityInterceptor（第11.4.2节）一样，这个属性的值是由一个属性编辑器负责解释的。第一行表明请求的URL在与随后各行中的模式进行比较之前必须首先转换成小写。

第一行之后的每一行都将一个通道规则关联到一个URL模式上。在上述配置中，我们使用正则表达式来定义URL模式，但正如安全拦截器一样，你也可以使用与Ant相似的模式，只需要增加一个PATTERN_TYPE_APACHE_ANT指令即可。

在URL模式之上可以应用两种通道规则：

n REQUIRES_SECURE_CHANNEL——规定与该模式匹配的URL必须经过一个安全通道传输（例如，HTTPS）；

n REQUIRES_INSECURE_CHANNEL——规定与该模式匹配的URL必须经过一个非安全的通道传输（例如，HTTP）。

对于上面的例子，我们定义了登录页面、认证过滤器（/j_acegi_security_check），以及所有位于“/secure”下的页面必须经过一个安全通道进行传输。所有其他的URL必须通过一个非安全的通道进行传输。

ChannelProcessingFilter并非独自完成强制通道安全性的工作的。它和由ChannelDecisionManager属性所引用一个ChannelDecisionManager合作，而ChannelDecisionManager又将责任进一步委托给一个或多个ChannelProcessor。这让人联想起在AccessDecisionManager和AccessDecisionVoters之间的关系。图11.9展示了这种关系。

图11.9 通道处理过滤器依赖通道决策管理器来决定是否要切换到安全通道或非安全通道。
如果需要切换，则由通道处理器执行实际的切换

通道决策管理器负责判断请求URL的通道是否满足通道安全性规则（由ChannelProcessingFilter的filterInvocationDefinitionSource属性定义）。然而，ChannelDecisionManagerImpl作为Acegi惟一内置的ChannelDecisionManager实现的，是把这个决策交给通道处理器来完成。

ChannelDecisionManagerImpl轮询它的所有通道处理器，给予他们重载请求通道的机会。每个通道处理器根据通道安全规则检查请求。一旦某个通道处理器觉得请求的通道有问题，它就会进行重定向以确保请求是足够安全的。

既然你已经看到ChannelProcessingFilter是如何工作的了，现在可以把所有的片断合为一体。正如你先前看到的，channelProcessingFilter Bean的channelDecisionManager属性装配了一个指向channelDecisionManager Bean的引用。这个channelDecisionManager Bean是按照以下方式声明的：

<bean id="channelDecisionManager" class= "net.sf.acegisecurity.

➥ securechannel.ChannelDecisionManagerImpl">

<property name="channelProcessors">

<list>

<ref bean="secureChannelProcessor"/>

<ref bean="insecureChannelProcessor"/>

</list>

</property>

</bean>

ChannelDecisionManagerImpl的通道处理器是通过channelProcessors属性提供的。在上面的例子中，我们为它提供了两个通道处理器，这些通道处理器通过以下XML声明的：

<bean id="secureChannelProcessor" class="net.sf.acegisecurity.

➥ securechannel.SecureChannelProcessor"/>

<bean id="insecureChannelProcessor" class=

"net.sf.acegisecurity.securechannel.InsecureChannelProcessor"/>

SecureChannelProcessor按照以下方式考察与请求的URL相关联的通道安全性规则。如果规则是REQUIRES_SECURE_CHANNEL，而请求是非安全的，则SecureChannelProcessor将请求重定向到一种安全的形式。例如，基于上面给出的channelProcessingFilter Bean的filterInvocationDefinitionSource属性：

http://www.springinaction.com/training/secure/editCourse.htm会被重定向到 https://www.springinaction.com/training/secure/editCourse.htm，因为它与一个关联着REQUIRES_SECURE_CHANNEL规则的URL模式相匹配。

http://www.springinaction.com/training/j_acegi_security_check 会被重定向到 https://www.springinaction.com/training/j_acegi_security_check，因为它与一个关联着REQUIRES_SECURE_CHANNEL规则的URL模式相匹配。

http://www.springinaction.com/training/displayCourse.htm 不会被重定向，因为它匹配的URL模式没有REQUIRES_SECURE_CHANNEL规则。

https://www.springinaction.com/training/j_acegi_security_check 不会被重定向，因为它已经是安全的了。

InsecureChannelProcessor的功能与SecureChannelProcessor正好相反。它不是确保一个请求是通过安全通道传输的，而是确保一个请求是通过非安全的通道传输的。例如：

n https://www.springinaction.com/training/displayCourse.htm 会被重定向为 http://www.springinaction.com/training/displayCourse.htm， 因为它与一个关联REQUIRES_INSECURE_CHANNEL规则的URL模式匹配。

n https://www.springinaction.com/training/j_acegi_security_check 不会被重定向，因为它匹配的URL模式没有关联的REQUIRES_INSECURE_CHANNEL规则。

n http://www.springinaction.com/training.displayCourse.htm 不会被重定向，因为它匹配的URL模式关联一条REQUIRES_INSECURE_CHANNEL规则，而它已经是非安全的了。

在我们告别Acegi对Web安全性的支持之前，让我们看一下如何使用Acegi的标签库在Web应用的页面内部强制安全规则。


11.4.6 使用Acegi的标签库 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.4.6 使用Acegi的标签库

称之为标签库可能有点言过其辞了。实际上，Acegi只提供了一个JSP标签：<authz:authorize>标签。

虽然Acegi的安全强制过滤器能够阻止用户浏览他们没有权限看到的页面，但最好的做法是从一开始就不提供指向受限制页面的链接。<authz:authorize>标签能够根据当前用户是否拥有恰当权限来决定显示或隐藏Web页面的内容。

<authz:authorize>是一个流程控制标签，能够在满足特定安全需求的条件下显示它的内容体。它有三个互斥的参数：

n ifAllGranted——是一个由逗号分隔的权限列表，用户必须拥有所有列出的权限才能渲染标签体；

n ifAnyGranted——是一个由逗号分隔的权限列表，用户必须至少拥有其中的一个才能渲染标签体；

n ifNotGranted——是一个由逗号分隔的权限列表，用户必须不拥有其中的任何一个才能渲染标签体。

你可以轻松地想像在JSP中如何使用<authz:authorize>标签根据用户的权限来限制他们的行为。例如，Spring培训应用有一个向用户显示课程有关信息的课程明细页面。对管理员来说，如果能够从课程明细页面直接跳转到课程编辑页面从而可以更新课程信息是很方便的。但你不希望这个链接对除了管理员之外的其他用户可见。

使用<authz:authorize>标签，在用户没有管理员权限的情况下，你可以避免渲染到课程编辑页面的链接：

<authz:authorize ifAllGranted="ROLE_ADMINISTRATOR">

<a href="admin/editCourse.htm?courseId=${course.id}">

Edit Course

</a>

</authz:authorize>

这里，我们使用了ifAllGranted参数，由于这里只需要检查一个授权，所以ifAllGranted标签也是可以使用的。Web应用的安全性只是Acegi功能的一个方面。现在让我们考察它的另一面——保护方法调用。


11.5 保护方法调用 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.5 保护方法调用

虽然Acegi保护Web请求的手段是使用Servlet过滤器，它却是利用Spring对AOP的支持来提供方法级别的声明式保护的。这意味着不是设置一个SecurityEnforcementFilter来强制安全性，而是设置一个Spring AOP代理来拦截方法调用，并将控制转交给安全拦截器。


11.5.1 创建一个安全切面 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.5.1 创建一个安全切面

也许设置一个AOP代理的最简单的方式是使用Spring的BeanNameAutoProxyCreator，并且简单地列举出你需要保护的Bean[3]。例如，假设你希望保护courseService和billingService Bean：

<bean id="autoProxyCreator" class="org.springframework.aop.framework.

➥ autoproxy.BeanNameAutoProxyCreator">

<property name="interceptorNames">

<list>

<value>securityInterceptor</value>

</list>

</property>

<property name="beanNames">

<list>

<value>courseService</value>

<value>billingService</value>

</list>

</property>

</bean>

在这个例子中，我们要求自动代理创建器autoProxyCreator通过一个名为securityInterceptor拦截器代理它的Bean。该securityInterceptor Bean按照以下方式配置：

<bean id="securityInterceptor" class="net.sf.acegisecurity.

➥intercept.method.MethodSecurityInterceptor">

<property name="authenticationManager">

<ref bean="authenticationManager"/>

</property>

<property name="accessDecisionManager">

<ref bean="accessDecisionManager"/>

</property>

<property name="objectDefinitionSource">

<value>

com.springinaction.springtraining.service.

➥ CourseService.createCourse=ROLE_ADMIN

com.springinaction.springtraining.service.

➥ CourseService.enroll*=ROLE_ADMIN,ROLE_REGISTRAR

</value>

</property>

</bean>

MethodSecurityInterceptor针对方法调用所做的一切与FilterSecurityFilter针对Servlet请求所做的是相同的。具体地说，它拦截调用，并协调认证管理器和访问决策管理器的工作，以确保方法的安全性需求得到满足。

你可以注意到authenticationManager和accessDecisionManager属性与FilterSecurityInterceptor是一样的。事实上，可以把应用于FilterSecurityInterceptor的同一个Bean装配到这些属性中。

MethodSecurityInterceptor也和FilterSecurityInterceptor一样有一个objectDefinitionSource属性。尽管它和FilterSecurityInterceptor的同名属性的目标是一致的，在配置方式上两者还是有些许不同。它不是将URL模式与权限相关联，而是将方法的模式与调用该方法所需要的权限进行关联。

一个方法模式包含全称类名和需要保护的方法的名字。值得注意的是你可以在方法模式的开头和结尾使用通配符以匹配多个方法。

Com.springinaction…CoruseService.enroll *

=ROLE_ADMIN, ROLE_REGISTRAR

当一个受保护的方法被调用时，MethodSecurityInterceptor将判断用户是否已通过身份验证并且拥有合适的授权来调用该方法。如果是，则会继续调用目标方法。如果不是，则会抛出一个AcegiSecurityException。更具体地说，如果用户身份无法得到验证，会抛出一个AuthenticationException，或者，如果用户没有足够权限调用本方法，则会抛出一个AccessDeniedException。

为了与Spring的异常理念相一致，AcegiSecurityException是一个未检查的异常。进行调用的代码可以选择捕捉或者忽略该异常。

在Spring配置文件中规定方法安全属性仅仅是声明方法级别安全性的一种手段。你已经看到如何使用Jakarta Commons Attributes来定义事务策略 (第4章) 和 URL映射 (第8章)了。现在让我们看一下如何使用Jakarta Commons Attributes来声明安全属性。


11.5.2 使用元数据保护方法 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.5.2 使用元数据保护方法

与事务和URL处理器映射机制相同，你首先需要做的是声明一个元数据实现以告诉Spring如何装载元数据。如果现在还没有在应用上下文中增加一个CommonsAttributes Bean，你需要增加一个：

<bean id="attributes"

class="org.springframework.metadata.commons.CommonsAttributes"/>

接下来，你需要声明一个对象定义源。在第11.5.1节中，你通过将objectDefinitionSource属性设置为一个将安全属性映射到方法的字符串定义了一个对象定义源。而现在你将在被保护对象的源代码中直接声明安全属性。Acegi的MethodDefinitionAttributes是一个对象定义源，它能够从受保护对象的元数据中获取它的安全属性：

<bean id="objectDefinitionSource"class="net.sf.acegisecurity.

➥ intercept. method.MethodDefinitionAttributes">

<property name="attributes"><ref bean="attributes"/></property>

</bean>

MethodDefinitionAttributes的attributes属性装配了一个指向attributes Bean的引用，因此它能够知道如何使用Jakarta Commons Attributes来获取安全属性。[4]

现在，属性objectDefinitionSource已经配置好了，我们把它装配到MethodSecurityInterceptor的objectDefinitionSource属性中（代替在11.5.1节中的字符串属性）：

<bean id="securityInterceptor" class="net.sf.acegisecurity.

➥ intercept.method.MethodSecurityInterceptor">

…

<property name="objectDefinitionSource">

<ref bean="objectDefinitionSource"/>

</property>

</bean>

现在可以开始为你的代码加上安全属性标记了。你惟一需要知道的安全属性是SecurityConfig，它在权限和方法之间建立关联。例如，以下的代码段展示了如何标记 CourseService 的enrollStudentInCourse()方法，声明它需要ROLE_ADMIN或ROLE_REGISTRAR属性：

/**

* @@net.sf.acegisecurity.SecurityConfig("ROLE_ADMIN")

* @@net.sf.acegisecurity.SecurityConfig("ROLE_REGISTRAR")

*/

public void enrollStudentInCourse(Course course,

Student student) throws CourseException;

在enrollStudentInCourse方法上声明这些安全属性和第11.5.1节中定义的objectDefinitionSource声明等价的。


11.6 小结 - 《Spring in Action中文版》 - 免费试读 - book.csdn.net

11.6 小结

安全性对于许多应用系统而言都是很重要的方面。Acegi安全系统提供了一种基于Spring的低耦合、依赖注入和面向切面编程的保护应用系统的机制。

你可能已经注意到了本章中只提供了很少的Java代码。希望你不会因此而感到失望。缺少Java代码显示了Acegi的关键强项——应用系统与安全性之间的低耦合。安全性是一个超越应用系统核心关注点的切面。使用Acegi，你可以不需要应用代码中直接编写任何与安全有关的代码而达到保护应用系统的目的。

另一件你可能已经注意到的事，是大多数使用Acegi保护应用系统所需的配置并不需要知道它所保护的应用系统本身。惟一真正需要知道被保护应用系统细节的Acegi组件是用于关联被保护资源和访问该资源所需权限的对象定义源。无论对于Acegi，还是受Acegi保护的应用系统，两者间的松耦合性都得到了充分的体现。


Easy CHM - http://www.eTextWizard.com

您现在使用的CHM文件由Easy CHM的未注册版本制作.
注册版本不会在CHM里自动添加此页面

Easy CHM - 版本: 2.30 软件出品: 国华软件 软件作者: 冯国华 中国北京 |  | 截屏图片
更新日期: 2006-02-27 | 文件大小: 1392k - 1.39MB | 软件类型: 共享软件 注册费用: 人民币 99 元
关键词: make chm, create chm, chm制作工具, chm电子书制作工具,chm帮助文件制作工具

描述:

Easy CHM是国华软件推出的一款强大的CHM电子书或CHM帮助文件的快速制作工具。使用EasyCHM只需要三个步骤就可以完成CHM的制作：1、用户指定一个目录，EasyCHM会自动导入全部目录及文件；2、设置CHM编译选项；3、开始制作。EasyCHM非常适合个人和单位制作高压缩比的带有全文检索及高亮显示搜索结果的网页集锦、CHM帮助文件、产品说明、公司介绍、CHM电子书等等。

主要功能：

  • 全自动的目录及文件导入（可以包括子目录）；
  • 支持导入任意的文件类型；
  • 自动生成CHM的目录列表并自动生成所有目录项；
  • 为CHM的目录列表自动添加多级编号；
  • 批量更换CHM目录各项的图标；
  • 支持批量查找替换多级目录各项的标题文字内容；
  • 允许用户指定从文本文件的第N行自动截取标题；
  • 易用的目录编辑器；
  • 丰富实用的CHM制作选项帮助用户制作更加个性化的专业CHM电子书或CHM帮助文件。
  • 内嵌CHM反编译工具。
  • 用户可以保存和打开工程文件。

EasyCHM未注册版本的主要限制：

  1. EasyCHM的未注册版本可以制作带任意层级目录的CHM，但是每个CHM的工具栏上都会自动添加一个ABOUT按钮，以提醒用户注册。注册版本不会添加ABOUT按钮。
  2. EasyCHM的未注册版本无法保存CHM的通用工程文件，如*.HHP、*.HHC、*.HHK等，未注册版本的EasyCHM会自动删除*.HHP、*.HHC、*.HHK这几个文件。
  3. EasyCHM的未注册版本制作的CHM的标题上会随机添加'Made by UNREGISTERED version of Easy CHM'这样的提醒注册的文字

注册版本无任何限制。
如果EasyCHM可以为您的工作和学习提供帮助，请注册购买，感谢您给予软件作者的支持。

联系方式:
国华软件站点一: http://www.etextwizard.com
国华软件站点二: http://www.zipghost.com
技术支持信箱: webmaster@etextwizard.com
购买方式:
在线购买:（可提供发票，支持很多种支付方式，包括邮政汇款）
通过“共享软件注册中心”购买
直接向作者注册（共2种方式）：

1、招商银行划款:
开户行： 招商银行北京分行双榆树支行
帐 号： 001015570093
户 名： 冯国华
2、中国农业银行金穗卡转帐（注意是卡号）：
金穗卡卡号： 95599 8001 42061 34216
收 款 人： 冯国华
注意事项：
直接向作者注册的用户请在汇出足额的注册费后以EMAIL形式通知作者，同时请尽量提供

  • 汇款日期

  • 汇出城市

  • 汇出行

  • 操作号

  • 汇款人姓名

  • 用于接收注册码的信箱（请确保可以接收邮件，最好多提供）

  • 您希望的注册用户名

  • 要注册的软件及套数

您提供的资料详细些会比较方便确认。
作者在确认支付已完成后将会马上发出注册码。
用户在收到注册信后请尽量给软件作者发个类似“已收到注册码”的确认信 - 感谢您的配合！
下载地址:

系统需求: Win98,WinME,WinXP,Windows2000
安装支持: 支持安装及卸载