集成
1. 远程调用与 Web 服务
Spring 提供了对多种远程调用技术的支持。 其远程调用支持简化了远程服务的开发,这些服务通过 Java 接口以及以 Java 对象作为输入和输出来实现。目前,Spring 支持以下远程调用技术:
-
远程方法调用(RMI):通过使用
RmiProxyFactoryBean和RmiServiceExporter,Spring 支持传统的 RMI(使用java.rmi.Remote接口和java.rmi.RemoteException)以及通过 RMI 调用器实现的透明远程调用(使用任意 Java 接口)。 -
Spring HTTP 调用器:Spring 提供了一种特殊的远程调用策略,允许通过 HTTP 进行 Java 序列化,支持任意 Java 接口(与 RMI 调用器类似)。对应的支撑类是
HttpInvokerProxyFactoryBean和HttpInvokerServiceExporter。 -
Hessian:通过使用 Spring 的
HessianProxyFactoryBean和HessianServiceExporter,你可以通过 Caucho 提供的轻量级基于 HTTP 的二进制协议透明地暴露你的服务。 -
Java Web 服务:Spring 通过 JAX-WS 为 Web 服务提供远程调用支持。
-
JMS:通过
JmsInvokerServiceExporter模块中的JmsInvokerProxyFactoryBean和spring-jms类,支持以 JMS 作为底层协议的远程调用。 -
AMQP:通过 AMQP 作为底层协议进行远程调用的功能由独立的 Spring AMQP 项目提供支持。
在讨论 Spring 的远程调用功能时,我们使用以下领域模型及相应的服务:
public class Account implements Serializable{
private String name;
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
}
public interface AccountService {
public void insertAccount(Account account);
public List<Account> getAccounts(String name);
}
// the implementation doing nothing at the moment
public class AccountServiceImpl implements AccountService {
public void insertAccount(Account acc) {
// do something...
}
public List<Account> getAccounts(String name) {
// do something...
}
}
本节首先通过使用RMI将服务暴露给远程客户端,并简要讨论了使用RMI的一些缺点。随后,继续介绍一个使用Hessian作为协议的示例。
1.1. RMI
通过使用 Spring 对 RMI 的支持,你可以透明地通过 RMI 基础设施暴露你的服务。完成此配置后,你基本上会得到一个类似于远程 EJB 的配置,唯一的区别在于它没有对安全上下文传播或远程事务传播提供标准支持。不过,当你使用 RMI 调用器时,Spring 确实为此类额外的调用上下文提供了钩子(hooks),因此你可以例如集成安全框架或自定义安全凭证。
1.1.1. 使用导出服务RmiServiceExporter
通过使用 RmiServiceExporter,我们可以将 AccountService 对象的接口暴露为 RMI 对象。该接口可以通过 RmiProxyFactoryBean 访问,或者在传统 RMI 服务的情况下,也可以通过普通的 RMI 方式访问。RmiServiceExporter 明确支持通过 RMI 调用器暴露任意非 RMI 服务。
我们首先需要在 Spring 容器中配置我们的服务。 以下示例展示了如何进行配置:
<bean id="accountService" class="example.AccountServiceImpl">
<!-- any additional properties, maybe a DAO? -->
</bean>
接下来,我们必须使用 RmiServiceExporter 来暴露我们的服务。
以下示例展示了如何实现这一点:
<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
<!-- does not necessarily have to be the same name as the bean to be exported -->
<property name="serviceName" value="AccountService"/>
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
<!-- defaults to 1099 -->
<property name="registryPort" value="1199"/>
</bean>
在前面的示例中,我们覆盖了 RMI 注册表的端口。通常,您的应用服务器也会维护一个 RMI 注册表,最好不要干扰它。
此外,服务名称用于绑定服务。因此,在前面的示例中,
服务被绑定到 'rmi://HOST:1199/AccountService'。我们稍后在客户端使用此 URL 来链接该服务。
servicePort 属性已被省略(其默认值为 0)。这意味着使用匿名端口与服务进行通信。 |
1.1.2. 在客户端链接服务
我们的客户端是一个简单的对象,它使用 AccountService 来管理账户,如下例所示:
public class SimpleObject {
private AccountService accountService;
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
// additional methods using the accountService
}
为了在客户端链接该服务,我们创建一个独立的 Spring 容器, 用于包含以下简单对象以及服务链接的配置片段:
<bean class="example.SimpleObject">
<property name="accountService" ref="accountService"/>
</bean>
<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl" value="rmi://HOST:1199/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
这就是我们在客户端支持远程账户服务所需做的全部工作。Spring 会透明地创建一个调用器,并通过 RmiServiceExporter 远程启用账户服务。在客户端,我们使用 RmiProxyFactoryBean 将其连接进来。
1.2. 使用 Hessian 通过 HTTP 远程调用服务
Hessian 提供了一种基于 HTTP 的二进制远程通信协议。它由 Caucho 开发,您可以在 https://www.caucho.com/ 上找到更多关于 Hessian 本身的信息。
1.2.1. Hessian
Hessian 通过 HTTP 进行通信,并使用一个自定义的 Servlet。通过使用 Spring 的
DispatcherServlet 原理(参见 webmvc.html),我们可以将此类 Servlet 配置起来以暴露您的服务。首先,我们必须在应用程序中创建一个新的 Servlet,如下所示的 web.xml 片段所示:
<servlet>
<servlet-name>remoting</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>remoting</servlet-name>
<url-pattern>/remoting/*</url-pattern>
</servlet-mapping>
如果你熟悉 Spring 的 DispatcherServlet 原理,可能已经知道现在需要在 remoting-servlet.xml 目录下创建一个名为 WEB-INF(以你的 servlet 名称为基础命名)的 Spring 容器配置资源文件。
该应用上下文将在下一节中使用。
或者,考虑使用 Spring 提供的更简单的 HttpRequestHandlerServlet。这样做可以将远程导出器的定义嵌入到你的根应用上下文中(默认位于 WEB-INF/applicationContext.xml),而各个 servlet 定义则指向特定的导出器 bean。在这种情况下,每个 servlet 的名称必须与其目标导出器 bean 的名称相匹配。
1.2.2. 通过使用 暴露您的 BeanHessianServiceExporter
在新创建的名为 remoting-servlet.xml 的应用上下文中,我们创建了一个
HessianServiceExporter 来导出我们的服务,如下例所示:
<bean id="accountService" class="example.AccountServiceImpl">
<!-- any additional properties, maybe a DAO? -->
</bean>
<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
现在我们已准备好在客户端连接该服务。由于未显式指定处理器映射(用于将请求 URL 映射到服务),因此使用了 BeanNameUrlHandlerMapping。因此,该服务会通过其在所属 DispatcherServlet 实例映射中定义的 bean 名称所对应的 URL 对外暴露:https://HOST:8080/remoting/AccountService。
或者,你可以在根应用上下文中创建一个 HessianServiceExporter(例如,在 WEB-INF/applicationContext.xml 中),如下例所示:
<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
在后一种情况下,您应在 web.xml 中为此导出器定义一个对应的 servlet,
最终效果相同:该导出器会被映射到请求路径
/remoting/AccountService。请注意,servlet 的名称必须与目标导出器的 bean 名称保持一致。以下示例展示了如何实现这一点:
<servlet>
<servlet-name>accountExporter</servlet-name>
<servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>accountExporter</servlet-name>
<url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>
1.2.3. 在客户端链接服务
通过使用 HessianProxyFactoryBean,我们可以在客户端连接该服务。此处应用的原理与 RMI 示例相同。我们创建一个独立的 Bean 工厂或应用上下文,并在其中声明以下 Bean,其中 SimpleObject 使用 AccountService 来管理账户,如下例所示:
<bean class="example.SimpleObject">
<property name="accountService" ref="accountService"/>
</bean>
<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
<property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
1.2.4. 对通过 Hessian 暴露的服务应用 HTTP 基本认证
Hessian 的一个优势在于我们可以轻松应用 HTTP 基本身份验证,因为这两种协议都是基于 HTTP 的。例如,您可以利用 web.xml 的安全特性来应用常规的 HTTP 服务器安全机制。通常情况下,您无需在此处使用每个用户单独的安全凭据,而是可以在 HessianProxyFactoryBean 级别使用共享的凭据(类似于 JDBC 的 DataSource),如下例所示:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors" ref="authorizationInterceptor"/>
</bean>
<bean id="authorizationInterceptor"
class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
<property name="authorizedRoles" value="administrator,operator"/>
</bean>
在前面的示例中,我们显式提到了 BeanNameUrlHandlerMapping 并设置了一个拦截器,以确保只有管理员和操作员才能调用此应用上下文中提到的 bean。
| 前面的示例并未展示一种灵活的安全基础设施。若要了解有关安全性的更多选项,请参阅 Spring Security 项目:https://projects.spring.io/spring-security/。 |
1.3. Spring HTTP 调用器
与 Hessian 不同,Spring HTTP 调用器是一种轻量级协议,它使用自身精简的序列化机制,并通过标准的 Java 序列化机制经由 HTTP 暴露服务。如果你的参数和返回类型是复杂类型,而这些类型无法通过 Hessian 所使用的序列化机制进行序列化,那么采用 Spring HTTP 调用器就具有巨大的优势(有关选择远程调用技术时的更多考虑因素,请参见下一节)。
在底层,Spring 使用 JDK 提供的标准功能或 Apache HttpComponents 来执行 HTTP 调用。如果你需要更高级且更易用的功能,请使用后者。更多信息请参见
hc.apache.org/httpcomponents-client-ga/。
|
请注意因不安全的 Java 反序列化而导致的漏洞: 在反序列化过程中,经过恶意篡改的输入流可能会导致服务器上执行非预期的代码。 因此,请勿将 HTTP Invoker 端点暴露给不可信的客户端,而应仅在您自己的服务之间暴露这些端点。 通常情况下,我们强烈建议使用其他任何消息格式(例如 JSON)来替代。 如果你担心 Java 序列化带来的安全漏洞, 请考虑使用 JVM 核心层面的通用序列化过滤机制, 该机制最初为 JDK 9 开发,但随后已向后移植到 JDK 8、7 和 6。参见 https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a 和 https://openjdk.java.net/jeps/290。 |
1.3.1. 暴露服务对象
为服务对象设置 HTTP Invoker 基础设施的方式与使用 Hessian 非常相似。正如 Hessian 支持提供了 HessianServiceExporter 一样,Spring 的 HttpInvoker 支持也提供了 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。
要在 Spring Web MVC 的 AccountService 中暴露之前提到的 DispatcherServlet,需要在该分发器的应用上下文中进行如下配置,如以下示例所示:
<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
此类导出器定义通过 DispatcherServlet 实例的标准映射功能进行暴露,如Hessian 章节中所述。
或者,你可以在根应用程序上下文中创建一个 HttpInvokerServiceExporter(例如,在 'WEB-INF/applicationContext.xml' 中),如下例所示:
<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
此外,您可以在 web.xml 中为此导出器定义一个对应的 servlet,且该 servlet 的名称需与目标导出器的 bean 名称相匹配,如下例所示:
<servlet>
<servlet-name>accountExporter</servlet-name>
<servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>accountExporter</servlet-name>
<url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>
1.3.2. 在客户端链接服务
再次强调,从客户端引入该服务的方式与使用 Hessian 时非常相似。通过使用代理,Spring 能够将您的方法调用转换为指向已导出服务 URL 的 HTTP POST 请求。以下示例展示了如何配置这种设置:
<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
如前所述,您可以选择要使用的 HTTP 客户端。默认情况下,HttpInvokerProxy 使用 JDK 自带的 HTTP 功能,但您也可以通过设置 HttpComponents 属性来使用 Apache 的 httpInvokerRequestExecutor 客户端。以下示例展示了如何进行此配置:
<property name="httpInvokerRequestExecutor">
<bean class="org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor"/>
</property>
1.4. Java Web 服务
Spring 对标准的 Java Web 服务 API 提供全面支持:
-
使用 JAX-WS 暴露 Web 服务
-
使用 JAX-WS 访问 Web 服务
除了 Spring Core 中对 JAX-WS 的内置支持外,Spring 产品组合还包含 Spring Web Services,这是一个面向契约优先、文档驱动的 Web 服务解决方案——强烈推荐用于构建现代化、面向未来的 Web 服务。
1.4.1. 使用 JAX-WS 暴露基于 Servlet 的 Web 服务
Spring 为 JAX-WS Servlet 端点实现提供了一个便捷的基类:SpringBeanAutowiringSupport。为了暴露我们的 AccountService,我们继承 Spring 的 SpringBeanAutowiringSupport 类,并在此处实现业务逻辑,通常将调用委托给业务层。我们使用 Spring 的 @Autowired 注解来表达对 Spring 管理的 Bean 的依赖关系。以下示例展示了我们继承 SpringBeanAutowiringSupport 的类:
/**
* JAX-WS compliant AccountService implementation that simply delegates
* to the AccountService implementation in the root web application context.
*
* This wrapper class is necessary because JAX-WS requires working with dedicated
* endpoint classes. If an existing service needs to be exported, a wrapper that
* extends SpringBeanAutowiringSupport for simple Spring bean autowiring (through
* the @Autowired annotation) is the simplest JAX-WS compliant way.
*
* This is the class registered with the server-side JAX-WS implementation.
* In the case of a Java EE server, this would simply be defined as a servlet
* in web.xml, with the server detecting that this is a JAX-WS endpoint and reacting
* accordingly. The servlet name usually needs to match the specified WS service name.
*
* The web service engine manages the lifecycle of instances of this class.
* Spring bean references will just be wired in here.
*/
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@WebService(serviceName="AccountService")
public class AccountServiceEndpoint extends SpringBeanAutowiringSupport {
@Autowired
private AccountService biz;
@WebMethod
public void insertAccount(Account acc) {
biz.insertAccount(acc);
}
@WebMethod
public Account[] getAccounts(String name) {
return biz.getAccounts(name);
}
}
我们的 AccountServiceEndpoint 需要与 Spring 上下文运行在同一个 Web 应用程序中,以便能够访问 Spring 提供的功能。在 Java EE 环境中,默认情况下使用 JAX-WS Servlet 端点部署的标准约定即可满足这一要求。具体细节请参阅各种 Java EE Web 服务教程。
1.4.2. 使用 JAX-WS 导出独立 Web 服务
Oracle JDK 自带的内置 JAX-WS 提供者支持通过 JDK 中包含的内置 HTTP 服务器来暴露 Web 服务。Spring 的 SimpleJaxWsServiceExporter 会检测 Spring 应用上下文中所有带有 @WebService 注解的 Bean,并通过默认的 JAX-WS 服务器(即 JDK HTTP 服务器)将它们导出。
在此场景中,端点实例本身被定义并作为 Spring Bean 进行管理。它们会被注册到 JAX-WS 引擎中,但其生命周期由 Spring 应用上下文控制。这意味着您可以将 Spring 的功能(例如显式的依赖注入)应用于这些端点实例。@Autowired 注解驱动的注入同样有效。以下示例展示了如何定义这些 Bean:
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter">
<property name="baseAddress" value="http://localhost:8080/"/>
</bean>
<bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint">
...
</bean>
...
AccountServiceEndpoint 可以(但不必)继承 Spring 的 SpringBeanAutowiringSupport,
因为在本例中该端点是一个完全由 Spring 管理的 bean。这意味着
端点的实现可以如下所示(无需声明任何父类——同时 Spring 的 @Autowired 配置注解仍然有效):
@WebService(serviceName="AccountService")
public class AccountServiceEndpoint {
@Autowired
private AccountService biz;
@WebMethod
public void insertAccount(Account acc) {
biz.insertAccount(acc);
}
@WebMethod
public List<Account> getAccounts(String name) {
return biz.getAccounts(name);
}
}
1.4.3. 使用 JAX-WS RI 的 Spring 支持导出 Web 服务
Oracle 的 JAX-WS RI(参考实现)作为 GlassFish 项目的一部分开发,其 JAX-WS Commons 项目提供了对 Spring 的支持。这使得可以将 JAX-WS 端点定义为 Spring 管理的 Bean,类似于上一节中讨论的独立模式——但这次是在 Servlet 环境中。
| 这在 Java EE 环境中不具备可移植性。它主要用于非 EE 环境,例如 Tomcat,这类环境会将 JAX-WS RI 作为 Web 应用程序的一部分嵌入。 |
与标准的基于 Servlet 导出端点的方式相比,其区别在于:端点实例自身的生命周期由 Spring 管理,并且在 web.xml 中仅定义了一个 JAX-WS Servlet。而在标准的 Java EE 风格中(如前所示),每个服务端点都需要单独定义一个 Servlet,每个端点通常会委托给 Spring Bean(如前所示,通过使用 @Autowired 注解)。
有关设置和使用方式的详细信息,请参见 https://jax-ws-commons.java.net/spring/。
1.4.4. 使用 JAX-WS 访问 Web 服务
Spring 提供了两个工厂 Bean 用于创建 JAX-WS Web 服务代理,即
LocalJaxWsServiceFactoryBean 和 JaxWsPortProxyFactoryBean。前者仅能
返回一个 JAX-WS 服务类供我们使用;后者则是功能完整的版本,
可以返回一个实现我们业务服务接口的代理。
在下面的示例中,我们使用 JaxWsPortProxyFactoryBean 为
AccountService 端点创建一个代理(再次说明):
<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
<property name="serviceInterface" value="example.AccountService"/> (1)
<property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/>
<property name="namespaceUri" value="https://example/"/>
<property name="serviceName" value="AccountService"/>
<property name="portName" value="AccountServiceEndpointPort"/>
</bean>
| 1 | 其中 serviceInterface 是我们的业务接口,供客户端使用。 |
wsdlDocumentUrl 是 WSDL 文件的 URL。Spring 在启动时需要此参数以创建 JAX-WS 服务。namespaceUri 对应于 .wsdl 文件中的 targetNamespace。serviceName 对应于 .wsdl 文件中的服务名称。portName 对应于 .wsdl 文件中的端口名称。
访问该 Web 服务非常简单,因为我们为其提供了一个 bean 工厂,它将该服务暴露为一个名为 AccountService 的接口。以下示例展示了如何在 Spring 中进行配置:
<bean id="client" class="example.AccountClientImpl">
...
<property name="service" ref="accountWebService"/>
</bean>
从客户端代码来看,我们可以像访问普通类一样访问该 Web 服务,如下例所示:
public class AccountClientImpl {
private AccountService service;
public void setService(AccountService service) {
this.service = service;
}
public void foo() {
service.insertAccount(...);
}
}
上述内容略有简化,因为 JAX-WS 要求端点接口和实现类必须使用 @WebService、@SOAPBinding 等注解进行标注。这意味着你不能(轻易地)直接使用普通的 Java 接口和实现类作为 JAX-WS 的端点构件;你首先需要对它们进行相应的注解。有关这些要求的详细信息,请查阅 JAX-WS 的官方文档。 |
1.5. JMS
你也可以使用 JMS 作为底层通信协议来透明地暴露服务。Spring 框架中的 JMS 远程支持相当基础,它在same thread中发送和接收消息,并且使用同一个非事务性的Session。因此,吞吐量取决于具体实现。请注意,这些单线程和非事务性的限制仅适用于 Spring 的 JMS 远程支持。有关 Spring 对基于 JMS 的消息传递的丰富支持信息,请参阅JMS(Java 消息服务)。
以下接口在服务器端和客户端均被使用:
package com.foo;
public interface CheckingAccountService {
public void cancelAccount(Long accountId);
}
以下是对前述接口的简单实现,用于服务器端:
package com.foo;
public class SimpleCheckingAccountService implements CheckingAccountService {
public void cancelAccount(Long accountId) {
System.out.println("Cancelling account [" + accountId + "]");
}
}
以下配置文件包含在客户端和服务器端共享的 JMS 基础设施 Bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://ep-t43:61616"/>
</bean>
<bean id="queue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="mmm"/>
</bean>
</beans>
1.5.1. 服务器端配置
在服务器端,您需要暴露使用 JmsInvokerServiceExporter 的服务对象,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="checkingAccountService"
class="org.springframework.jms.remoting.JmsInvokerServiceExporter">
<property name="serviceInterface" value="com.foo.CheckingAccountService"/>
<property name="service">
<bean class="com.foo.SimpleCheckingAccountService"/>
</property>
</bean>
<bean class="org.springframework.jms.listener.SimpleMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="queue"/>
<property name="concurrentConsumers" value="3"/>
<property name="messageListener" ref="checkingAccountService"/>
</bean>
</beans>
package com.foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Server {
public static void main(String[] args) throws Exception {
new ClassPathXmlApplicationContext("com/foo/server.xml", "com/foo/jms.xml");
}
}
1.5.2. 客户端配置
客户端只需创建一个实现双方约定接口(CheckingAccountService)的客户端代理即可。
以下示例定义了一些 bean,您可以将它们注入到其他客户端对象中 (代理会负责通过 JMS 将调用转发到服务器端对象):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="checkingAccountService"
class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean">
<property name="serviceInterface" value="com.foo.CheckingAccountService"/>
<property name="connectionFactory" ref="connectionFactory"/>
<property name="queue" ref="queue"/>
</bean>
</beans>
package com.foo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Client {
public static void main(String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/foo/client.xml", "com/foo/jms.xml");
CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService");
service.cancelAccount(new Long(10));
}
}
1.6. AMQP
Spring AMQP 项目支持通过 AMQP 作为底层协议进行远程调用。 更多详细信息,请参阅 Spring AMQP 参考文档中的Spring 远程调用部分。
|
远程接口未实现自动检测 远程接口不自动检测所实现接口的主要原因,是为了避免向远程调用者开放过多的入口。目标对象可能会实现一些内部回调接口,例如 在本地场景中,为目标对象提供一个实现其所有接口的代理通常无关紧要。 然而,当你导出一个远程服务时,应当暴露一个特定的服务接口,该接口包含专为远程使用而设计的操作。 除了内部回调接口外,目标对象可能实现了多个业务接口,但其中仅有一个接口是用于远程暴露的。 基于这些原因,我们要求必须明确指定此类服务接口。 这是在配置便利性与意外暴露内部方法的风险之间的一种权衡。始终明确指定服务接口并不会带来太多额外工作,并且能确保你安全地控制特定方法的暴露。 |
1.7. 选择技术时的注意事项
此处介绍的每一项技术都有其缺点。在选择技术时,您应仔细考虑自身需求、所暴露的服务以及通过网络传输的对象。
使用 RMI 时,除非对 RMI 流量进行隧道传输,否则无法通过 HTTP 协议访问这些对象。RMI 是一种相当重量级的协议,因为它支持完整的对象序列化,当你使用需要通过网络进行序列化的复杂数据模型时,这一点非常重要。然而,RMI-JRMP 仅限于 Java 客户端,它是一种 Java 到 Java 的远程调用解决方案。
如果需要基于 HTTP 的远程调用,同时又依赖 Java 序列化,Spring 的 HTTP 调用器是一个很好的选择。它与 RMI 调用器共享基本的基础设施,但使用 HTTP 作为传输协议。需要注意的是,HTTP 调用器不仅限于 Java 到 Java 的远程调用,还要求客户端和服务器端都使用 Spring。(后一点同样适用于 Spring 的 RMI 调用器,即使其用于非 RMI 接口。)
当在异构环境中运行时,Hessian 可能会带来显著的价值,因为它们明确允许非 Java 客户端的使用。然而,对非 Java 的支持仍然有限。已知的问题包括在结合延迟初始化集合的情况下对 Hibernate 对象进行序列化。如果你的数据模型存在这种情况,请考虑使用 RMI 或 HTTP 调用器来替代 Hessian。
JMS 可用于提供服务集群,并让 JMS 代理(broker)负责负载均衡、服务发现和自动故障转移。默认情况下,JMS 远程调用使用 Java 序列化,但 JMS 提供商也可以使用其他机制进行网络传输格式化,例如使用 XStream,以便让服务器能够采用其他技术实现。
最后但同样重要的是,EJB 相较于 RMI 具有一个优势,即它支持基于标准角色的身份验证和授权,以及远程事务传播。虽然也可以让 RMI 调用器或 HTTP 调用器支持安全上下文传播,但这并非由 Spring 核心提供。Spring 仅提供了适当的钩子(hooks),以便集成第三方或自定义的解决方案。
1.8. REST 端点
Spring 框架提供了两种调用 REST 端点的方式:
-
RestTemplate:原始的 Spring REST 客户端,提供同步的模板方法 API。 -
WebClient:一种非阻塞的响应式替代方案, 同时支持同步、异步以及流式处理场景。
从 5.0 版本起,RestTemplate 已进入维护模式,今后仅接受对小范围变更和 bug 修复的请求。请考虑使用
WebClient,它提供了更现代化的 API,并支持同步、异步和流式处理场景。 |
1.8.1. RestTemplate
RestTemplate 在 HTTP 客户端库之上提供了更高层次的 API。它使得只需一行代码即可轻松调用 REST 端点。它提供了以下几组重载方法:
| 方法组 | 描述 |
|---|---|
|
通过 GET 请求获取一个表示。 |
|
使用 GET 方法获取一个 |
|
通过使用 HEAD 方法检索资源的所有标头。 |
|
通过使用 POST 创建一个新资源,并从响应中返回 |
|
通过使用 POST 创建一个新资源,并返回响应中的表示形式。 |
|
通过使用 POST 创建一个新资源,并返回响应中的表示形式。 |
|
通过使用 PUT 创建或更新资源。 |
|
通过使用 PATCH 方法更新资源,并返回响应中的表示形式。
请注意,JDK 的 |
|
使用 DELETE 方法删除指定 URI 处的资源。 |
|
通过使用 ALLOW 方法检索资源所允许的 HTTP 方法。 |
|
前述方法的一个更为通用(且约束更少)的版本,在需要时提供了额外的灵活性。它接受一个 这些方法允许使用 |
|
通过回调接口对请求准备和响应提取进行完全控制,从而以最通用的方式执行请求。 |
初始化
默认构造函数使用 java.net.HttpURLConnection 来执行请求。你可以通过提供一个 ClientHttpRequestFactory 的实现来切换到其他 HTTP 库。
内置支持以下库:
-
Apache HttpComponents
-
Netty
-
OkHttp
例如,要切换到 Apache HttpComponents,您可以使用以下方式:
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
每个 ClientHttpRequestFactory 都会暴露底层 HTTP 客户端库特有的配置选项——例如,用于凭证、连接池以及其他细节。
请注意,当访问表示错误(例如 401)的响应状态时,java.net 的 HTTP 请求实现可能会抛出异常。如果这会造成问题,请切换到其他 HTTP 客户端库。 |
统一资源标识符(URIs)
许多 RestTemplate 方法接受一个 URI 模板和 URI 模板变量,这些变量可以作为 String 可变参数传入,也可以作为 Map<String,String> 传入。
以下示例使用了一个 String 可变参数:
String result = restTemplate.getForObject(
"https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
以下示例使用了一个 Map<String, String>:
Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject(
"https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
请记住,URI 模板会自动进行编码,如下例所示:
restTemplate.getForObject("https://example.com/hotel list", String.class);
// Results in request to "https://example.com/hotel%20list"
您可以使用 uriTemplateHandler 的 RestTemplate 属性来自定义 URI 的编码方式。或者,您也可以预先创建一个 java.net.URI 对象,并将其传入某个接受 RestTemplate 参数的 URI 方法中。
有关处理和编码 URI 的更多详细信息,请参阅URI 链接。
headers
您可以使用 exchange() 方法来指定请求头,如下例所示:
String uriTemplate = "https://example.com/hotels/{hotel}";
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);
RequestEntity<Void> requestEntity = RequestEntity.get(uri)
.header(("MyRequestHeader", "MyValue")
.build();
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();
你可以通过许多返回 RestTemplate 的 ResponseEntity 方法变体来获取响应头。
身体
传入和从 RestTemplate 方法返回的对象,会借助 HttpMessageConverter 转换为原始内容或从原始内容转换而来。
在 POST 请求中,输入对象会被序列化到请求体中,如下例所示:
URI location = template.postForLocation("https://example.com/people", person);
你无需显式设置请求的 Content-Type 头。在大多数情况下,你可以根据源 Object 类型找到一个兼容的消息转换器,而所选的消息转换器会相应地设置内容类型。如有必要,你可以使用 exchange 方法显式提供 Content-Type 请求头,这反过来会影响所选择的消息转换器。
在执行 GET 请求时,响应体将被反序列化为一个输出 Object,如下例所示:
Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42);
请求的 Accept 头部无需显式设置。在大多数情况下,可以根据预期的响应类型找到兼容的消息转换器,该转换器随后会帮助填充 Accept 头部。如有必要,您可以使用 exchange 方法显式提供 Accept 头部。
默认情况下,RestTemplate 会根据类路径检查注册所有内置的
消息转换器,这些检查有助于确定哪些可选的转换库已存在。你也可以显式地设置要使用的消息转换器。
消息转换
spring-web 模块包含了 HttpMessageConverter 接口,用于通过 InputStream 和 OutputStream 读取和写入 HTTP 请求与响应的正文。
HttpMessageConverter 实例在客户端(例如,在 RestTemplate 中)和服务器端(例如,在 Spring MVC 的 REST 控制器中)都会被使用。
框架中提供了主要媒体(MIME)类型的具體實現,默認情況下,這些實現會在客戶端註冊到 RestTemplate 中,在服務器端註冊到 RequestMethodHandlerAdapter 中(參見
配置消息轉換器)。
HttpMessageConverter 的实现将在以下各节中进行描述。
对于所有转换器,都会使用一个默认的媒体类型,但你可以通过设置
supportedMediaTypes bean 属性来覆盖它。下表描述了每种实现:
| 消息转换器 | 描述 |
|---|---|
|
一种 |
|
一种 |
|
一种 |
|
一种 |
|
一种 |
|
一种 |
|
一种 |
|
一种 |
Jackson JSON 视图
您可以指定一个Jackson JSON 视图, 以仅序列化对象属性的一个子集,如下例所示:
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
RequestEntity<MappingJacksonValue> requestEntity =
RequestEntity.post(new URI("https://example.com/user")).body(value);
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
文件上传
要发送 multipart 数据,您需要提供一个 MultiValueMap<String, Object>,其值可以是用于部分(part)内容的 Object、用于文件部分的 Resource,或用于带有头部信息的部分内容的 HttpEntity。例如:
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
在大多数情况下,您无需为每个部分指定 Content-Type。内容类型会根据用于序列化该部分的 HttpMessageConverter 自动确定,或者在基于 Resource 的情况下,根据文件扩展名自动确定。如有必要,您可以使用 MediaType 包装器显式提供 HttpEntity。
一旦 MultiValueMap 准备就绪,你就可以将其传递给 RestTemplate,如下所示:
MultiValueMap<String, Object> parts = ...;
template.postForObject("https://example.com/upload", parts, Void.class);
如果 MultiValueMap 包含至少一个非 String 的值,则 Content-Type 将由 FormHttpMessageConverter 设置为 multipart/form-data。如果 MultiValueMap 具有 String 个值,则 Content-Type 将默认为 application/x-www-form-urlencoded。如有必要,也可以显式设置 Content-Type。
1.8.2. 使用AsyncRestTemplate(已弃用)
AsyncRestTemplate 已被弃用。对于所有可能考虑使用 AsyncRestTemplate 的场景,请改用 WebClient。
2. Enterprise JavaBeans (EJB) 集成
作为一个轻量级容器,Spring 常被视为 EJB 的替代方案。我们确实认为,对于许多(即使不是大多数)应用程序和使用场景而言,Spring 容器结合其在事务、ORM 和 JDBC 访问方面丰富的支持功能,相比通过 EJB 容器和 EJB 实现同等功能,是一个更好的选择。
然而,需要注意的是,使用 Spring 并不会妨碍你使用 EJB。 事实上,Spring 使得访问 EJB 以及在 EJB 内部实现 EJB 和相关功能变得更加容易。此外,通过 Spring 来访问 EJB 提供的服务,可以让你在后续透明地将这些服务的实现切换为本地 EJB、远程 EJB 或 POJO(普通 Java 对象)形式,而无需修改客户端代码。
在本章中,我们将探讨 Spring 如何帮助您访问和实现 EJB。Spring 在访问无状态会话 Bean(SLSB)时尤为有用,因此我们首先讨论这一主题。
2.1. 访问 EJB
本节介绍如何访问EJB。
2.1.1. 概念
为了调用本地或远程无状态会话 Bean 上的方法,客户端代码通常必须执行 JNDI 查找以获取(本地或远程)EJB Home 对象,然后在该对象上调用 create 方法以获得实际的(本地或远程)EJB 对象。
随后即可在该 EJB 上调用一个或多个方法。
为了避免重复编写底层代码,许多 EJB 应用程序使用服务定位器(Service Locator)和业务委托(Business Delegate)模式。这些模式优于在客户端代码中到处散布 JNDI 查找,但它们的常规实现存在明显的缺点:
-
通常,使用 EJB 的代码依赖于服务定位器(Service Locator)或业务委托(Business Delegate)单例,这使得测试变得困难。
-
在未结合业务委托(Business Delegate)而直接使用服务定位器(Service Locator)模式的情况下,应用程序代码仍然需要调用 EJB Home 上的
create()方法,并处理由此产生的异常。因此,它依然与 EJB API 紧密耦合,并且不得不面对 EJB 编程模型的复杂性。 -
实现业务委托(Business Delegate)模式通常会导致大量代码重复,因为我们不得不编写许多方法,而这些方法都只是调用EJB上的同一个方法。
Spring 的方法是允许创建和使用代理对象(通常在 Spring 容器内部进行配置),这些代理对象充当无代码的业务委托。除非你确实在此类代码中增加了实际价值,否则无需再编写另一个服务定位器(Service Locator)、另一个 JNDI 查找,或在手写的业务委托(Business Delegate)中重复方法。
2.1.2. 访问本地 SLSB
假设我们有一个 Web 控制器需要使用本地 EJB。我们遵循最佳实践,采用 EJB 业务方法接口(Business Methods Interface)模式,使得 EJB 的本地接口扩展一个与 EJB 无关的业务方法接口。我们将这个业务方法接口称为 MyComponent。以下示例展示了这样一个接口:
public interface MyComponent {
...
}
使用业务方法接口(Business Methods Interface)模式的主要原因之一,是为了确保本地接口与 Bean 实现类中的方法签名能够自动保持同步。另一个原因是,如果将来有必要,我们可以更轻松地切换到该服务的 POJO(Plain Old Java Object,普通 Java 对象)实现。我们还需要实现本地 Home 接口,并提供一个实现类,该类既要实现 SessionBean,也要实现 MyComponent 业务方法接口。现在,要将我们的 Web 层控制器连接到 EJB 实现,我们唯一需要编写的 Java 代码就是在控制器中暴露一个类型为 MyComponent 的 setter 方法。该方法会将引用保存为控制器中的一个实例变量。以下示例展示了如何实现这一点:
private MyComponent myComponent;
public void setMyComponent(MyComponent myComponent) {
this.myComponent = myComponent;
}
随后,我们可以在控制器中的任意业务方法中使用这个实例变量。
现在,假设我们从 Spring 容器中获取控制器对象,那么(在同一上下文中)我们可以配置一个 LocalStatelessSessionProxyFactoryBean 实例,
该实例即为 EJB 代理对象。我们通过以下配置项来配置该代理,并设置控制器的 myComponent 属性:
<bean id="myComponent"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/myBean"/>
<property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>
<bean id="myController" class="com.mycom.myController">
<property name="myComponent" ref="myComponent"/>
</bean>
大量工作在幕后由 Spring AOP 框架完成,尽管您无需直接使用 AOP 概念即可享受其带来的好处。myComponent 的 bean 定义会为 EJB 创建一个代理,该代理实现了业务方法接口。EJB 的本地 Home 接口在启动时被缓存,因此仅需执行一次 JNDI 查找。每次调用 EJB 时,代理都会在本地 EJB 上调用 classname 方法,并在 EJB 上调用相应的业务方法。
myController 的 bean 定义将控制器类的 myComponent 属性设置为 EJB 代理。
或者(尤其是在存在大量此类代理定义的情况下,更推荐使用这种方式),请考虑使用 Spring “jee” 命名空间中的 <jee:local-slsb> 配置元素。
以下示例展示了如何使用该方式:
<jee:local-slsb id="myComponent" jndi-name="ejb/myBean"
business-interface="com.mycom.MyComponent"/>
<bean id="myController" class="com.mycom.myController">
<property name="myComponent" ref="myComponent"/>
</bean>
这种EJB访问机制极大地简化了应用程序代码。Web层代码(或其他EJB客户端代码)完全不依赖于EJB的使用。若要将此EJB引用替换为POJO、模拟对象或其他测试桩,我们只需修改myComponent的bean定义,而无需更改任何Java代码。此外,我们也不必在应用程序中编写任何JNDI查找代码或其他EJB相关的底层代码。
基准测试和实际应用中的经验表明,这种方法(涉及对目标EJB的反射调用)所带来的性能开销极小,在典型使用场景中几乎无法察觉。请记住,无论如何我们都不应频繁地对EJB进行细粒度调用,因为应用服务器中的EJB基础设施本身就会带来一定的开销。
关于 JNDI 查找,有一点需要注意。在 bean 容器中,此类通常最好以单例(singleton)方式使用(将其设为原型(prototype)是没有必要的)。
然而,如果该 bean 容器会预先实例化单例 bean(例如各种基于 XML 的 ApplicationContext 变体),那么当 bean 容器在 EJB 容器加载目标 EJB 之前就被加载时,就可能出现问题。这是因为此类的 JNDI 查找是在 init() 方法中执行并缓存的,但此时 EJB 尚未绑定到目标位置。解决方法是不要预先实例化此工厂对象,而是让它在首次使用时才被创建。在基于 XML 的容器中,你可以通过使用 lazy-init 属性来控制这一点。
尽管大多数 Spring 用户对此不感兴趣,但那些使用 EJB 进行编程式 AOP 工作的开发者可能希望查看 LocalSlsbInvokerInterceptor。
2.1.3. 访问远程 SLSB
访问远程 EJB 本质上与访问本地 EJB 相同,只是需要使用 SimpleRemoteStatelessSessionProxyFactoryBean 或 <jee:remote-slsb> 配置元素。当然,无论是否使用 Spring,远程调用语义都适用:在另一个计算机的另一个虚拟机中调用对象上的方法时,在使用场景和故障处理方面有时必须进行不同的处理。
Spring 的 EJB 客户端支持相比非 Spring 方法又多了一项优势。通常情况下,EJB 客户端代码很难在本地调用和远程调用 EJB 之间轻松切换。这是因为远程接口的方法必须声明抛出 RemoteException,客户端代码必须处理该异常,而本地接口的方法则无需如此。为本地EJB编写的客户端代码,若需迁移到远程EJB,通常必须进行修改以增加对远程异常的处理;而为远程EJB编写的客户端代码,若需迁移到本地EJB,则要么保持不变但会进行大量不必要的远程异常处理,要么修改代码以移除这些处理逻辑。使用 Spring 远程 EJB 代理,您可以在业务方法接口和实现的 EJB 代码中不需要声明任何抛出的RemoteException。相反,可以有一个远程接口,它与前者完全相同(除了它会抛出RemoteException),并且依靠代理动态地将这两个接口视为相同。那也就是说,客户端代码无需处理
checked RemoteException
类。任何实际的RemoteException在EJB调用期间抛出时,会被重新抛出为非检查类型的RemoteAccessException类,该类是RuntimeException类的子类。然后,您可以随意在本地EJB、远程EJB(甚至普通Java对象)实现之间切换目标服务,而客户端代码无需知晓或关心这一变化。当然,这完全是可选的:你完全可以在你的业务接口中声明 RemoteException。
2.1.4. 访问 EJB 2.x SLSB 与 EJB 3 SLSB
通过 Spring 访问 EJB 2.x 会话 Bean 和 EJB 3 会话 Bean 在很大程度上是透明的。Spring 的 EJB 访问器(包括 <jee:local-slsb> 和 <jee:remote-slsb> 功能)会在运行时自动适配实际的组件。如果找到 Home 接口(EJB 2.x 风格),它们会处理该接口;如果没有可用的 Home 接口(EJB 3 风格),则直接执行组件调用。
注意:对于 EJB 3 会话 Bean,您也可以有效地使用 JndiObjectFactoryBean /
<jee:jndi-lookup>,因为在普通 JNDI 查找中会暴露完全可用的组件引用。显式定义 <jee:local-slsb> 或 <jee:remote-slsb>
查找可以提供一致且更明确的 EJB 访问配置。
3. JMS (Java 消息服务)
Spring 提供了一套JMS集成框架,简化了JMS API的使用方式,与Spring对JDBC API的集成方式类似。
JMS 的功能大致可分为两个方面,即消息的生产和消息的消费。JmsTemplate 类用于消息的生产和同步消息接收。对于类似于 Java EE 中消息驱动 Bean(Message-Driven Bean)风格的异步接收,Spring 提供了多种消息监听器容器,可用于创建消息驱动的 POJO(MDP)。Spring 还提供了一种声明式的方式来创建消息监听器。
org.springframework.jms.core 包提供了使用 JMS 的核心功能。它包含 JMS 模板类,通过处理资源的创建和释放来简化 JMS 的使用,其作用类似于 JdbcTemplate 对 JDBC 所做的那样。Spring 模板类所共有的设计原则是:提供辅助方法以执行常见操作;对于更复杂的用法,则将处理任务的核心逻辑委托给用户实现的回调接口。JMS 模板遵循相同的设计理念。这些类提供了多种便捷方法,用于发送消息、同步接收消息,以及向用户暴露 JMS 会话(session)和消息生产者(message producer)。
org.springframework.jms.support 包提供了 JMSException 异常转换功能。该转换将受检的 JMSException 异常层次结构转换为对应的非受检异常层次结构。如果存在任何特定于 JMS 提供商的受检 javax.jms.JMSException 子类,则该异常会被包装在非受检的 UncategorizedJmsException 中。
org.springframework.jms.support.converter 包提供了一个 MessageConverter 抽象,用于在 Java 对象和 JMS 消息之间进行转换。
org.springframework.jms.support.destination 包提供了多种管理 JMS 目的地的策略,例如为存储在 JNDI 中的目的地提供服务定位器。
org.springframework.jms.annotation 包提供了必要的基础设施,通过使用 @JmsListener 注解来支持注解驱动的监听器端点。
org.springframework.jms.config 包提供了 jms 命名空间的解析器实现,以及用于配置监听器容器和创建监听器端点的 Java 配置支持。
最后,org.springframework.jms.connection 包提供了适用于独立应用程序的 ConnectionFactory 实现。该包还包含了 Spring 的 JMS PlatformTransactionManager 实现(巧妙地命名为 JmsTransactionManager)。这使得 JMS 能够作为事务性资源无缝集成到 Spring 的事务管理机制中。
|
从 Spring Framework 5 开始,Spring 的 JMS 包全面支持 JMS 2.0,并且在运行时需要 JMS 2.0 API 的存在。我们建议使用兼容 JMS 2.0 的提供者。 如果你的系统中恰好使用了较旧的消息代理,可以尝试为现有的代理版本升级到兼容 JMS 2.0 的驱动程序。或者,你也可以尝试使用基于 JMS 1.1 的驱动程序,只需将 JMS 2.0 API 的 jar 包放入 classpath 中,但仅使用与 JMS 1.1 兼容的 API 来调用你的驱动程序。Spring 的 JMS 支持默认遵循 JMS 1.1 的规范,因此通过相应的配置,它确实支持这种场景。不过,请仅将此方案视为过渡时期的临时措施。 |
3.1. 使用 Spring JMS
本节介绍如何使用 Spring 的 JMS 组件。
3.1.1. 使用JmsTemplate
JmsTemplate 类是 JMS 核心包中的中心类。它简化了 JMS 的使用,因为它在发送消息或同步接收消息时会自动处理资源的创建和释放。
使用 JmsTemplate 的代码只需实现回调接口,这些接口为其提供了清晰定义的高层契约。MessageCreator 回调接口在接收到 Session 中调用代码所提供的 JmsTemplate 时创建一条消息。为了支持对 JMS API 更复杂的使用,SessionCallback 提供了 JMS 会话,而 ProducerCallback 则暴露了一对 Session 和 MessageProducer。
JMS API 提供了两类发送方法:一类接收传递模式(delivery mode)、优先级(priority)和生存时间(time-to-live)作为服务质量(QoS)参数;另一类不接收 QoS 参数,而是使用默认值。由于 JmsTemplate 提供了多种发送方法,为了避免发送方法数量的重复膨胀,QoS 参数的设置已作为 bean 属性公开。同样地,同步接收调用的超时值通过 setReceiveTimeout 属性进行设置。
某些 JMS 提供商允许通过配置 ConnectionFactory 在管理层面设置默认的 QOS(服务质量)值。这样会导致对 MessageProducer 实例的 send 方法(send(Destination destination, Message message))的调用所使用的 QOS 默认值与 JMS 规范中指定的值不同。为了实现对 QOS 值的一致性管理,必须显式启用 JmsTemplate 使用其自身的 QOS 值,即将布尔属性 isExplicitQosEnabled 设置为 true。
为方便起见,JmsTemplate 还提供了一个基本的请求-回复操作,该操作允许发送一条消息,并在作为操作一部分而创建的临时队列上等待回复。
JmsTemplate 类的实例在配置完成后是线程安全的。这一点非常重要,因为这意味着你可以配置一个 JmsTemplate 的单一实例,然后安全地将这个共享引用注入到多个协作者中。需要明确的是,JmsTemplate 是有状态的,因为它持有一个对 ConnectionFactory 的引用,但这种状态并非会话状态。 |
从 Spring Framework 4.1 起,JmsMessagingTemplate 基于 JmsTemplate 构建,
并提供了与消息抽象(即 org.springframework.messaging.Message)的集成。这使您可以以通用的方式创建要发送的消息。
3.1.2. 连接
JmsTemplate 需要一个对 ConnectionFactory 的引用。ConnectionFactory 是 JMS 规范的一部分,作为使用 JMS 的入口点。客户端应用程序使用它作为工厂来创建与 JMS 提供者的连接,并封装了各种配置参数,其中许多是提供商特定的,例如 SSL 配置选项。
在 EJB 中使用 JMS 时,提供商会提供 JMS 接口的实现,以便这些实现能够参与声明式事务管理,并对连接和会话进行池化。为了使用这种实现,Java EE 容器通常要求您在 EJB 或 Servlet 的部署描述符中将 JMS 连接工厂声明为 resource-ref。为了确保在 EJB 中使用 JmsTemplate 时能够利用这些特性,客户端应用程序应确保引用的是由容器管理的 ConnectionFactory 实现。
缓存消息资源
标准 API 涉及创建许多中间对象。要发送一条消息,需要执行以下“API”调用流程:
ConnectionFactory->Connection->Session->MessageProducer->send
在 ConnectionFactory 和 Send 操作之间,会创建并销毁三个中间对象。为了优化资源使用并提升性能,Spring 提供了两种 ConnectionFactory 的实现。
使用SingleConnectionFactory
Spring 提供了 ConnectionFactory 接口的一个实现类 SingleConnectionFactory,该实现对所有 Connection 调用均返回同一个 createConnection(),并忽略对 close() 的调用。这在测试和独立环境中非常有用,使得同一个连接可用于多次 JmsTemplate 调用,而这些调用可能跨越任意数量的事务。SingleConnectionFactory 需要引用一个标准的 ConnectionFactory,该引用通常来自 JNDI。
使用CachingConnectionFactory
CachingConnectionFactory扩展了SingleConnectionFactory的功能,并添加了对Session、MessageProducer和MessageConsumer实例的缓存。初始缓存大小设置为1。您可以使用sessionCacheSize属性来增加缓存会话的数量。请注意,实际缓存的会话数量会多于该数值,因为会话是根据其确认模式进行缓存的,所以当sessionCacheSize设置为 1 时,最多可能存在四个缓存的会话实例(每种确认模式一个)。MessageProducer和MessageConsumer实例在其所属会话内进行缓存,同时在缓存时也会考虑生产者和消费者的独特属性。MessageProducers 根据其目标地址进行缓存。MessageConsumers 则根据由目标地址、选择器、noLocal 交付标志以及持久订阅名称(如果创建的是持久消费者)组成的键进行缓存。
3.1.3. 目的地管理
目的地(Destinations)作为 ConnectionFactory 实例,是 JMS 管理的对象,可以存储在 JNDI 中并从中检索。在配置Spring应用程序上下文时,您可以使用JNDI JndiObjectFactoryBean 工厂类或<jee:jndi-lookup>来对对象引用的JMS目的地进行依赖注入。然而,如果应用程序中存在大量目的地,或者JMS提供商具有独特的高级目的地管理功能,这种策略通常会显得笨拙繁琐。此类高级目标管理的示例包括动态目标的创建,或对目标的分层命名空间的支持。The JmsTemplate 代理将
解析一个目标名称并将其转换为实现DestinationResolver接口的JMS目标对象。DynamicDestinationResolver 是默认实现,用于 JmsTemplate 并能够解决动态目标。A
JndiDestinationResolver 还可用作服务定位器,用于查找包含在 JNDI 中的目标,并可选择性地回退到 DynamicDestinationResolver 中包含的行为。
在JMS应用程序中,目标(destinations)通常仅在运行时才确定,因此无法在应用程序部署时通过管理方式创建。这通常是因为在相互交互的系统组件之间存在共享的应用逻辑,这些组件会根据一个众所周知的命名约定在运行时创建目标。尽管动态目的地的创建不属于JMS规范的一部分,但大多数提供商都提供了此功能。动态目的地是使用用户定义的名称创建的,
这使其区别于临时目的地,并且通常
不会在 JNDI 中注册。用于创建动态目的地的 API 因提供商而异,因为与目的地相关的属性是厂商特定的。然而,提供商有时会采用一种简单的实现方式,即忽略 JMS 规范中的警告,而使用 TopicSession 的 createTopic(String topicName) 方法或 QueueSession 的 createQueue(String
queueName) 方法来创建一个具有默认目标属性的新目的地。取决于提供商的实现,DynamicDestinationResolver 可以创建一个物理目标,而不仅仅是解析一个。
布尔属性 pubSubDomain 用于配置 JmsTemplate,使其了解当前使用的是哪种 JMS 域。默认情况下,该属性的值为 false,表示将使用点对点(point-to-point)域,即 Queues(队列)。此属性(由 JmsTemplate 使用)通过 DestinationResolver 接口的实现来决定动态目的地解析的行为。
您还可以通过 JmsTemplate 属性为 defaultDestination 配置一个默认目的地。该默认目的地用于那些未指定具体目的地的发送和接收操作。
3.1.4. 消息监听器容器
在EJB领域中,JMS消息最常见的用途之一是驱动消息驱动Bean(MDB)。Spring提供了一种创建消息驱动POJO(MDP)的解决方案,这种方式不会将用户绑定到EJB容器。(有关Spring对MDP支持的详细内容,请参见异步接收:消息驱动POJO。)从Spring Framework 4.1开始,端点方法可以使用@JmsListener注解进行标注——更多详情请参见基于注解的监听器端点。
消息监听器容器用于从 JMS 消息队列接收消息,并驱动注入其中的 MessageListener。该监听器容器负责处理消息接收的所有线程操作,并将消息分发给监听器进行处理。消息监听器容器是消息驱动 POJO(MDP)与消息服务提供者之间的中介,负责注册以接收消息、参与事务、获取和释放资源、异常转换等任务。这使得你可以专注于编写(可能较为复杂的)与接收消息(并可能作出响应)相关的业务逻辑,而将样板化的 JMS 基础设施相关问题委托给框架处理。
Spring 自带了两种标准的 JMS 消息监听器容器,每种都具有其特有的功能集。
使用SimpleMessageListenerContainer
该消息监听器容器是两种标准类型中较为简单的一种。它在启动时创建固定数量的 JMS 会话和消费者,通过标准的 JMS MessageConsumer.setMessageListener() 方法注册监听器,并将监听器回调的执行交由 JMS 提供商处理。这种变体不支持根据运行时需求动态调整,也不支持参与外部管理的事务。
从兼容性角度来看,它非常贴近独立 JMS 规范的设计理念,但通常不符合 Java EE 对 JMS 的限制要求。
虽然 SimpleMessageListenerContainer 不允许参与外部管理的事务,但它支持原生 JMS 事务。要启用此功能,您可以将 sessionTransacted 标志设置为 true,或者在 XML 命名空间中将 acknowledge 属性设为 transacted。此时,如果您的监听器抛出异常,将触发回滚,并重新投递消息。另外,也可以考虑使用 CLIENT_ACKNOWLEDGE 模式,该模式在发生异常时同样会重新投递消息,但不会使用事务型的 Session 实例,因此不会将其他 Session 操作(例如发送响应消息)纳入事务协议中。 |
默认的 AUTO_ACKNOWLEDGE 模式无法提供适当的可靠性保证。
当监听器执行失败时(因为消息提供商会自动在监听器调用后确认每条消息,且不会将任何异常传播回提供商),或者当监听器容器关闭时(可通过设置 acceptMessagesWhileStopping 标志进行配置),消息可能会丢失。在需要可靠性保障的情况下(例如,用于可靠的队列处理和持久化主题订阅),请务必使用事务型会话。 |
使用DefaultMessageListenerContainer
该消息监听器容器在大多数情况下被使用。与SimpleMessageListenerContainer相比,这种容器变体能够动态适应运行时需求,并且可以参与外部管理的事务。
当配置了JtaTransactionManager时,每条接收到的消息都会注册到一个XA事务中。因此,消息处理可以利用XA事务的语义。
该监听器容器在对JMS提供者要求较低、高级功能(例如参与外部管理的事务)以及与Java EE环境的兼容性之间取得了良好的平衡。
您可以自定义容器的缓存级别。请注意,当未启用缓存时,每次接收消息都会创建一个新的连接和一个新的会话。在高负载情况下,如果同时使用非持久订阅,可能会导致消息丢失。在这种情况下,请务必使用适当的缓存级别。
当代理(broker)宕机时,该容器还具备可恢复的能力。默认情况下,一个简单的BackOff实现会每隔五秒重试一次。您可以指定一个自定义的BackOff实现,以获得更细粒度的恢复选项。有关示例,请参见api-spring-framework/util/backoff/ExponentialBackOff.html[ExponentialBackOff]。
与其兄弟框架(SimpleMessageListenerContainer)类似,
DefaultMessageListenerContainer 支持原生 JMS 事务,并允许自定义确认模式。如果您的场景可行,强烈建议优先采用此方式,而非外部管理的事务——也就是说,如果您能够接受在 JVM 崩溃时偶尔出现重复消息的情况。您可以在业务逻辑中实现自定义的重复消息检测步骤来应对此类情况,例如通过检查业务实体是否存在,或查询协议表等方式。
任何此类安排都显著优于替代方案:即使用 XA 事务包裹整个处理流程(通过将您的 DefaultMessageListenerContainer 配置为使用 JtaTransactionManager),以同时涵盖 JMS 消息的接收以及消息监听器中业务逻辑的执行(包括数据库操作等)。 |
默认的 AUTO_ACKNOWLEDGE 模式无法提供适当的可靠性保证。
当监听器执行失败时(因为消息提供商会自动在监听器调用后确认每条消息,且不会将任何异常传播回提供商),或者当监听器容器关闭时(可通过设置 acceptMessagesWhileStopping 标志进行配置),消息可能会丢失。在需要可靠性保障的情况下(例如,用于可靠的队列处理和持久化主题订阅),请务必使用事务型会话。 |
3.1.5. 事务管理
Spring 提供了一个 JmsTransactionManager,用于管理单个 JMS
ConnectionFactory 的事务。这使得 JMS 应用程序能够利用 Spring 的托管事务功能,如
数据访问章节中的事务管理部分 所述。
JmsTransactionManager 执行本地资源事务,将来自指定 ConnectionFactory 的 JMS
连接(Connection)/会话(Session)对绑定到当前线程。
JmsTemplate 会自动检测此类事务性资源,并相应地对其进行操作。
在 Java EE 环境中,ConnectionFactory 会对 Connection 和 Session 实例进行池化,从而在多个事务之间高效地复用这些资源。在独立(standalone)环境中,使用 Spring 的 SingleConnectionFactory 会共享一个 JMS Connection,而每个事务则拥有自己独立的 Session。此外,也可以考虑使用特定于 JMS 提供商的连接池适配器,例如 ActiveMQ 的 PooledConnectionFactory 类。
你也可以将 JmsTemplate 与 JtaTransactionManager 以及支持 XA 的 JMS
ConnectionFactory 一起使用,以执行分布式事务。请注意,这需要使用 JTA 事务管理器以及经过正确 XA 配置的 ConnectionFactory。
(请查阅你的 Java EE 服务器或 JMS 提供商的文档。)
在使用 JMS API 从 Connection 创建 Session 时,如果在受管和非受管的事务环境中复用代码,可能会令人困惑。这是因为 JMS API 仅提供一个用于创建 Session 的工厂方法,且该方法需要指定事务模式和确认模式的值。在受管环境中,设置这些值是环境事务基础设施的职责,因此厂商对 JMS Connection 的包装器会忽略这些值。当您在非受管环境中使用 JmsTemplate 时,可以通过属性 sessionTransacted 和 sessionAcknowledgeMode 来指定这些值。当您将 PlatformTransactionManager 与 JmsTemplate 一起使用时,该模板始终会被赋予一个事务性的 JMS Session。
3.2. 发送消息
JmsTemplate 包含许多用于发送消息的便捷方法。其中一些发送方法通过使用 javax.jms.Destination 对象来指定目标,另一些则通过使用 JNDI 查找中的 String 来指定目标。不带目标参数的 send 方法会使用默认目标。
以下示例使用 MessageCreator 回调,从提供的 Session 对象创建一条文本消息:
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;
import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.core.JmsTemplate;
public class JmsQueueSender {
private JmsTemplate jmsTemplate;
private Queue queue;
public void setConnectionFactory(ConnectionFactory cf) {
this.jmsTemplate = new JmsTemplate(cf);
}
public void setQueue(Queue queue) {
this.queue = queue;
}
public void simpleSend() {
this.jmsTemplate.send(this.queue, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("hello queue world");
}
});
}
}
在前面的示例中,JmsTemplate 是通过传入一个 ConnectionFactory 的引用来构造的。另一种方式是使用无参构造函数,并提供 connectionFactory 属性,以便以 JavaBean 的风格(使用 BeanFactory 或普通 Java 代码)来构造该实例。此外,也可以考虑继承 Spring 提供的便捷基类 JmsGatewaySupport,它已预置了用于 JMS 配置的 bean 属性。
send(String destinationName, MessageCreator creator) 方法允许你使用目标的字符串名称来发送消息。如果这些名称已在 JNDI 中注册,则应将模板的 destinationResolver 属性设置为 JndiDestinationResolver 的一个实例。
如果你创建了 JmsTemplate 并指定了一个默认目的地,那么 send(MessageCreator c) 方法会将消息发送到该目的地。
3.2.1. 使用消息转换器
为了便于发送领域模型对象,JmsTemplate 提供了多种发送方法,这些方法接受一个 Java 对象作为消息数据内容的参数。JmsTemplate 中重载的 convertAndSend() 和 receiveAndConvert() 方法将转换过程委托给 MessageConverter 接口的实例。该接口定义了一个简单的契约,用于在 Java 对象和 JMS 消息之间进行转换。默认实现(SimpleMessageConverter)支持 String 与 TextMessage、byte[] 与 BytesMesssage 以及 java.util.Map 与 MapMessage 之间的转换。通过使用转换器,您和您的应用程序代码可以专注于通过 JMS 发送或接收的业务对象,而无需关心其如何表示为 JMS 消息的细节。
沙箱当前包含一个 MapMessageConverter,它使用反射在 JavaBean 和 MapMessage 之间进行转换。你也可以自行实现其他常用的选择,例如使用现有的 XML 序列化包(如 JAXB 或 XStream)来创建表示该对象的 TextMessage。
为了支持对消息的属性、头信息和正文进行设置(这些内容无法被通用地封装在转换器类中),MessagePostProcessor 接口允许您在消息完成转换之后、发送之前对其进行访问。以下示例展示了在将 java.util.Map 转换为消息后,如何修改消息的头信息和属性:
public void sendWithConversion() {
Map map = new HashMap();
map.put("Name", "Mark");
map.put("Age", new Integer(47));
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setIntProperty("AccountID", 1234);
message.setJMSCorrelationID("123-00001");
return message;
}
});
}
这将生成如下形式的消息:
MapMessage={
Header={
... standard headers ...
CorrelationID={123-00001}
}
Properties={
AccountID={Integer:1234}
}
Fields={
Name={String:Mark}
Age={Integer:47}
}
}
3.3. 接收消息
这描述了如何使用Spring接收JMS消息。
3.3.1. 同步接收
虽然 JMS 通常与异步处理相关联,但你也可以同步地消费消息。重载的 receive(..) 方法提供了此功能。在同步接收过程中,调用线程会一直阻塞,直到有消息可用为止。这可能是一项危险的操作,因为调用线程可能会被无限期地阻塞。receiveTimeout 属性用于指定接收器在放弃等待消息之前应等待多长时间。
3.3.2. 异步接收:消息驱动的 POJO
Spring 还通过使用 @JmsListener 注解支持带注解的监听器端点,并提供了一个开放的基础设施,用于以编程方式注册端点。
这是迄今为止设置异步接收器最便捷的方式。
更多详情请参见启用监听器端点注解。 |
与 EJB 世界中的消息驱动 Bean (MDB) 类似,消息驱动 POJO (MDP) 充当 JMS 消息的接收器。MDP 的一个限制(但请参阅 使用 MessageListenerAdapter)是它必须实现 javax.jms.MessageListener 接口。请注意,如果您的 POJO 在多个线程上接收消息,确保您的实现是线程安全的非常重要。
以下示例展示了一个 MDP 的简单实现:
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
public class ExampleListener implements MessageListener {
public void onMessage(Message message) {
if (message instanceof TextMessage) {
try {
System.out.println(((TextMessage) message).getText());
}
catch (JMSException ex) {
throw new RuntimeException(ex);
}
}
else {
throw new IllegalArgumentException("Message must be of type TextMessage");
}
}
}
一旦你实现了自己的 MessageListener,就该创建一个消息监听器容器了。
以下示例展示了如何定义和配置 Spring 自带的某个消息监听器容器(在本例中为 DefaultMessageListenerContainer):
<!-- this is the Message Driven POJO (MDP) -->
<bean id="messageListener" class="jmsexample.ExampleListener"/>
<!-- and this is the message listener container -->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="destination"/>
<property name="messageListener" ref="messageListener"/>
</bean>
有关各个消息监听器容器(全部实现了MessageListenerContainer)所支持功能的完整描述,请参阅 Spring 的相关 JavaDoc 文档。
3.3.3. 使用SessionAwareMessageListener接口
SessionAwareMessageListener 接口是 Spring 特有的一个接口,它提供的契约与 JMS 的 MessageListener 接口类似,但同时还允许消息处理方法访问接收该 Session 所用的 JMS Message。
以下代码清单展示了 SessionAwareMessageListener 接口的定义:
package org.springframework.jms.listener;
public interface SessionAwareMessageListener {
void onMessage(Message message, Session session) throws JMSException;
}
如果你希望你的消息驱动 POJO(MDP)能够响应接收到的任何消息(通过在 MessageListener 方法中使用所提供的 Session),你可以选择让你的 MDP 实现此接口(优先于标准的 JMS onMessage(Message, Session) 接口)。Spring 自带的所有消息监听器容器实现均支持实现了 MessageListener 或 SessionAwareMessageListener 接口的 MDP。需要注意的是,实现 SessionAwareMessageListener 接口的类会因此与 Spring 框架产生耦合。是否使用该接口完全由你作为应用程序开发者或架构师自行决定。
请注意,onMessage(..) 接口的 SessionAwareMessageListener 方法会抛出 JMSException。与标准的 JMS MessageListener 接口不同,当使用 SessionAwareMessageListener 接口时,处理所抛出异常的责任在于客户端代码。
3.3.4. 使用MessageListenerAdapter
MessageListenerAdapter 类是 Spring 异步消息支持中的最后一个组件。简而言之,它允许你将几乎任意类暴露为 MDP(消息驱动 POJO,Message-Driven POJO),尽管存在一些限制条件。
考虑以下接口定义:
public interface MessageDelegate {
void handleMessage(String message);
void handleMessage(Map message);
void handleMessage(byte[] message);
void handleMessage(Serializable message);
}
请注意,尽管该接口既未继承 MessageListener 接口,也未继承 SessionAwareMessageListener 接口,但你仍然可以通过使用 MessageListenerAdapter 类将其作为消息驱动 POJO(MDP)来使用。同时请注意,各个消息处理方法是如何根据它们可以接收和处理的不同 Message 类型的内容进行强类型定义的。
现在考虑以下 MessageDelegate 接口的实现:
public class DefaultMessageDelegate implements MessageDelegate {
// implementation elided for clarity...
}
特别要注意的是,前面所示的 MessageDelegate 接口实现(即 DefaultMessageDelegate 类)完全没有任何 JMS 依赖。它确实是一个普通的 Java 对象(POJO),我们可以通过以下配置将其转变为消息驱动 POJO(MDP):
<!-- this is the Message Driven POJO (MDP) -->
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="jmsexample.DefaultMessageDelegate"/>
</constructor-arg>
</bean>
<!-- and this is the message listener container... -->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="destination"/>
<property name="messageListener" ref="messageListener"/>
</bean>
下一个示例展示了另一个消息驱动 POJO(MDP),它只能处理接收 JMS
TextMessage 消息。请注意,此处的消息处理方法实际名为
receive(在 MessageListenerAdapter 中,消息处理方法的默认名称为 handleMessage),但该名称是可配置的(如本节稍后所示)。同时请注意,receive(..) 方法是强类型的,仅用于接收和响应 JMS
TextMessage 消息。
以下代码清单展示了 TextMessageDelegate 接口的定义:
public interface TextMessageDelegate {
void receive(TextMessage message);
}
以下代码清单展示了一个实现了 TextMessageDelegate 接口的类:
public class DefaultTextMessageDelegate implements TextMessageDelegate {
// implementation elided for clarity...
}
相应的 MessageListenerAdapter 的配置如下所示:
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="jmsexample.DefaultTextMessageDelegate"/>
</constructor-arg>
<property name="defaultListenerMethod" value="receive"/>
<!-- we don't want automatic message context extraction -->
<property name="messageConverter">
<null/>
</property>
</bean>
请注意,如果 messageListener 接收到的 JMS Message 类型不是 TextMessage,则会抛出一个 IllegalStateException(随后被忽略)。此外,MessageListenerAdapter 类的另一项功能是:如果处理方法返回一个非 void 值,它能够自动发送一个响应 Message。请考虑以下接口和类:
public interface ResponsiveTextMessageDelegate {
// notice the return type...
String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
// implementation elided for clarity...
}
如果您将 DefaultResponsiveTextMessageDelegate 与 MessageListenerAdapter 结合使用,则从 'receive(..)' 方法执行返回的任何非空值(在默认配置下)都会被转换为 TextMessage。生成的 TextMessage 随后会被发送到原始 Message 的 JMS Reply-To 属性中定义的 Destination(如果存在),或者发送到在 MessageListenerAdapter 上设置的默认 Destination(如果已配置)。如果未找到 Destination,则会抛出 InvalidDestinationException(请注意,此异常不会被吞没,而是会向上传播到调用栈)。
3.3.5. 在事务中处理消息
在事务中调用消息监听器仅需重新配置监听器容器。
您可以通过监听器容器定义中的 sessionTransacted 标志来启用本地资源事务。这样,每次消息监听器的调用都会在一个活跃的 JMS 事务中执行,如果监听器执行失败,消息接收操作将被回滚。发送响应消息(通过 SessionAwareMessageListener)属于同一个本地事务,但其他任何资源操作(例如数据库访问)则独立进行。这通常要求在监听器实现中包含重复消息检测机制,以处理数据库处理已提交但消息处理未能成功提交的情况。
请考虑以下 bean 定义:
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="destination"/>
<property name="messageListener" ref="messageListener"/>
<property name="sessionTransacted" value="true"/>
</bean>
要参与外部管理的事务,您需要配置一个事务管理器,并使用支持外部管理事务的监听器容器(通常为 DefaultMessageListenerContainer)。
要为 XA 事务参与配置消息监听器容器,您需要配置一个 JtaTransactionManager(默认情况下,它会委托给 Java EE 服务器的事务子系统)。请注意,底层的 JMS ConnectionFactory 必须支持 XA,并且已正确注册到您的 JTA 事务协调器中。(请检查您的 Java EE 服务器对 JNDI 资源的配置。)这样可以让消息接收以及(例如)数据库访问成为同一事务的一部分(具有统一的提交语义,但会带来 XA 事务日志的开销)。
以下的 bean 定义创建了一个事务管理器:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
然后我们需要将其添加到我们之前的容器配置中。容器会处理其余的工作。以下示例展示了如何实现这一点:
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="destination"/>
<property name="messageListener" ref="messageListener"/>
<property name="transactionManager" ref="transactionManager"/> (1)
</bean>
| 1 | 我们的事务管理器。 |
3.4. 支持 JCA 消息端点
从 2.5 版本开始,Spring 还提供了基于 JCA 的MessageListener容器支持。JmsMessageEndpointManager会尝试根据提供者的ActivationSpec类名自动确定ResourceAdapter类名。因此,通常可以像下面示例所示那样,直接提供 Spring 通用的JmsActivationSpecConfig:
<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
<property name="resourceAdapter" ref="resourceAdapter"/>
<property name="activationSpecConfig">
<bean class="org.springframework.jms.listener.endpoint.JmsActivationSpecConfig">
<property name="destinationName" value="myQueue"/>
</bean>
</property>
<property name="messageListener" ref="myMessageListener"/>
</bean>
或者,你可以使用给定的 JmsMessageEndpointManager 对象来配置一个 ActivationSpec。ActivationSpec 对象也可以通过 JNDI 查找获得(使用 <jee:jndi-lookup>)。以下示例展示了如何进行此操作:
<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
<property name="resourceAdapter" ref="resourceAdapter"/>
<property name="activationSpec">
<bean class="org.apache.activemq.ra.ActiveMQActivationSpec">
<property name="destination" value="myQueue"/>
<property name="destinationType" value="javax.jms.Queue"/>
</bean>
</property>
<property name="messageListener" ref="myMessageListener"/>
</bean>
使用 Spring 的 ResourceAdapterFactoryBean,您可以像下面示例所示那样在本地配置目标 ResourceAdapter:
<bean id="resourceAdapter" class="org.springframework.jca.support.ResourceAdapterFactoryBean">
<property name="resourceAdapter">
<bean class="org.apache.activemq.ra.ActiveMQResourceAdapter">
<property name="serverUrl" value="tcp://localhost:61616"/>
</bean>
</property>
<property name="workManager">
<bean class="org.springframework.jca.work.SimpleTaskWorkManager"/>
</property>
</bean>
指定的 WorkManager 也可以指向一个环境特定的线程池——通常通过 SimpleTaskWorkManager 实例的 asyncTaskExecutor 属性来实现。如果您使用了多个适配器,建议为所有 ResourceAdapter 实例定义一个共享的线程池。
在某些环境中(例如 WebLogic 9 或更高版本),您可以改为从 JNDI 获取整个 ResourceAdapter 对象(通过使用 <jee:jndi-lookup>)。基于 Spring 的消息监听器随后可以与服务器托管的 ResourceAdapter 进行交互,该适配器也会使用服务器内置的 WorkManager。
有关更多详细信息,请参阅 JmsMessageEndpointManager、
JmsActivationSpecConfig
和 ResourceAdapterFactoryBean 的 Javadoc。
Spring 还提供了一个通用的 JCA 消息端点管理器,该管理器不依赖于 JMS:
org.springframework.jca.endpoint.GenericMessageEndpointManager。此组件允许使用任何类型的消息监听器(例如 CCI MessageListener)以及任何特定于提供者的 ActivationSpec 对象。请参阅您的 JCA 提供者文档,以了解连接器的实际功能,并查看
GenericMessageEndpointManager
的 Javadoc 以获取 Spring 特定的配置详情。
| 基于 JCA 的消息端点管理与 EJB 2.1 的消息驱动 Bean(Message-Driven Beans)非常类似。 它使用相同的底层资源提供者契约。与 EJB 2.1 MDB 一样,您也可以在 Spring 上下文中使用 JCA 提供者所支持的任何 消息监听器接口。 不过,Spring 仍然为 JMS 提供了明确的“便捷”支持,因为 JMS 是与 JCA 端点管理契约一起使用的 最常见端点 API。 |
3.5. 基于注解的监听器端点
接收消息最简单的方式是使用带注解的监听器端点基础设施。简而言之,它允许你将一个托管 Bean 的方法暴露为 JMS 监听器端点。以下示例展示了如何使用它:
@Component
public class MyService {
@JmsListener(destination = "myDestination")
public void processOrder(String data) { ... }
}
上述示例的理念是,每当javax.jms.Destination myDestination上有消息可用时,就会相应地调用processOrder方法(在本例中,使用 JMS 消息的内容,类似于MessageListenerAdapter所提供的功能)。
带注解的端点基础设施会为每个带注解的方法在后台创建一个消息监听器容器,该容器通过使用 JmsListenerContainerFactory 来创建。
此类容器不会注册到应用上下文中,但可以通过 JmsListenerEndpointRegistry bean 轻松定位,以便进行管理。
@JmsListener 是 Java 8 中的可重复注解,因此你可以通过在同一个方法上添加多个 @JmsListener 声明,将其与多个 JMS 目的地关联起来。 |
3.5.1. 启用监听器端点注解
要启用对 @JmsListener 注解的支持,您可以在其中一个 @EnableJms 类上添加 @Configuration,如下例所示:
@Configuration
@EnableJms
public class AppConfig {
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setDestinationResolver(destinationResolver());
factory.setSessionTransacted(true);
factory.setConcurrency("3-10");
return factory;
}
}
默认情况下,该基础设施会查找名为 jmsListenerContainerFactory 的 bean,
作为用于创建消息监听器容器的工厂来源。在此情况下(忽略 JMS 基础设施的设置),
你可以使用核心线程池大小为 3、最大线程池大小为 10 来调用 processOrder 方法。
您可以为每个注解自定义要使用的监听器容器工厂,或者通过实现 JmsListenerConfigurer 接口来配置一个明确的默认值。
仅当至少有一个端点在没有指定特定容器工厂的情况下注册时,才需要该默认值。有关详细信息和示例,请参阅实现
JmsListenerConfigurer
的类的 Javadoc。
如果你更喜欢XML 配置,可以使用<jms:annotation-driven>元素,如下例所示:
<jms:annotation-driven/>
<bean id="jmsListenerContainerFactory"
class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destinationResolver" ref="destinationResolver"/>
<property name="sessionTransacted" value="true"/>
<property name="concurrency" value="3-10"/>
</bean>
3.5.2. 以编程方式注册端点
JmsListenerEndpoint 提供了一个 JMS 端点的模型,并负责根据该模型配置容器。
除了通过 JmsListener 注解检测到的端点之外,该基础设施还允许你以编程方式配置端点。
以下示例展示了如何实现这一点:
@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {
@Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
endpoint.setId("myJmsEndpoint");
endpoint.setDestination("anotherQueue");
endpoint.setMessageListener(message -> {
// processing
});
registrar.registerEndpoint(endpoint);
}
}
在前面的示例中,我们使用了 SimpleJmsListenerEndpoint,它提供了要调用的实际 MessageListener。不过,你也可以构建自己的端点变体,以描述自定义的调用机制。
请注意,您可以完全跳过使用 @JmsListener,而仅通过 JmsListenerConfigurer 以编程方式注册您的端点。
3.5.3. 带注解的端点方法签名
到目前为止,我们一直在端点中注入一个简单的String,但实际上它的方法签名可以非常灵活。在下面的示例中,我们将其重写为通过自定义请求头注入Order对象:
@Component
public class MyService {
@JmsListener(destination = "myDestination")
public void processOrder(Order order, @Header("order_type") String orderType) {
...
}
}
你可以在 JMS 监听器端点中注入的主要元素如下:
-
原始的
javax.jms.Message或其任意子类(前提是它与传入的消息类型匹配)。 -
用于可选地访问原生 JMS API 的
javax.jms.Session(例如,用于发送自定义回复)。 -
表示传入的 JMS 消息的
org.springframework.messaging.Message。 请注意,此消息同时包含自定义头信息和标准头信息(由JmsHeaders定义)。 -
@Header注解的方法参数用于提取特定的头部值,包括标准的 JMS 头部。 -
一个使用
@Headers注解的参数,该参数还必须可赋值给java.util.Map,以便访问所有请求头。 -
一个未加注解且不属于受支持类型(
Message或Session)的元素将被视为有效载荷(payload)。您可以通过在参数上添加@Payload注解来显式地表明这一点。此外,您还可以通过额外添加@Valid注解来启用验证。
注入 Spring 的 Message 抽象的能力特别有用,它能让你充分利用存储在传输协议特定消息中的所有信息,而无需依赖于传输协议特定的 API。以下示例展示了如何实现这一点:
@JmsListener(destination = "myDestination")
public void processOrder(Message<Order> order) { ... }
方法参数的处理由 DefaultMessageHandlerMethodFactory 提供,您可以进一步自定义该工厂以支持额外的方法参数。您也可以在此处自定义类型转换和验证支持。
例如,如果我们希望在处理 Order 之前确保其有效性,可以使用 @Valid 注解标注该有效载荷,并配置必要的验证器,如下例所示:
@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {
@Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
}
@Bean
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(myValidator());
return factory;
}
}
3.5.4. 响应管理
MessageListenerAdapter 中现有的支持已经允许您的方法具有非 void 返回类型。在这种情况下,调用的结果将被封装在 javax.jms.Message 中,并发送到原始消息的 JMSReplyTo 头中指定的目标,或发送到监听器上配置的默认目标。您现在可以使用消息抽象的 @SendTo 注解来设置该默认目标。
假设我们的 processOrder 方法现在应返回一个 OrderStatus,我们可以将其编写为自动发送响应,如下例所示:
@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
// order processing
return status;
}
如果你有多个使用 @JmsListener 注解的方法,也可以将 @SendTo 注解放在类级别上,以共享一个默认的回复目标。 |
如果你需要以与传输无关的方式设置额外的头部信息,可以改为返回一个Message,方法类似于以下所示:
@JmsListener(destination = "myDestination")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
// order processing
return MessageBuilder
.withPayload(status)
.setHeader("code", 1234)
.build();
}
如果你需要在运行时计算响应的目标地址,可以将你的响应封装在一个 JmsResponse 实例中,该实例同时提供运行时要使用的目标地址。我们可以将前面的示例重写如下:
@JmsListener(destination = "myDestination")
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
// order processing
Message<OrderStatus> response = MessageBuilder
.withPayload(status)
.setHeader("code", 1234)
.build();
return JmsResponse.forQueue(response, "status");
}
最后,如果你需要为响应指定某些服务质量(QoS)值,例如优先级或生存时间(TTL),你可以相应地配置 JmsListenerContainerFactory,如下例所示:
@Configuration
@EnableJms
public class AppConfig {
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
QosSettings replyQosSettings = new QosSettings();
replyQosSettings.setPriority(2);
replyQosSettings.setTimeToLive(10000);
factory.setReplyQosSettings(replyQosSettings);
return factory;
}
}
3.6. JMS 命名空间支持
Spring 提供了用于简化 JMS 配置的 XML 命名空间。要使用 JMS 命名空间元素,您需要引用 JMS 架构,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms" (1)
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd">
<!-- bean definitions here -->
</beans>
| 1 | 引用JMS模式。 |
该命名空间包含三个顶级元素:<annotation-driven/>、<listener-container/>
和<jca-listener-container/>。<annotation-driven/> 启用基于注解的监听器端点。
<listener-container/> 和 <jca-listener-container/>
用于定义共享的监听器容器配置,并可包含 <listener/> 子元素。
以下示例展示了两个监听器的基本配置:
<jms:listener-container>
<jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>
<jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>
</jms:listener-container>
前面的示例等效于创建两个不同的监听器容器 Bean 定义和两个不同的 MessageListenerAdapter Bean 定义,如 使用 MessageListenerAdapter 中所示。除了前面示例中显示的属性外,listener 元素还可以包含多个可选属性。下表描述了所有可用的属性:
| 属性 | 描述 |
|---|---|
|
用于托管监听器容器的 Bean 名称。如果未指定,将自动生成一个 Bean 名称。 |
|
此监听器的目标名称,通过 |
|
处理器对象的 Bean 名称。 |
|
要调用的处理方法的名称。如果 |
|
用于发送响应消息的默认响应目标名称。当请求消息未包含 |
|
持久订阅的名称(如果有的话)。 |
|
此监听器的可选消息选择器。 |
|
为此监听器启动的并发会话数或消费者数量。该值可以是一个简单数字,表示最大数量(例如, |
<listener-container/> 元素还接受多个可选属性。这允许您自定义各种策略(例如 taskExecutor 和 destinationResolver),以及基本的 JMS 设置和资源引用。通过使用这些属性,您可以在仍然享受命名空间便利性的同时,定义高度定制化的监听器容器。
你可以通过指定 JmsListenerContainerFactory 属性来暴露 bean 的 id,从而自动将此类设置暴露为 factory-id,如下例所示:
<jms:listener-container connection-factory="myConnectionFactory"
task-executor="myTaskExecutor"
destination-resolver="myDestinationResolver"
transaction-manager="myTransactionManager"
concurrency="10">
<jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>
<jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>
</jms:listener-container>
下表描述了所有可用的属性。有关各个属性的更多详细信息,请参阅 AbstractMessageListenerContainer 及其具体子类的类级别 Javadoc。该 Javadoc 还讨论了事务选择和消息重新投递场景。
| 属性 | 描述 |
|---|---|
|
此监听器容器的类型。可用选项包括 |
|
一个自定义监听器容器实现类的完整限定类名。
默认为 Spring 的标准 |
|
将此元素定义的设置以指定的 |
|
对 JMS |
|
对 Spring |
|
对用于解析 JMS |
|
对用于将 JMS 消息转换为监听器方法参数的 |
|
对一个 |
|
此监听器的 JMS 目标类型: |
|
响应的 JMS 目标类型: |
|
此监听器容器的 JMS 客户端 ID。当您使用持久订阅时,必须指定该 ID。 |
|
JMS 资源的缓存级别: |
|
原生 JMS 确认模式: |
|
对外部 |
|
为每个监听器启动的并发会话或消费者的数量。它可以是一个简单的数字,表示最大数量(例如, |
|
单个会话中可加载的最大消息数量。请注意,提高此数值可能会导致并发消费者出现饥饿现象。 |
|
用于接收调用的超时时间(以毫秒为单位)。默认值为 |
|
指定用于计算恢复尝试间隔的 |
|
指定恢复尝试之间的间隔时间,单位为毫秒。它提供了一种便捷的方式来创建一个具有指定间隔的 |
|
此容器应在其中启动和停止的生命周期阶段。数值越小,该容器启动得越早,停止得越晚。默认值为
|
使用 jms 命名空间支持来配置基于 JCA 的监听器容器非常相似,如下例所示:
<jms:jca-listener-container resource-adapter="myResourceAdapter"
destination-resolver="myDestinationResolver"
transaction-manager="myTransactionManager"
concurrency="10">
<jms:listener destination="queue.orders" ref="myMessageListener"/>
</jms:jca-listener-container>
下表描述了 JCA 变体可用的配置选项:
| 属性 | 描述 |
|---|---|
|
将此元素定义的设置以指定的 |
|
对 JCA |
|
对 |
|
对用于解析 JMS |
|
对用于将 JMS 消息转换为监听器方法参数的 |
|
此监听器的 JMS 目标类型: |
|
响应的 JMS 目标类型: |
|
此监听器容器的 JMS 客户端 ID。使用持久订阅时需要指定该值。 |
|
原生 JMS 确认模式: |
|
对 Spring 的 |
|
为每个监听器启动的并发会话数或消费者数量。它可以是一个简单的数字,表示最大数量(例如 |
|
单个会话中可加载的最大消息数量。请注意,提高此数值可能会导致并发消费者出现饥饿现象。 |
4. JMX
Spring 中的 JMX(Java 管理扩展)支持提供了相关功能,可让你轻松且透明地将 Spring 应用程序集成到 JMX 基础设施中。
具体而言,Spring 的 JMX 支持提供了四项核心功能:
-
将任意 Spring Bean 自动注册为 JMX MBean。
-
一种用于控制 Bean 管理接口的灵活机制。
-
通过远程 JSR-160 连接器以声明方式暴露 MBean。
-
对本地和远程 MBean 资源的简单代理。
这些功能的设计目标是在不将您的应用程序组件与 Spring 或 JMX 接口和类耦合的情况下正常工作。事实上,在大多数情况下,您的应用程序类无需知晓 Spring 或 JMX,即可充分利用 Spring 提供的 JMX 功能。
4.1. 将您的 Bean 导出到 JMX
Spring JMX 框架中的核心类是 MBeanExporter。该类负责将你的 Spring Bean 注册到 JMX 的 MBeanServer 中。
例如,考虑以下类:
package org.springframework.jmx;
public class JmxTestBean implements IJmxTestBean {
private String name;
private int age;
private boolean isSuperman;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int add(int x, int y) {
return x + y;
}
public void dontExposeMe() {
throw new RuntimeException();
}
}
要将此 bean 的属性和方法作为 MBean 的属性和操作暴露出来,您可以在配置文件中配置一个 MBeanExporter 类的实例,并传入该 bean,如下例所示:
<beans>
<!-- this bean must not be lazily initialized if the exporting is to happen -->
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
前述配置片段中相关的 Bean 定义是 exporter Bean。beans 属性会明确告知 MBeanExporter 必须将您的哪些 Bean 导出到 JMX MBeanServer。在默认配置下,beans Map 中每个条目的键将用作对应条目值所引用的 Bean 的 ObjectName。您可以更改此行为,具体请参阅 控制您 Bean 的 ObjectName 实例。
通过此配置,testBean bean 会以 MBean 的形式暴露出来,其 ObjectName 为 bean:name=testBean1。默认情况下,该 bean 的所有 public 属性都会作为属性(attributes)暴露出来,所有 public 方法(从 Object 类继承的方法除外)都会作为操作(operations)暴露出来。
MBeanExporter 是一个 Lifecycle bean(参见启动和关闭回调)。默认情况下,MBean 会在应用程序生命周期中尽可能晚的阶段进行导出。你可以配置导出发生的phase(阶段),或者通过设置 autoStartup 标志来禁用自动注册。 |
4.1.1. 创建 MBeanServer
上一节中所示的配置假定应用程序运行在一个已经存在一个(且仅有一个)MBeanServer的环境中。在这种情况下,Spring 会尝试查找正在运行的 MBeanServer,并将您的 bean 注册到该服务器上(如果存在的话)。当您的应用程序运行在拥有自身 MBeanServer 的容器(例如 Tomcat 或 IBM WebSphere)中时,这种行为非常有用。
然而,这种方法在独立环境(standalone environment)中或在未提供 MBeanServer 的容器中运行时毫无用处。为了解决这个问题,您可以在配置中添加一个 MBeanServer 类的实例,以声明式的方式创建一个 org.springframework.jmx.support.MBeanServerFactoryBean 实例。
您还可以通过将 MBeanServer 实例的 MBeanExporter 属性设置为由 server 返回的 MBeanServer 值,来确保使用特定的 MBeanServerFactoryBean,如下例所示:
<beans>
<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"/>
<!--
this bean needs to be eagerly pre-instantiated in order for the exporting to occur;
this means that it must not be marked as lazily initialized
-->
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
<property name="server" ref="mbeanServer"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
在前面的示例中,MBeanServer 的一个实例由 MBeanServerFactoryBean 创建,并通过 MBeanExporter 属性提供给 server。当您提供自己的 MBeanServer 实例时,MBeanExporter 不会尝试查找正在运行的 MBeanServer,而是使用所提供的 MBeanServer 实例。要使此功能正常工作,您的类路径中必须包含一个 JMX 实现。
4.1.2. 复用现有的MBeanServer
如果没有指定服务器,MBeanExporter 会尝试自动检测一个正在运行的
MBeanServer。这在大多数环境中都能正常工作,因为通常只使用一个 MBeanServer 实例。
然而,当存在多个实例时,导出器可能会选择错误的服务器。
在这种情况下,您应使用 MBeanServer 的 agentId 来指明应使用哪个实例,如下例所示:
<beans>
<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
<!-- indicate to first look for a server -->
<property name="locateExistingServerIfPossible" value="true"/>
<!-- search for the MBeanServer instance with the given agentId -->
<property name="agentId" value="MBeanServer_instance_agentId>"/>
</bean>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="server" ref="mbeanServer"/>
...
</bean>
</beans>
对于那些现有 MBeanServer 具有动态(或未知)agentId 的平台或场景,该 core.html#beans-factory-class-static-factory-method 需通过查找方法获取,此时应使用
factory-method,
如下例所示:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="server">
<!-- Custom MBeanServerLocator -->
<bean class="platform.package.MBeanServerLocator" factory-method="locateMBeanServer"/>
</property>
</bean>
<!-- other beans here -->
</beans>
4.1.3. 延迟初始化的 MBean
如果你配置了一个使用 MBeanExporter 的 Bean,并且该 Bean 同时被配置为延迟初始化,那么 MBeanExporter 不会破坏这一约定,也不会提前实例化该 Bean。相反,它会向 MBeanServer 注册一个代理,并将从容器中获取该 Bean 的操作推迟到首次通过该代理进行调用时才执行。
4.1.4. MBean 的自动注册
任何通过 MBeanExporter 导出且已经是有效 MBean 的 Bean,都会直接注册到 MBeanServer 中,而无需 Spring 进一步干预。你可以通过将 MBeanExporter 属性设置为 autodetect,使 true 自动检测 MBean,如下例所示:
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="autodetect" value="true"/>
</bean>
<bean name="spring:mbean=true" class="org.springframework.jmx.export.TestDynamicMBean"/>
在前面的示例中,名为 spring:mbean=true 的 Bean 已经是一个有效的 JMX MBean,并由 Spring 自动注册。默认情况下,被自动检测用于 JMX 注册的 Bean 会将其 Bean 名称用作 ObjectName。您可以覆盖此行为,详见 控制您 Bean 的 ObjectName 实例。
4.1.5. 控制注册行为
考虑这样一种场景:Spring 的 MBeanExporter 尝试使用 MBean MBeanServer 向 ObjectName 注册一个 bean:name=testBean1。如果已有另一个 MBean 实例注册在相同的 ObjectName 下,默认行为是失败(并抛出 InstanceAlreadyExistsException)。
你可以精确控制当一个 MBean 向 MBeanServer 注册时所发生的行为。Spring 的 JMX 支持三种不同的注册行为,用于在注册过程中发现已有相同 MBean 的 ObjectName 时控制注册行为。下表总结了这些注册行为:
| 注册行为 | 说明 |
|---|---|
|
这是默认的注册行为。如果一个 |
|
如果已有一个 |
|
如果一个 |
上表中的值在 RegistrationPolicy 类中定义为枚举。
如果你想更改默认的注册行为,需要将 registrationPolicy 定义中的
MBeanExporter 属性值设置为这些枚举值之一。
以下示例展示了如何将默认的注册行为更改为 REPLACE_EXISTING 行为:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
<property name="registrationPolicy" value="REPLACE_EXISTING"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
4.2. 控制 Bean 的管理接口
在前一节的示例中,
您对自己的 bean 的管理接口几乎没有控制权。每个导出的 bean 的所有public
属性和方法分别被暴露为 JMX 属性和操作。为了更精细地控制导出的 bean 中
哪些属性和方法实际被暴露为 JMX 属性和操作,Spring JMX 提供了一种全面且可扩展的机制,
用于控制 bean 的管理接口。
4.2.1. 使用MBeanInfoAssembler接口
在幕后,MBeanExporter 会委托给一个实现了
org.springframework.jmx.export.assembler.MBeanInfoAssembler 接口的类,
该类负责定义每个被暴露 Bean 的管理接口。
默认的实现是
org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler,
它所定义的管理接口会暴露所有公共属性和方法
(正如你在前面各节的示例中所看到的那样)。
Spring 还提供了另外两种 MBeanInfoAssembler 接口的实现,
允许你通过源代码级别的元数据或任意接口来控制生成的管理接口。
4.2.2. 使用源码级元数据:Java 注解
通过使用 MetadataMBeanInfoAssembler,您可以利用源代码级别的元数据来定义 Bean 的管理接口。元数据的读取由 org.springframework.jmx.export.metadata.JmxAttributeSource 接口进行封装。Spring JMX 提供了一个默认实现,该实现使用 Java 注解,即 org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource。您必须为 MetadataMBeanInfoAssembler 配置一个 JmxAttributeSource 接口的实现实例,才能使其正常工作(没有默认实现)。
要将一个 Bean 标记为导出到 JMX,您应使用 ManagedResource 注解标注该 Bean 的类。您必须使用 ManagedOperation 注解标注希望作为操作暴露的每个方法,并使用 ManagedAttribute 注解标注希望暴露的每个属性。在标注属性时,您可以省略 getter 或 setter 方法上的注解,以分别创建只写或只读属性。
带有 ManagedResource 注解的 Bean 必须是 public 的,暴露操作或属性的方法也必须是 public 的。 |
以下示例展示了我们在创建 MBeanServer中使用的#jmx-exporting-mbeanserver类的注解版本:
package org.springframework.jmx;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedAttribute;
@ManagedResource(
objectName="bean:name=testBean4",
description="My Managed Bean",
log=true,
logFile="jmx.log",
currencyTimeLimit=15,
persistPolicy="OnUpdate",
persistPeriod=200,
persistLocation="foo",
persistName="bar")
public class AnnotationTestBean implements IJmxTestBean {
private String name;
private int age;
@ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15)
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@ManagedAttribute(description="The Name Attribute",
currencyTimeLimit=20,
defaultValue="bar",
persistPolicy="OnUpdate")
public void setName(String name) {
this.name = name;
}
@ManagedAttribute(defaultValue="foo", persistPeriod=300)
public String getName() {
return name;
}
@ManagedOperation(description="Add two numbers")
@ManagedOperationParameters({
@ManagedOperationParameter(name = "x", description = "The first number"),
@ManagedOperationParameter(name = "y", description = "The second number")})
public int add(int x, int y) {
return x + y;
}
public void dontExposeMe() {
throw new RuntimeException();
}
}
在前面的示例中,您可以看到 JmxTestBean 类使用了 ManagedResource 注解,并且该 ManagedResource 注解配置了一组属性。这些属性可用于配置由 MBeanExporter 生成的 MBean 的各个方面,具体细节将在后面的源码级元数据类型一节中详细说明。
age 和 name 两个属性都使用了 ManagedAttribute 注解,但在 age 属性的情况下,仅 getter 方法被标记。
这会导致这两个属性都被包含在管理接口中作为属性,但 age 属性是只读的。
最后,add(int, int) 方法使用了 ManagedOperation 注解进行标记,
而 dontExposeMe() 方法则没有。当你使用 add(int, int) 时,
这将导致管理接口仅包含一个操作(即 MetadataMBeanInfoAssembler)。
以下配置展示了如何配置 MBeanExporter 以使用
MetadataMBeanInfoAssembler:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="assembler" ref="assembler"/>
<property name="namingStrategy" ref="namingStrategy"/>
<property name="autodetect" value="true"/>
</bean>
<bean id="jmxAttributeSource"
class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
<!-- will create management interface using annotation metadata -->
<bean id="assembler"
class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<!-- will pick up the ObjectName from the annotation -->
<bean id="namingStrategy"
class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.AnnotationTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
在前面的示例中,一个 MetadataMBeanInfoAssembler Bean 已使用 AnnotationJmxAttributeSource 类的实例进行配置,并通过 assembler 属性传递给 MBeanExporter。只需完成这些配置,即可为 Spring 暴露的 MBean 启用基于元数据的管理接口。
4.2.3. 源码级元数据类型
下表描述了可在 Spring JMX 中使用的源代码级元数据类型:
| 目的 | 注解 | 注解类型 |
|---|---|---|
将某个 |
|
类 |
将一个方法标记为 JMX 操作。 |
|
方法 |
将一个 getter 或 setter 方法标记为 JMX 属性的一半。 |
|
方法(仅限 getter 和 setter) |
为操作参数定义描述。 |
|
方法 |
下表描述了可在这些源代码级元数据类型上使用的配置参数:
| 参数 | 描述 | 适用范围 |
|---|---|---|
|
由 |
|
|
设置资源、属性或操作的友好描述。 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置操作参数的显示名称。 |
|
|
设置操作参数的索引。 |
|
4.2.4. 使用AutodetectCapableMBeanInfoAssembler接口
为了进一步简化配置,Spring 提供了 AutodetectCapableMBeanInfoAssembler 接口,该接口扩展了 MBeanInfoAssembler 接口,以增加对 MBean 资源自动检测的支持。如果你使用 MBeanExporter 的实例来配置 AutodetectCapableMBeanInfoAssembler,它就可以对是否将某个 Bean 暴露给 JMX 进行“投票”决定。
AutodetectCapableMBeanInfo 接口的唯一实现是
MetadataMBeanInfoAssembler,它会投票包含所有标有
ManagedResource 注解的 Bean。在此情况下,默认方法是使用
Bean 名称作为 ObjectName,从而产生类似于以下的配置:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<!-- notice how no 'beans' are explicitly configured here -->
<property name="autodetect" value="true"/>
<property name="assembler" ref="assembler"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource">
<bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
</property>
</bean>
</beans>
请注意,在上述配置中,没有将任何 bean 传递给 MBeanExporter。
然而,JmxTestBean 仍然被注册,因为它标记了 ManagedResource
属性,且 MetadataMBeanInfoAssembler 会检测到这一点并投票决定将其包含在内。
这种方法唯一的问题在于,JmxTestBean 的名称现在具有业务含义。您可以通过更改 控制 Bean 的 ObjectName 实例 中定义的 ObjectName 创建的默认行为来解决此问题。
4.2.5. 使用 Java 接口定义管理接口
除了 MetadataMBeanInfoAssembler 之外,Spring 还提供了
InterfaceBasedMBeanInfoAssembler,它允许你根据一组接口中定义的方法集合来限制所暴露的方法和属性。
尽管暴露 MBean 的标准机制是使用接口和简单的命名规则,但 InterfaceBasedMBeanInfoAssembler 通过以下方式扩展了这一功能:不再需要遵循命名约定,允许你使用多个接口,并且无需让你的 bean 实现 MBean 接口。
考虑以下接口,它用于为我们之前展示的 JmxTestBean 类定义一个管理接口:
public interface IJmxTestBean {
public int add(int x, int y);
public long myOperation();
public int getAge();
public void setAge(int age);
public void setName(String name);
public String getName();
}
该接口定义了在 JMX MBean 上作为操作和属性公开的方法和属性。以下代码展示了如何配置 Spring JMX,以使用此接口作为管理接口的定义:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean5" value-ref="testBean"/>
</map>
</property>
<property name="assembler">
<bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
<property name="managedInterfaces">
<value>org.springframework.jmx.IJmxTestBean</value>
</property>
</bean>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
在前面的示例中,InterfaceBasedMBeanInfoAssembler 被配置为在为任意 bean 构建管理接口时使用 IJmxTestBean 接口。需要注意的是,由 InterfaceBasedMBeanInfoAssembler 处理的 bean 并不要求实现用于生成 JMX 管理接口的该接口。
在前述情况下,IJmxTestBean 接口被用于为所有 Bean 构建管理接口。但在许多情况下,这并非期望的行为,你可能希望为不同的 Bean 使用不同的接口。此时,你可以通过 InterfaceBasedMBeanInfoAssembler 属性向 Properties 传入一个 interfaceMappings 实例,其中每个条目的键是 Bean 的名称,值则是该 Bean 所应使用的一组以逗号分隔的接口名称列表。
如果未通过 managedInterfaces 或 interfaceMappings 属性指定管理接口,则 InterfaceBasedMBeanInfoAssembler 会通过反射检查该 Bean,并使用该 Bean 实现的所有接口来创建管理接口。
4.2.6. 使用MethodNameBasedMBeanInfoAssembler
MethodNameBasedMBeanInfoAssembler 允许你指定一组方法名称,这些方法将作为属性和操作暴露给 JMX。以下代码展示了一个示例配置:
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean5" value-ref="testBean"/>
</map>
</property>
<property name="assembler">
<bean class="org.springframework.jmx.export.assembler.MethodNameBasedMBeanInfoAssembler">
<property name="managedMethods">
<value>add,myOperation,getName,setName,getAge</value>
</property>
</bean>
</property>
</bean>
在前面的示例中,您可以看到 add 和 myOperation 方法被暴露为 JMX 操作,而 getName()、setName(String) 和 getAge() 则被暴露为 JMX 属性的相应部分(读取或写入方法)。在上述代码中,这些方法映射适用于暴露给 JMX 的 Bean。若要逐个 Bean 地控制方法的暴露行为,您可以使用 methodMappings 的 MethodNameMBeanInfoAssembler 属性,将 Bean 名称映射到对应的方法名称列表。
4.3. 控制ObjectName为您的 Bean 创建实例
在幕后,MBeanExporter 会委托 ObjectNamingStrategy 的实现,为其注册的每个 bean 获取一个 ObjectName 实例。默认情况下,默认实现 KeyNamingStrategy 使用 beans 的 Map 键作为 ObjectName。此外,KeyNamingStrategy 可以将 beans 的 Map 键映射到 Properties 文件(或多个文件)中的条目,以解析 ObjectName。除了 KeyNamingStrategy 之外,Spring 还提供了另外两个 ObjectNamingStrategy 实现:IdentityNamingStrategy(根据 bean 的 JVM 身份构建 ObjectName)和 MetadataNamingStrategy(使用源码级元数据来获取 ObjectName)。
4.3.1. 读取ObjectName来自属性的实例
您可以配置自己的 KeyNamingStrategy 实例,并将其设置为从 ObjectName 实例中读取 Properties 实例,而不是使用 bean 的键。 KeyNamingStrategy 会尝试在 Properties 中查找一个键与 bean 键对应的条目。如果未找到该条目,或者 Properties 实例为 null,则直接使用 bean 键本身。
以下代码展示了 KeyNamingStrategy 的一个示例配置:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="testBean" value-ref="testBean"/>
</map>
</property>
<property name="namingStrategy" ref="namingStrategy"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="namingStrategy" class="org.springframework.jmx.export.naming.KeyNamingStrategy">
<property name="mappings">
<props>
<prop key="testBean">bean:name=testBean1</prop>
</props>
</property>
<property name="mappingLocations">
<value>names1.properties,names2.properties</value>
</property>
</bean>
</beans>
前面的示例配置了一个 KeyNamingStrategy 实例,其使用的 Properties 实例是由 mapping 属性所定义的 Properties 实例与 mappings 属性所指定路径下的属性文件合并而成的。在此配置中,testBean bean 被赋予了一个 ObjectName,其值为 bean:name=testBean1,因为这是 Properties 实例中与 bean 键相对应的键所对应的条目。
如果在 Properties 实例中找不到任何条目,则使用 bean 的键名作为 ObjectName。
4.3.2. 使用MetadataNamingStrategy
MetadataNamingStrategy 使用每个 Bean 上 objectName 属性的 ManagedResource 属性来创建 ObjectName。以下代码展示了 MetadataNamingStrategy 的配置:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="testBean" value-ref="testBean"/>
</map>
</property>
<property name="namingStrategy" ref="namingStrategy"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="namingStrategy" class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
<property name="attributeSource" ref="attributeSource"/>
</bean>
<bean id="attributeSource"
class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
</beans>
如果未为 objectName 属性提供 ManagedResource,则会按照以下格式创建一个 ObjectName:[完整限定包名]:type=[简短类名],name=[Bean名称]。例如,以下 Bean 生成的 ObjectName 将是 com.example:type=MyClass,name=myBean:
<bean id="myBean" class="com.example.MyClass"/>
4.3.3. 配置基于注解的 MBean 导出
如果您更喜欢使用 基于注解的方法 来定义管理接口,那么可以使用 MBeanExporter 的一个便捷子类:AnnotationMBeanExporter。在定义该子类的实例时,您不再需要 namingStrategy、assembler 和 attributeSource 配置,因为它始终使用标准的基于 Java 注解的元数据(自动检测也始终启用)。实际上,与其定义一个 MBeanExporter Bean,@EnableMBeanExport @Configuration 注解还支持更简单的语法,如下例所示:
@Configuration
@EnableMBeanExport
public class AppConfig {
}
如果你更喜欢基于 XML 的配置,<context:mbean-export/> 元素具有相同的作用,如下列代码所示:
<context:mbean-export/>
如有必要,您可以提供对特定 MBean server 的引用,而 defaultDomain 属性(AnnotationMBeanExporter 的一个属性)则接受一个替代值,用于生成的 MBean ObjectName 的域。该值将替代前一节MetadataNamingStrategy中所述的完整限定包名,如下例所示:
@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain")
@Configuration
ContextConfiguration {
}
以下示例展示了前述基于注解的示例所对应的 XML 配置:
<context:mbean-export server="myMBeanServer" default-domain="myDomain"/>
不要在 bean 类中同时使用基于接口的 AOP 代理和 JMX 注解的自动检测。基于接口的代理会“隐藏”目标类,从而也会隐藏 JMX 管理资源相关的注解。因此,在这种情况下,您应使用基于目标类的代理(通过在 <aop:config/>、<tx:annotation-driven/> 等标签中设置 'proxy-target-class' 标志)。否则,您的 JMX Bean 可能在启动时被静默忽略。 |
4.4. 使用 JSR-160 连接器
对于远程访问,Spring JMX 模块在 FactoryBean 包中提供了两个 org.springframework.jmx.support 实现,用于创建服务器端和客户端连接器。
4.4.1. 服务器端连接器
要让 Spring JMX 创建、启动并暴露一个 JSR-160 JMXConnectorServer,您可以使用以下配置:
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"/>
默认情况下,ConnectorServerFactoryBean 会创建一个绑定到
JMXConnectorServer 的 service:jmx:jmxmp://localhost:9875。
因此,serverConnector bean 通过 JMXMP 协议在 localhost 的 9875 端口向客户端暴露本地的
MBeanServer。请注意,JMXMP 协议在 JSR 160 规范中被标记为可选。
目前,主流的开源 JMX 实现 MX4J 以及 JDK 自带的实现均不支持 JMXMP。
要指定另一个 URL 并将 JMXConnectorServer 自身注册到 MBeanServer,您可以分别使用 serviceUrl 和 ObjectName 属性,如下例所示:
<bean id="serverConnector"
class="org.springframework.jmx.support.ConnectorServerFactoryBean">
<property name="objectName" value="connector:name=rmi"/>
<property name="serviceUrl"
value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector"/>
</bean>
如果设置了 ObjectName 属性,Spring 会自动使用该 MBeanServer 将您的连接器注册到 ObjectName 中。以下示例展示了在创建 ConnectorServerFactoryBean 时可以传递给 JMXConnector 的完整参数集:
<bean id="serverConnector"
class="org.springframework.jmx.support.ConnectorServerFactoryBean">
<property name="objectName" value="connector:name=iiop"/>
<property name="serviceUrl"
value="service:jmx:iiop://localhost/jndi/iiop://localhost:900/myconnector"/>
<property name="threaded" value="true"/>
<property name="daemon" value="true"/>
<property name="environment">
<map>
<entry key="someKey" value="someValue"/>
</map>
</property>
</bean>
请注意,当你使用基于 RMI 的连接器时,需要先启动查找服务(tnameserv 或
rmiregistry),名称注册才能完成。如果你使用 Spring 通过 RMI 为你导出远程服务,Spring 已经创建了一个 RMI 注册表。否则,你可以通过以下配置片段轻松启动一个注册表:
<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
<property name="port" value="1099"/>
</bean>
4.4.2. 客户端连接器
要创建一个连接到启用了 JSR-160 的远程 MBeanServerConnection 的 MBeanServer,您可以使用 MBeanServerConnectionFactoryBean,如下例所示:
<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
<property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>
4.4.3. 通过 Hessian 或 SOAP 使用 JMX
JSR-160 允许对客户端与服务器之间的通信方式进行扩展。前面章节中展示的示例使用了 JSR-160 规范所要求的基于 RMI 的强制实现(IIOP 和 JRMP)以及(可选的)JMXMP。通过使用其他提供者或 JMX 实现(例如 MX4J),您可以利用诸如 SOAP 或 Hessian 等协议,通过简单的 HTTP、SSL 或其他协议进行通信,如下例所示:
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean">
<property name="objectName" value="connector:name=burlap"/>
<property name="serviceUrl" value="service:jmx:burlap://localhost:9874"/>
</bean>
在前面的示例中,我们使用了 MX4J 3.0.0。有关更多信息,请参阅官方 MX4J 文档。
4.5. 通过代理访问 MBean
Spring JMX 允许你创建代理,将调用重新路由到在本地或远程 MBeanServer 中注册的 MBean。这些代理为你提供了一个标准的 Java 接口,通过该接口你可以与你的 MBean 进行交互。以下代码展示了如何为运行在本地 MBeanServer 中的 MBean 配置一个代理:
<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
<property name="objectName" value="bean:name=testBean"/>
<property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/>
</bean>
在前面的示例中,您可以看到为注册在 ObjectName 为 bean:name=testBean 下的 MBean 创建了一个代理。该代理所实现的接口集合由 proxyInterfaces 属性控制,而这些接口上的方法和属性映射到 MBean 上的操作和属性所遵循的规则,与 InterfaceBasedMBeanInfoAssembler 所使用的规则相同。
MBeanProxyFactoryBean 可以为任何可通过 MBeanServerConnection 访问的 MBean 创建代理。默认情况下,会定位并使用本地的 MBeanServer,但您可以覆盖此行为,提供一个指向远程 MBeanServerConnection 的 MBeanServer,以支持指向远程 MBean 的代理:
<bean id="clientConnector"
class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
<property name="serviceUrl" value="service:jmx:rmi://remotehost:9875"/>
</bean>
<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
<property name="objectName" value="bean:name=testBean"/>
<property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/>
<property name="server" ref="clientConnector"/>
</bean>
在前面的示例中,我们创建了一个指向远程机器的 MBeanServerConnection,
该连接使用了 MBeanServerConnectionFactoryBean。然后,此 MBeanServerConnection
通过 MBeanProxyFactoryBean 属性传递给 server。所创建的代理会将所有调用
通过此 MBeanServer 转发到 MBeanServerConnection。
4.6. 通知
Spring 的 JMX 功能包含对 JMX 通知的全面支持。
4.6.1. 注册通知监听器
Spring 的 JMX 支持使得向任意数量的 MBean 注册任意数量的
NotificationListeners 变得非常简单(这包括由 Spring 的 MBeanExporter 导出的 MBean,以及通过其他机制注册的 MBean)。例如,考虑这样一种场景:每当目标 MBean 的某个属性发生变化时,用户都希望收到通知(通过 Notification)。以下示例会将通知输出到控制台:
package com.example;
import javax.management.AttributeChangeNotification;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
public class ConsoleLoggingNotificationListener
implements NotificationListener, NotificationFilter {
public void handleNotification(Notification notification, Object handback) {
System.out.println(notification);
System.out.println(handback);
}
public boolean isNotificationEnabled(Notification notification) {
return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
}
}
以下示例将ConsoleLoggingNotificationListener(在前面的示例中定义)添加到notificationListenerMappings中:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
<property name="notificationListenerMappings">
<map>
<entry key="bean:name=testBean1">
<bean class="com.example.ConsoleLoggingNotificationListener"/>
</entry>
</map>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
通过上述配置,每当目标 MBean(Notification)广播一条 JMX bean:name=testBean1 时,通过 ConsoleLoggingNotificationListener 属性注册为监听器的 notificationListenerMappings bean 就会收到通知。随后,ConsoleLoggingNotificationListener bean 可以根据该 Notification 执行其认为合适的任何操作。
您也可以直接使用 bean 名称作为导出的 bean 与监听器之间的关联,如下例所示:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
<property name="notificationListenerMappings">
<map>
<entry key="testBean">
<bean class="com.example.ConsoleLoggingNotificationListener"/>
</entry>
</map>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
如果你想为当前 NotificationListener 导出的所有 Bean 注册同一个 MBeanExporter 实例,可以在 * 属性映射中使用特殊的通配符(notificationListenerMappings)作为键,如下例所示:
<property name="notificationListenerMappings">
<map>
<entry key="*">
<bean class="com.example.ConsoleLoggingNotificationListener"/>
</entry>
</map>
</property>
如果您需要执行相反的操作(即,将多个不同的监听器注册到同一个 MBean),则必须使用 notificationListeners 列表属性(优先于 notificationListenerMappings 属性)。这次,我们不再为单个 MBean 配置一个 NotificationListener,而是配置多个 NotificationListenerBean 实例。NotificationListenerBean 封装了一个 NotificationListener 以及它要在 MBeanServer 中注册的 ObjectName(或 ObjectNames)。
NotificationListenerBean 还封装了其他一些属性,例如 NotificationFilter 和一个任意的手持返回对象,该对象可用于高级 JMX 通知场景。
使用 NotificationListenerBean 实例时的配置与前面介绍的内容并没有太大不同,如下例所示:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
<property name="notificationListeners">
<list>
<bean class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg>
<bean class="com.example.ConsoleLoggingNotificationListener"/>
</constructor-arg>
<property name="mappedObjectNames">
<list>
<value>bean:name=testBean1</value>
</list>
</property>
</bean>
</list>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
前面的示例等同于第一个通知示例。现在假设我们希望每次触发 Notification 时都能获得一个回传对象(handback object),并且希望通过提供一个 Notifications 来过滤掉无关的 NotificationFilter。以下示例实现了这些目标:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean1"/>
<entry key="bean:name=testBean2" value-ref="testBean2"/>
</map>
</property>
<property name="notificationListeners">
<list>
<bean class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg ref="customerNotificationListener"/>
<property name="mappedObjectNames">
<list>
<!-- handles notifications from two distinct MBeans -->
<value>bean:name=testBean1</value>
<value>bean:name=testBean2</value>
</list>
</property>
<property name="handback">
<bean class="java.lang.String">
<constructor-arg value="This could be anything..."/>
</bean>
</property>
<property name="notificationFilter" ref="customerNotificationListener"/>
</bean>
</list>
</property>
</bean>
<!-- implements both the NotificationListener and NotificationFilter interfaces -->
<bean id="customerNotificationListener" class="com.example.ConsoleLoggingNotificationListener"/>
<bean id="testBean1" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="testBean2" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="ANOTHER TEST"/>
<property name="age" value="200"/>
</bean>
</beans>
(关于回传对象(handback object)的完整讨论,以及NotificationFilter 的定义,请参见 JMX 规范(1.2 版)中题为“JMX 通知模型”的章节。)
4.6.2. 发布通知
Spring 不仅支持注册以接收Notifications,还支持发布Notifications。
本节仅适用于通过 MBeanExporter 暴露为 MBean 的 Spring 管理的 Bean。任何已有的用户自定义 MBean 应使用标准的 JMX API 来发布通知。 |
Spring JMX 通知发布支持中的关键接口是 NotificationPublisher 接口(定义在 org.springframework.jmx.export.notification 包中)。任何将通过 MBeanExporter 实例导出为 MBean 的 Bean 都可以实现相关的 NotificationPublisherAware 接口,以获得对 NotificationPublisher 实例的访问权限。NotificationPublisherAware 接口通过一个简单的 setter 方法向实现该接口的 Bean 提供一个 NotificationPublisher 实例,该 Bean 随后即可使用此实例来发布 Notifications。
正如
NotificationPublisher
接口的 Javadoc 中所述,通过 NotificationPublisher 机制发布事件的管理 Bean 不负责通知监听器的状态管理。
Spring 的 JMX 支持负责处理所有 JMX 基础设施问题。
作为应用程序开发人员,您只需实现
NotificationPublisherAware 接口,并使用提供的 NotificationPublisher 实例开始发布事件。请注意,NotificationPublisher
是在管理 Bean 向 MBeanServer 注册之后设置的。
使用 NotificationPublisher 实例非常简单。您需要创建一个 JMX
Notification 实例(或某个合适的 Notification 子类的实例),
用与要发布事件相关的数据填充该通知,然后在 sendNotification(Notification) 实例上调用
NotificationPublisher 方法,并传入该 Notification。
在以下示例中,JmxTestBean 的导出实例每次调用 NotificationEvent 操作时都会发布一个 add(int, int):
package org.springframework.jmx;
import org.springframework.jmx.export.notification.NotificationPublisherAware;
import org.springframework.jmx.export.notification.NotificationPublisher;
import javax.management.Notification;
public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware {
private String name;
private int age;
private boolean isSuperman;
private NotificationPublisher publisher;
// other getters and setters omitted for clarity
public int add(int x, int y) {
int answer = x + y;
this.publisher.sendNotification(new Notification("add", this, 0));
return answer;
}
public void dontExposeMe() {
throw new RuntimeException();
}
public void setNotificationPublisher(NotificationPublisher notificationPublisher) {
this.publisher = notificationPublisher;
}
}
NotificationPublisher 接口以及使其正常工作的相关机制,是 Spring 对 JMX 支持中较为出色的功能之一。然而,这也带来了将您的类同时耦合到 Spring 和 JMX 的代价。一如既往,这里的建议是保持务实:如果您需要 NotificationPublisher 提供的功能,并且可以接受与 Spring 和 JMX 的耦合,那么就放心使用它。
4.7. 更多资源
本节包含有关 JMX 的更多资源链接:
-
Oracle 的 JMX 主页。
-
JMX 规范(JSR-000003)。
-
JMX 远程 API 规范(JSR-000160)。
-
MX4J 主页。(MX4J 是各种 JMX 规范的开源实现。)
5. JCA CCI
Java EE 提供了一个用于标准化访问企业信息系统(EIS)的规范:JCA(Java EE 连接器架构)。该规范分为两个不同的部分:
-
连接器提供者必须实现的 SPI(服务提供者接口)。这些接口构成了一个资源适配器,可部署在 Java EE 应用服务器上。在此场景中,服务器负责管理连接池、事务和安全性(托管模式)。应用服务器还负责管理配置,而该配置位于客户端应用程序之外。连接器也可以在没有应用服务器的情况下使用。此时,应用程序必须直接对其进行配置(非托管模式)。
-
CCI(通用客户端接口),应用程序可使用该接口与连接器进行交互,从而与企业信息系统(EIS)通信。同时,还提供了用于本地事务界定的 API。
Spring CCI 支持的目标是提供一些类,以便以典型的 Spring 风格访问 CCI 连接器,并利用 Spring 框架通用的资源和事务管理功能。
| 连接器的客户端并不总是使用CCI。某些连接器会暴露其自身的API,并提供一个JCA资源适配器,以利用Java EE容器的系统契约(如连接池、全局事务和安全性)。Spring并未对这类特定于连接器的API提供特殊支持。 |
5.1. 配置 CCI
本节介绍如何配置通用客户端接口(CCI)。内容包括以下主题:
5.1.1. 连接器配置
用于 JCA CCI 的基础资源是 ConnectionFactory 接口。您所使用的连接器必须提供该接口的实现。
要使用您的连接器,您可以将其部署到应用服务器上,并从服务器的 JNDI 环境中获取 ConnectionFactory(托管模式)。该连接器必须打包为 RAR 文件(资源适配器归档文件),并包含一个 ra.xml 文件,用于描述其部署特性。资源的实际名称在部署时指定。要在 Spring 中访问它,您可以使用 Spring 的 JndiObjectFactoryBean 或 <jee:jndi-lookup> 通过 JNDI 名称获取该工厂。
使用连接器的另一种方式是将其嵌入到您的应用程序中(非托管模式),而不使用应用服务器来部署和配置它。Spring 提供了一种通过名为 FactoryBean 的 LocalConnectionFactoryBean 实现,将连接器配置为一个 bean 的可能性。通过这种方式,您只需将连接器库放入类路径中即可(无需 RAR 文件,也无需 ra.xml 描述符)。如有必要,该库必须从连接器的 RAR 文件中提取出来。
一旦你获得了 ConnectionFactory 实例,就可以将其注入到你的组件中。这些组件既可以直接使用原生的 CCI API 编写,也可以使用 Spring 提供的 CCI 访问支持类(例如 CciTemplate)。
| 当你在非托管模式下使用连接器时,无法使用全局事务,因为该资源永远不会注册(enlisted)或注销(delisted)到当前线程的全局事务中。该资源无法感知任何可能正在运行的全局 Java EE 事务。 |
5.1.2. ConnectionFactorySpring 中的配置
要连接到企业信息系统(EIS),您需要从应用服务器获取一个ConnectionFactory(如果您处于托管模式),或者直接从 Spring 获取(如果您处于非托管模式)。
在托管模式下,您可以从 JNDI 访问一个 ConnectionFactory。其属性在应用服务器中进行配置。以下示例展示了如何实现这一点:
<jee:jndi-lookup id="eciConnectionFactory" jndi-name="eis/cicseci"/>
在非托管模式下,您必须将希望使用的 ConnectionFactory 配置为 Spring 中的一个 JavaBean。LocalConnectionFactoryBean 类提供了这种配置方式,它接收您连接器的 ManagedConnectionFactory 实现,并暴露应用级别的 CCI ConnectionFactory。以下示例展示了如何进行此类配置:
<bean id="eciManagedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
<property name="serverName" value="TXSERIES"/>
<property name="connectionURL" value="tcp://localhost/"/>
<property name="portNumber" value="2006"/>
</bean>
<bean id="eciConnectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="eciManagedConnectionFactory"/>
</bean>
您无法直接实例化一个特定的ConnectionFactory。您需要通过连接器对应的ManagedConnectionFactory接口实现来完成。该接口是JCA SPI规范的一部分。 |
5.1.3. 配置 CCI 连接
JCA CCI 允许你通过使用连接器的 ConnectionSpec 实现来配置与 EIS 的连接。要配置其属性,你需要使用专用适配器 ConnectionSpecConnectionFactoryAdapter 包装目标连接工厂。你可以通过 ConnectionSpec 属性(作为内部 bean)来配置专用的 connectionSpec。
此属性不是必需的,因为 CCI ConnectionFactory 接口定义了两种不同的方法来获取 CCI 连接。您通常可以在应用服务器中(在托管模式下)或在相应的本地 ConnectionSpec 实现上配置部分 ManagedConnectionFactory 属性。以下代码清单展示了 ConnectionFactory 接口定义中的相关部分:
public interface ConnectionFactory implements Serializable, Referenceable {
...
Connection getConnection() throws ResourceException;
Connection getConnection(ConnectionSpec connectionSpec) throws ResourceException;
...
}
Spring 提供了一个 ConnectionSpecConnectionFactoryAdapter,允许你为给定工厂上的所有操作指定要使用的 ConnectionSpec 实例。如果适配器的 connectionSpec 属性被指定,则适配器将使用带有 getConnection 参数的 ConnectionSpec 变体;否则,适配器将使用不带该参数的变体。
以下示例展示了如何配置一个 ConnectionSpecConnectionFactoryAdapter:
<bean id="managedConnectionFactory"
class="com.sun.connector.cciblackbox.CciLocalTxManagedConnectionFactory">
<property name="connectionURL" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="driverName" value="org.hsqldb.jdbcDriver"/>
</bean>
<bean id="targetConnectionFactory"
class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>
<bean id="connectionFactory"
class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
<property name="targetConnectionFactory" ref="targetConnectionFactory"/>
<property name="connectionSpec">
<bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
<property name="user" value="sa"/>
<property name="password" value=""/>
</bean>
</property>
</bean>
5.1.4. 使用单个 CCI 连接
如果你想使用单一的CCI连接,Spring提供了一个额外的ConnectionFactory适配器来管理该连接。SingleConnectionFactory适配器类会延迟打开一个单一连接,并在应用程序关闭、该Bean被销毁时关闭该连接。该类暴露了特殊的Connection代理,这些代理会相应地表现行为,并共享同一个底层物理连接。以下示例展示了如何使用SingleConnectionFactory适配器类:
<bean id="eciManagedConnectionFactory"
class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
<property name="serverName" value="TEST"/>
<property name="connectionURL" value="tcp://localhost/"/>
<property name="portNumber" value="2006"/>
</bean>
<bean id="targetEciConnectionFactory"
class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="eciManagedConnectionFactory"/>
</bean>
<bean id="eciConnectionFactory"
class="org.springframework.jca.cci.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="targetEciConnectionFactory"/>
</bean>
此 ConnectionFactory 适配器不能直接使用 ConnectionSpec 进行配置。
如果您需要为特定的 ConnectionSpecConnectionFactoryAdapter 使用单一连接,
可以使用一个中间的 SingleConnectionFactory,
由 ConnectionSpec 与之通信。 |
5.2. 使用 Spring 的 CCI 访问支持
本节介绍如何使用 Spring 对 CCI 的支持来实现各种目的。 其中包括以下主题:
5.2.1. 记录转换
Spring 对 JCA CCI 支持的目标之一是提供便捷的功能来操作 CCI 记录。您可以指定创建记录和从记录中提取数据的策略,以便与 Spring 的 CciTemplate 配合使用。本节所述的接口用于配置输入和输出记录的使用策略,如果您不想在应用程序中直接处理记录的话。
要创建一个输入的 Record,您可以使用 RecordCreator 接口的专用实现。以下代码清单展示了 RecordCreator 接口的定义:
public interface RecordCreator {
Record createRecord(RecordFactory recordFactory) throws ResourceException, DataAccessException;
}
createRecord(..) 方法接收一个 RecordFactory 实例作为参数,该实例对应于所使用的 RecordFactory 的 ConnectionFactory。
您可以使用此引用来创建 IndexedRecord 或 MappedRecord 实例。以下示例展示了如何使用 RecordCreator 接口以及索引记录(indexed records)或映射记录(mapped records):
public class MyRecordCreator implements RecordCreator {
public Record createRecord(RecordFactory recordFactory) throws ResourceException {
IndexedRecord input = recordFactory.createIndexedRecord("input");
input.add(new Integer(id));
return input;
}
}
您可以使用一个输出 Record 来接收来自企业信息系统(EIS)返回的数据。因此,您可以将 RecordExtractor 接口的一个具体实现传递给 Spring 的 CciTemplate,以从输出 Record 中提取数据。以下代码清单展示了 RecordExtractor 接口的定义:
public interface RecordExtractor {
Object extractData(Record record) throws ResourceException, SQLException, DataAccessException;
}
以下示例展示了如何使用 RecordExtractor 接口:
public class MyRecordExtractor implements RecordExtractor {
public Object extractData(Record record) throws ResourceException {
CommAreaRecord commAreaRecord = (CommAreaRecord) record;
String str = new String(commAreaRecord.toByteArray());
String field1 = string.substring(0,6);
String field2 = string.substring(6,1);
return new OutputObject(Long.parseLong(field1), field2);
}
}
5.2.2. 使用CciTemplate
CciTemplate 是核心 CCI 支持包(org.springframework.jca.cci.core)中的中心类。它简化了 CCI 的使用,因为它负责资源的创建和释放,有助于避免常见错误,例如忘记始终关闭连接。它管理连接和交互对象的生命周期,使应用程序代码能够专注于从应用数据生成输入记录,以及从输出记录中提取应用数据。
JCA CCI 规范定义了两种不同的方法来调用企业信息系统(EIS)上的操作。
CCI Interaction 接口提供了两个 execute 方法签名,如下列代码所示:
public interface javax.resource.cci.Interaction {
...
boolean execute(InteractionSpec spec, Record input, Record output) throws ResourceException;
Record execute(InteractionSpec spec, Record input) throws ResourceException;
...
}
根据所调用的模板方法,CciTemplate 知道应在交互(interaction)上调用哪个 execute 方法。无论如何,必须提供一个正确初始化的 InteractionSpec 实例。
你可以通过两种方式使用 CciTemplate.execute(..):
-
使用直接的
Record参数。在这种情况下,您需要传入 CCI 输入记录,返回的对象则是相应的 CCI 输出记录。 -
对于应用程序对象,可通过记录映射(record mapping)的方式。在这种情况下,您需要提供相应的
RecordCreator和RecordExtractor实例。
在第一种方法中,会使用模板的以下方法(这些方法直接对应于Interaction接口中的方法):
public class CciTemplate implements CciOperations {
public Record execute(InteractionSpec spec, Record inputRecord)
throws DataAccessException { ... }
public void execute(InteractionSpec spec, Record inputRecord, Record outputRecord)
throws DataAccessException { ... }
}
采用第二种方法时,我们需要将记录创建策略和记录提取策略作为参数进行指定。所使用的接口即为上一节关于记录转换中描述的那些接口。以下代码清单展示了相应的CciTemplate方法:
public class CciTemplate implements CciOperations {
public Record execute(InteractionSpec spec,
RecordCreator inputCreator) throws DataAccessException {
// ...
}
public Object execute(InteractionSpec spec, Record inputRecord,
RecordExtractor outputExtractor) throws DataAccessException {
// ...
}
public Object execute(InteractionSpec spec, RecordCreator creator,
RecordExtractor extractor) throws DataAccessException {
// ...
}
}
除非在模板上设置了 outputRecordCreator 属性(参见下文),否则每个方法都会调用 CCI execute 的相应 Interaction 方法,并传入两个参数:InteractionSpec 和一个输入 Record。该方法会返回一个输出 Record 作为其返回值。
CciTemplate 还提供了方法,用于在 RecordCreator 实现之外创建 IndexRecord 和 MappedRecord,这是通过其 createIndexRecord(..) 和 createMappedRecord(..) 方法实现的。您可以在 DAO 实现中使用此功能来创建 Record 实例,并将其传递给相应的 CciTemplate.execute(..) 方法。以下列表展示了 CciTemplate 接口的定义:
public class CciTemplate implements CciOperations {
public IndexedRecord createIndexedRecord(String name) throws DataAccessException { ... }
public MappedRecord createMappedRecord(String name) throws DataAccessException { ... }
}
5.2.3. 使用 DAO 支持
Spring 的 CCI 支持提供了一个用于 DAO 的抽象类,支持注入 ConnectionFactory 或 CciTemplate 实例。该类的名称为 CciDaoSupport。它提供了简单的 setConnectionFactory 和 setCciTemplate 方法。
在内部,此类会为传入的 CciTemplate 创建一个 ConnectionFactory 实例,并将其暴露给子类中的具体数据访问实现。
以下示例展示了如何使用 CciDaoSupport:
public abstract class CciDaoSupport {
public void setConnectionFactory(ConnectionFactory connectionFactory) {
// ...
}
public ConnectionFactory getConnectionFactory() {
// ...
}
public void setCciTemplate(CciTemplate cciTemplate) {
// ...
}
public CciTemplate getCciTemplate() {
// ...
}
}
5.2.4. 自动输出记录生成
如果你使用的连接器仅支持以输入记录和输出记录作为参数的 Interaction.execute(..) 方法(即,它要求传入期望的输出记录,而不是返回一个合适的输出记录),你可以设置 outputRecordCreator 的 CciTemplate 属性,以自动创建一个输出记录。当接收到响应时,该记录将由 JCA 连接器填充,然后返回给模板的调用者。
此属性持有 RecordCreator 接口 的一个实现,
用于该目的。您必须在 CciTemplate 上直接指定 outputRecordCreator 属性。
以下示例展示了如何操作:
cciTemplate.setOutputRecordCreator(new EciOutputRecordCreator());
或者(我们推荐采用这种方式),在 Spring 配置中,如果将 CciTemplate 配置为一个专用的 bean 实例,你可以按如下方式定义 bean:
<bean id="eciOutputRecordCreator" class="eci.EciOutputRecordCreator"/>
<bean id="cciTemplate" class="org.springframework.jca.cci.core.CciTemplate">
<property name="connectionFactory" ref="eciConnectionFactory"/>
<property name="outputRecordCreator" ref="eciOutputRecordCreator"/>
</bean>
由于 CciTemplate 类是线程安全的,通常将其配置为共享实例。 |
5.2.5. CciTemplate Interaction摘要
下表总结了 CciTemplate 类的机制以及在 CCI Interaction 接口上对应调用的方法:
CciTemplate 方法签名 |
CciTemplate outputRecordCreator 属性 |
在 CCI Interaction 上调用的 execute 方法 |
|---|---|---|
|
未设置 |
|
|
集合 |
|
void execute(InteractionSpec, Record, Record) |
未设置 |
void execute(InteractionSpec, Record, Record) |
|
集合 |
|
|
未设置 |
|
|
集合 |
|
|
未设置 |
|
|
集合 |
|
|
未设置 |
|
|
集合 |
|
5.2.6. 直接使用 CCI 连接和交互
CciTemplate 还允许您像使用 JdbcTemplate 和 JmsTemplate 一样,直接操作 CCI 连接和交互。例如,当您希望在 CCI 连接或交互上执行多个操作时,这非常有用。
ConnectionCallback 接口提供一个 CCI Connection 作为参数(用于在其上执行自定义操作),以及创建该 ConnectionFactory 所用的 CCI Connection。后者可能很有用(例如,用于获取关联的 RecordFactory 实例并创建索引记录或映射记录)。
以下代码清单展示了 ConnectionCallback 接口的定义:
public interface ConnectionCallback {
Object doInConnection(Connection connection, ConnectionFactory connectionFactory)
throws ResourceException, SQLException, DataAccessException;
}
InteractionCallback 接口提供了 CCI Interaction(用于在其上执行自定义操作)以及相应的 CCI ConnectionFactory。
以下代码清单展示了 InteractionCallback 接口的定义:
public interface InteractionCallback {
Object doInInteraction(Interaction interaction, ConnectionFactory connectionFactory)
throws ResourceException, SQLException, DataAccessException;
}
InteractionSpec 对象可以在多个模板调用之间共享,也可以在每次回调方法中重新创建。这完全取决于 DAO 的实现方式。 |
5.2.7. 示例:CciTemplate用法
在本节中,我们展示了如何使用 CciTemplate 通过 IBM CICS ECI 连接器以 ECI 模式访问 CICS。
首先,我们必须对 CCI InteractionSpec 进行一些初始化设置,以指定要访问的 CICS 程序以及与其交互的方式,如下例所示:
ECIInteractionSpec interactionSpec = new ECIInteractionSpec();
interactionSpec.setFunctionName("MYPROG");
interactionSpec.setInteractionVerb(ECIInteractionSpec.SYNC_SEND_RECEIVE);
然后,程序可以通过 Spring 的模板使用 CCI,并指定自定义对象与 CCI Records 之间的映射关系,如下例所示:
public class MyDaoImpl extends CciDaoSupport implements MyDao {
public OutputObject getData(InputObject input) {
ECIInteractionSpec interactionSpec = ...;
OutputObject output = (ObjectOutput) getCciTemplate().execute(interactionSpec,
new RecordCreator() {
public Record createRecord(RecordFactory recordFactory) throws ResourceException {
return new CommAreaRecord(input.toString().getBytes());
}
},
new RecordExtractor() {
public Object extractData(Record record) throws ResourceException {
CommAreaRecord commAreaRecord = (CommAreaRecord)record;
String str = new String(commAreaRecord.toByteArray());
String field1 = string.substring(0,6);
String field2 = string.substring(6,1);
return new OutputObject(Long.parseLong(field1), field2);
}
});
return output;
}
}
如前所述,您可以使用回调直接处理CCI连接或交互。以下示例展示了如何实现这一点:
public class MyDaoImpl extends CciDaoSupport implements MyDao {
public OutputObject getData(InputObject input) {
ObjectOutput output = (ObjectOutput) getCciTemplate().execute(
new ConnectionCallback() {
public Object doInConnection(Connection connection,
ConnectionFactory factory) throws ResourceException {
// do something...
}
});
}
return output;
}
}
使用 ConnectionCallback 时,所使用的 Connection 由 CciTemplate 管理并关闭,但回调实现必须管理在该连接上创建的任何交互。 |
为了实现更具体的回调,你可以实现一个 InteractionCallback。如果这样做,传入的 Interaction 将由 CciTemplate 负责管理并关闭。以下示例展示了如何实现这一点:
public class MyDaoImpl extends CciDaoSupport implements MyDao {
public String getData(String input) {
ECIInteractionSpec interactionSpec = ...;
String output = (String) getCciTemplate().execute(interactionSpec,
new InteractionCallback() {
public Object doInInteraction(Interaction interaction,
ConnectionFactory factory) throws ResourceException {
Record input = new CommAreaRecord(inputString.getBytes());
Record output = new CommAreaRecord();
interaction.execute(holder.getInteractionSpec(), input, output);
return new String(output.toByteArray());
}
});
return output;
}
}
对于前面的示例,所涉及的 Spring Bean 的相应配置在非托管模式下可能类似于以下示例:
<bean id="managedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
<property name="serverName" value="TXSERIES"/>
<property name="connectionURL" value="local:"/>
<property name="userName" value="CICSUSER"/>
<property name="password" value="CICS"/>
</bean>
<bean id="connectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>
<bean id="component" class="mypackage.MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
在托管模式下(即在 Java EE 环境中),配置可能类似于以下示例:
<jee:jndi-lookup id="connectionFactory" jndi-name="eis/cicseci"/>
<bean id="component" class="MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
5.3. 将 CCI 访问建模为操作对象
org.springframework.jca.cci.object 包包含一些支持类,允许您以另一种方式访问 EIS:通过可重用的操作对象,类似于 Spring 的 JDBC 操作对象(参见数据访问章节中的 JDBC 部分)。这通常会封装 CCI API。应用程序级别的输入对象会被传递给操作对象,以便它能够构造输入记录,然后将接收到的记录数据转换为应用程序级别的输出对象并返回。
此方法在内部基于 CciTemplate 类以及 RecordCreator 或 RecordExtractor 接口,复用了 Spring 核心 CCI 支持的机制。 |
5.3.1. 使用MappingRecordOperation
MappingRecordOperation 本质上执行与 CciTemplate 相同的工作,但将一个特定的、预先配置好的操作表示为一个对象。它提供了两个模板方法,用于指定如何将输入对象转换为输入记录,以及如何将输出记录转换为输出对象(记录映射):
-
createInputRecord(..):用于指定如何将输入对象转换为输入Record -
extractOutputData(..):用于指定如何从输出Record中提取输出对象
以下列表展示了这些方法的签名:
public abstract class MappingRecordOperation extends EisOperation {
...
protected abstract Record createInputRecord(RecordFactory recordFactory,
Object inputObject) throws ResourceException, DataAccessException {
// ...
}
protected abstract Object extractOutputData(Record outputRecord)
throws ResourceException, SQLException, DataAccessException {
// ...
}
...
}
此后,要执行一个 EIS 操作,您需要使用一个 execute 方法,传入一个应用级别的输入对象,并接收一个应用级别的输出对象作为结果。以下示例展示了如何实现这一点:
public abstract class MappingRecordOperation extends EisOperation {
...
public Object execute(Object inputObject) throws DataAccessException {
}
...
}
与 CciTemplate 类不同,此 execute(..) 方法不以 InteractionSpec 作为参数。相反,InteractionSpec 是该操作的全局配置。您必须使用以下构造函数来实例化一个带有特定 InteractionSpec 的操作对象。以下示例展示了如何实现这一点:
InteractionSpec spec = ...;
MyMappingRecordOperation eisOperation = new MyMappingRecordOperation(getConnectionFactory(), spec);
...
5.3.2. 使用MappingCommAreaOperation
某些连接器使用基于 COMMAREA 的记录,COMMAREA 表示一个字节数组,其中包含要发送给企业信息系统(EIS)的参数以及由 EIS 返回的数据。Spring 提供了一个特殊的操作类,用于直接处理 COMMAREA,而不是处理记录。MappingCommAreaOperation 类继承自 MappingRecordOperation 类,以提供这种专门的 COMMAREA 支持。它隐式地使用 CommAreaRecord 类作为输入和输出记录类型,并提供了两个新方法:一个将输入对象转换为输入 COMMAREA,另一个将输出 COMMAREA 转换为输出对象。以下代码清单展示了相关的方法签名:
public abstract class MappingCommAreaOperation extends MappingRecordOperation {
...
protected abstract byte[] objectToBytes(Object inObject)
throws IOException, DataAccessException;
protected abstract Object bytesToObject(byte[] bytes)
throws IOException, DataAccessException;
...
}
5.3.3. 自动输出记录生成
由于每个 MappingRecordOperation 子类在内部都基于 CciTemplate,因此可以像使用 CciTemplate 一样自动生成功能输出记录。
每个操作对象都提供了一个对应的 setOutputRecordCreator(..) 方法。
更多信息,请参见 自动输出记录生成。
5.3.4. 总结
操作对象方法以与CciTemplate类相同的方式使用记录。
MappingRecordOperation 方法签名 |
MappingRecordOperation outputRecordCreator 属性 |
在 CCI Interaction 上调用的 execute 方法 |
|---|---|---|
|
未设置 |
|
|
集合 |
|
5.3.5. 示例MappingRecordOperation用法
在本节中,我们将展示如何使用 MappingRecordOperation 通过 Blackbox CCI 连接器访问数据库。
| 此连接器的原始版本由 Java EE SDK(1.3 版)提供, 该 SDK 可从 Oracle 获取。 |
首先,您必须对 CCI 的 InteractionSpec 进行一些初始化,以指定要执行的 SQL 请求。在下面的示例中,我们直接定义了将请求参数转换为 CCI 记录的方式,以及将 CCI 结果记录转换为 Person 类实例的方式:
public class PersonMappingOperation extends MappingRecordOperation {
public PersonMappingOperation(ConnectionFactory connectionFactory) {
setConnectionFactory(connectionFactory);
CciInteractionSpec interactionSpec = new CciConnectionSpec();
interactionSpec.setSql("select * from person where person_id=?");
setInteractionSpec(interactionSpec);
}
protected Record createInputRecord(RecordFactory recordFactory,
Object inputObject) throws ResourceException {
Integer id = (Integer) inputObject;
IndexedRecord input = recordFactory.createIndexedRecord("input");
input.add(new Integer(id));
return input;
}
protected Object extractOutputData(Record outputRecord)
throws ResourceException, SQLException {
ResultSet rs = (ResultSet) outputRecord;
Person person = null;
if (rs.next()) {
Person person = new Person();
person.setId(rs.getInt("person_id"));
person.setLastName(rs.getString("person_last_name"));
person.setFirstName(rs.getString("person_first_name"));
}
return person;
}
}
然后应用程序可以执行该操作对象,并将人员标识符作为参数传入。请注意,由于该操作对象是线程安全的,你可以将其配置为一个共享实例。以下代码演示了如何以人员标识符作为参数来执行该操作对象:
public class MyDaoImpl extends CciDaoSupport implements MyDao {
public Person getPerson(int id) {
PersonMappingOperation query = new PersonMappingOperation(getConnectionFactory());
Person person = (Person) query.execute(new Integer(id));
return person;
}
}
在非托管模式下,Spring Bean 的相应配置可能如下所示:
<bean id="managedConnectionFactory"
class="com.sun.connector.cciblackbox.CciLocalTxManagedConnectionFactory">
<property name="connectionURL" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="driverName" value="org.hsqldb.jdbcDriver"/>
</bean>
<bean id="targetConnectionFactory"
class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>
<bean id="connectionFactory"
class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
<property name="targetConnectionFactory" ref="targetConnectionFactory"/>
<property name="connectionSpec">
<bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
<property name="user" value="sa"/>
<property name="password" value=""/>
</bean>
</property>
</bean>
<bean id="component" class="MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
在托管模式下(即在 Java EE 环境中),配置可能如下所示:
<jee:jndi-lookup id="targetConnectionFactory" jndi-name="eis/blackbox"/>
<bean id="connectionFactory"
class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
<property name="targetConnectionFactory" ref="targetConnectionFactory"/>
<property name="connectionSpec">
<bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
<property name="user" value="sa"/>
<property name="password" value=""/>
</bean>
</property>
</bean>
<bean id="component" class="MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
5.3.6. 示例:MappingCommAreaOperation用法
在本节中,我们将展示如何使用 MappingCommAreaOperation 通过 IBM CICS ECI 连接器以 ECI 模式访问 CICS。
首先,我们需要初始化 CCI InteractionSpec,以指定要访问哪个 CICS 程序以及如何与之交互,如下例所示:
public abstract class EciMappingOperation extends MappingCommAreaOperation {
public EciMappingOperation(ConnectionFactory connectionFactory, String programName) {
setConnectionFactory(connectionFactory);
ECIInteractionSpec interactionSpec = new ECIInteractionSpec(),
interactionSpec.setFunctionName(programName);
interactionSpec.setInteractionVerb(ECIInteractionSpec.SYNC_SEND_RECEIVE);
interactionSpec.setCommareaLength(30);
setInteractionSpec(interactionSpec);
setOutputRecordCreator(new EciOutputRecordCreator());
}
private static class EciOutputRecordCreator implements RecordCreator {
public Record createRecord(RecordFactory recordFactory) throws ResourceException {
return new CommAreaRecord();
}
}
}
然后,我们可以继承抽象类 EciMappingOperation,以指定自定义对象与 Records 之间的映射关系,如下例所示:
public class MyDaoImpl extends CciDaoSupport implements MyDao {
public OutputObject getData(Integer id) {
EciMappingOperation query = new EciMappingOperation(getConnectionFactory(), "MYPROG") {
protected abstract byte[] objectToBytes(Object inObject) throws IOException {
Integer id = (Integer) inObject;
return String.valueOf(id);
}
protected abstract Object bytesToObject(byte[] bytes) throws IOException;
String str = new String(bytes);
String field1 = str.substring(0,6);
String field2 = str.substring(6,1);
String field3 = str.substring(7,1);
return new OutputObject(field1, field2, field3);
}
});
return (OutputObject) query.execute(new Integer(id));
}
}
在非托管模式下,Spring Bean 的相应配置可能如下所示:
<bean id="managedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
<property name="serverName" value="TXSERIES"/>
<property name="connectionURL" value="local:"/>
<property name="userName" value="CICSUSER"/>
<property name="password" value="CICS"/>
</bean>
<bean id="connectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
<property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>
<bean id="component" class="MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
在托管模式下(即在 Java EE 环境中),配置可能如下所示:
<jee:jndi-lookup id="connectionFactory" jndi-name="eis/cicseci"/>
<bean id="component" class="MyDaoImpl">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
5.4. 事务
JCA 为资源适配器指定了多个级别的事务支持。您的资源适配器所支持的事务类型在其 ra.xml 文件中指定。
基本上有三种选项:无事务(例如,CICS EPI 连接器)、本地事务(例如,CICS ECI 连接器)和全局事务(例如,IMS 连接器)。以下示例配置了全局事务选项:
<connector>
<resourceadapter>
<!-- <transaction-support>NoTransaction</transaction-support> -->
<!-- <transaction-support>LocalTransaction</transaction-support> -->
<transaction-support>XATransaction</transaction-support>
<resourceadapter>
<connector>
对于全局事务,您可以使用 Spring 的通用事务基础设施来界定事务,后端使用 JtaTransactionManager(底层委托给 Java EE 服务器的分布式事务协调器)。
对于在单个 CCI ConnectionFactory 上的本地事务,Spring 提供了一种专门针对 CCI 的事务管理策略,类似于 JDBC 中的 DataSourceTransactionManager。CCI API 定义了一个本地事务对象以及相应的本地事务边界划分方法。Spring 的 CciLocalTransactionManager 以完全符合 Spring 通用 PlatformTransactionManager 抽象的方式执行此类本地 CCI 事务。以下示例配置了一个 CciLocalTransactionManager:
<jee:jndi-lookup id="eciConnectionFactory" jndi-name="eis/cicseci"/>
<bean id="eciTransactionManager"
class="org.springframework.jca.cci.connection.CciLocalTransactionManager">
<property name="connectionFactory" ref="eciConnectionFactory"/>
</bean>
您可以将这两种事务策略与 Spring 的任意事务界定功能一起使用,无论是声明式还是编程式。这是 Spring 通用的 PlatformTransactionManager 抽象所带来的结果,该抽象将事务界定与实际的执行策略解耦。您可以根据需要在 JtaTransactionManager 和 CciLocalTransactionManager 之间切换,同时保持原有的事务界定方式不变。
有关 Spring 事务功能的更多信息,请参阅 事务管理。
6. 电子邮件
本节介绍如何使用 Spring Framework 发送电子邮件。
Spring 框架提供了一个用于发送电子邮件的实用工具库,该库屏蔽了底层邮件系统的具体细节,并代表客户端负责底层资源的处理。
org.springframework.mail 包是 Spring 框架电子邮件支持的根级包。发送电子邮件的核心接口是 MailSender 接口。from 类是一个简单的值对象,用于封装简单邮件的属性,例如 to 和 SimpleMailMessage(以及其他许多属性)。该包还包含一个受检异常的层次结构,对底层邮件系统异常提供了更高层次的抽象,其根异常为 MailException。有关丰富的邮件异常层次结构的更多信息,请参阅 javadoc。
org.springframework.mail.javamail.JavaMailSender 接口在 MailSender 接口(它继承自该接口)的基础上增加了专门的 JavaMail 功能,例如对 MIME 消息的支持。JavaMailSender 还提供了一个名为 org.springframework.mail.javamail.MimeMessagePreparator 的回调接口,用于准备 MimeMessage。
6.1. 用法
假设我们有一个名为 OrderManager 的业务接口,如下例所示:
public interface OrderManager {
void placeOrder(Order order);
}
进一步假设我们有一个需求,即需要生成一封包含订单号的电子邮件,并将其发送给下相关订单的客户。
6.1.1. 基础MailSender和SimpleMailMessage用法
以下示例展示了如何在用户下单时使用 MailSender 和 SimpleMailMessage 发送电子邮件:
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
public class SimpleOrderManager implements OrderManager {
private MailSender mailSender;
private SimpleMailMessage templateMessage;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void setTemplateMessage(SimpleMailMessage templateMessage) {
this.templateMessage = templateMessage;
}
public void placeOrder(Order order) {
// Do the business calculations...
// Call the collaborators to persist the order...
// Create a thread safe "copy" of the template message and customize it
SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
msg.setTo(order.getCustomer().getEmailAddress());
msg.setText(
"Dear " + order.getCustomer().getFirstName()
+ order.getCustomer().getLastName()
+ ", thank you for placing order. Your order number is "
+ order.getOrderNumber());
try{
this.mailSender.send(msg);
}
catch (MailException ex) {
// simply log it and go on...
System.err.println(ex.getMessage());
}
}
}
以下示例展示了上述代码的 bean 定义:
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="mail.mycompany.example"/>
</bean>
<!-- this is a template message that we can pre-load with default state -->
<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
<property name="from" value="[email protected]"/>
<property name="subject" value="Your order"/>
</bean>
<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager">
<property name="mailSender" ref="mailSender"/>
<property name="templateMessage" ref="templateMessage"/>
</bean>
6.1.2. 使用JavaMailSender和MimeMessagePreparator
本节描述了另一个使用 OrderManager 回调接口的 MimeMessagePreparator 实现。在以下示例中,mailSender 属性的类型为 JavaMailSender,以便我们可以使用 JavaMail 的 MimeMessage 类:
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;
public class SimpleOrderManager implements OrderManager {
private JavaMailSender mailSender;
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void placeOrder(final Order order) {
// Do the business calculations...
// Call the collaborators to persist the order...
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
mimeMessage.setRecipient(Message.RecipientType.TO,
new InternetAddress(order.getCustomer().getEmailAddress()));
mimeMessage.setFrom(new InternetAddress("[email protected]"));
mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " +
order.getCustomer().getLastName() + ", thanks for your order. " +
"Your order number is " + order.getOrderNumber() + ".");
}
};
try {
this.mailSender.send(preparator);
}
catch (MailException ex) {
// simply log it and go on...
System.err.println(ex.getMessage());
}
}
}
邮件代码是一个横切关注点,非常适合重构为一个自定义的 Spring AOP 切面,然后在 OrderManager 目标对象的适当连接点上执行。 |
Spring 框架的邮件支持附带了标准的 JavaMail 实现。 有关更多信息,请参阅相关的 Javadoc。
6.2. 使用 JavaMailMimeMessageHelper
在处理 JavaMail 消息时,有一个非常实用的类是
org.springframework.mail.javamail.MimeMessageHelper,它可以避免你使用冗长繁琐的 JavaMail API。通过使用 MimeMessageHelper,创建 MimeMessage 非常简单,如下例所示:
// of course you would use DI in any real-world cases
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setTo("[email protected]");
helper.setText("Thank you for ordering!");
sender.send(message);
6.2.1. 发送附件和内联资源
多部分电子邮件消息既支持附件,也支持内联资源。内联资源的示例包括您希望在邮件中使用但不希望作为附件显示的图片或样式表。
附件
以下示例展示了如何使用 MimeMessageHelper 发送一封带有单个 JPEG 图像附件的电子邮件:
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[email protected]");
helper.setText("Check out this image!");
// let's attach the infamous windows Sample file (this time copied to c:/)
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addAttachment("CoolImage.jpg", file);
sender.send(message);
内联资源
以下示例展示了如何使用 MimeMessageHelper 发送包含内联图片的电子邮件:
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[email protected]");
// use the true flag to indicate the text included is HTML
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);
// let's include the infamous windows Sample file (this time copied to c:/)
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addInline("identifier1234", res);
sender.send(message);
内联资源通过指定的 MimeMessage(如上例中的 Content-ID)添加到 identifier1234 中。添加文本和资源的顺序非常重要。请务必先添加文本,然后再添加资源。如果顺序颠倒,则无法正常工作。 |
6.2.2. 使用模板库创建电子邮件内容
前面几节示例中所示的代码通过调用诸如 message.setText(..) 等方法显式地创建了电子邮件消息的内容。
对于简单场景而言,这种方式是合适的;在前述示例的上下文中也是可以接受的,因为这些示例的目的是向您展示该 API 最基本的用法。
然而,在典型的企业应用程序中,开发人员通常不会使用前面所示的方法来创建电子邮件内容,原因有很多:
-
在 Java 代码中创建基于 HTML 的电子邮件内容既繁琐又容易出错。
-
显示逻辑与业务逻辑之间没有清晰的分离。
-
更改电子邮件内容的显示结构需要编写 Java 代码、重新编译、重新部署等操作。
通常,解决这些问题的方法是使用模板库(例如 FreeMarker)来定义邮件内容的显示结构。这样,您的代码只需负责创建要在邮件模板中渲染的数据并发送邮件即可。当您的邮件内容变得哪怕只是稍微复杂一些时,这种做法无疑是一种最佳实践;而借助 Spring 框架对 FreeMarker 提供的支持类,实现起来也变得非常简单。
7. 任务执行与调度
Spring 框架分别通过 TaskExecutor 和 TaskScheduler 接口提供了对任务异步执行和调度的抽象。Spring 还提供了这些接口的实现,支持线程池或在应用服务器环境中委托给 CommonJ。最终,通过这些通用接口背后的实现,屏蔽了 Java SE 5、Java SE 6 和 Java EE 环境之间的差异。
Spring 还提供了集成类,以支持使用 Timer(自 JDK 1.3 起内置)和 Quartz 调度器(https://www.quartz-scheduler.org/)进行任务调度。
你可以通过使用一个 FactoryBean 并分别传入可选的 Timer 或 Trigger 实例引用来配置这两种调度器。此外,还提供了一个便捷类,可用于 Quartz 调度器和 Timer,允许你调用现有目标对象的方法(类似于标准的 MethodInvokingFactoryBean 操作)。
7.1. SpringTaskExecutor抽象
执行器(Executors)是 JDK 中对线程池概念的称呼。“执行器”这一命名的原因在于,其底层实现并不保证实际上是一个池。执行器可能是单线程的,甚至可能是同步的。Spring 的抽象层隐藏了 Java SE 与 Java EE 环境之间的实现细节差异。
Spring 的 TaskExecutor 接口与 java.util.concurrent.Executor 接口完全相同。事实上,它最初存在的主要原因是为了在使用线程池时消除对 Java 5 的依赖。该接口仅包含一个方法(execute(Runnable task)),用于根据线程池的语义和配置来接受任务并执行。
TaskExecutor 最初是为了给其他 Spring 组件提供一个在需要时进行线程池管理的抽象而创建的。ApplicationEventMulticaster、JMS 的 AbstractMessageListenerContainer 以及 Quartz 集成等组件都使用 TaskExecutor 抽象来进行线程池管理。然而,如果你的 Bean 需要线程池行为,也可以将此抽象用于你自己的需求。
7.1.1. TaskExecutor类型
Spring 包含了若干个预构建的 TaskExecutor 实现。
在大多数情况下,你很可能永远都不需要自己实现一个。
Spring 提供的变体如下所示:
-
SyncTaskExecutor: 该实现不会异步执行调用。相反,每次调用都在调用线程中进行。它主要用于不需要多线程的场景,例如简单的测试用例。 -
SimpleAsyncTaskExecutor: 此实现不会重用任何线程。相反,它会为每次调用启动一个新线程。 然而,它确实支持并发限制,当调用数量超过该限制时,后续调用将被阻塞, 直到有空闲槽位为止。如果您需要真正的线程池功能,请参见本列表稍后介绍的ThreadPoolTaskExecutor。 -
ConcurrentTaskExecutor: 此实现是java.util.concurrent.Executor实例的一个适配器。 还有一个替代方案(ThreadPoolTaskExecutor),它将Executor的配置参数以 bean 属性的形式暴露出来。 通常很少需要直接使用ConcurrentTaskExecutor。 然而,如果ThreadPoolTaskExecutor无法满足你的灵活性需求,ConcurrentTaskExecutor可作为替代选择。 -
ThreadPoolTaskExecutor: 这是最常用的实现。它暴露了用于配置java.util.concurrent.ThreadPoolExecutor的 Bean 属性,并将其包装在一个TaskExecutor中。 如果您需要适配其他类型的java.util.concurrent.Executor,我们建议您改用ConcurrentTaskExecutor。 -
WorkManagerTaskExecutor: 该实现使用 CommonJWorkManager作为其底层服务提供者, 是在 Spring 应用上下文中为 WebLogic 或 WebSphere 设置基于 CommonJ 的线程池集成的核心便捷类。 -
DefaultManagedTaskExecutor: 此实现在 JSR-236 兼容的运行时环境(例如 Java EE 7+ 应用服务器)中使用通过 JNDI 获取的ManagedExecutorService, 以替代 CommonJ WorkManager 来实现该目的。
7.1.2. 使用一个TaskExecutor
Spring 的 TaskExecutor 实现被用作简单的 JavaBean。在下面的示例中,
我们定义了一个使用 ThreadPoolTaskExecutor 异步打印
一组消息的 bean:
import org.springframework.core.task.TaskExecutor;
public class TaskExecutorExample {
private class MessagePrinterTask implements Runnable {
private String message;
public MessagePrinterTask(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
private TaskExecutor taskExecutor;
public TaskExecutorExample(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printMessages() {
for(int i = 0; i < 25; i++) {
taskExecutor.execute(new MessagePrinterTask("Message" + i));
}
}
}
正如你所见,你不需要自己从线程池中获取线程并执行它,而是将你的 Runnable 添加到队列中。然后,TaskExecutor 会根据其内部规则决定何时运行该任务。
为了配置 TaskExecutor 所使用的规则,我们提供了简单的 bean 属性:
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
<constructor-arg ref="taskExecutor"/>
</bean>
7.2. SpringTaskScheduler抽象
除了 TaskExecutor 抽象之外,Spring 3.0 还引入了一个 TaskScheduler,
它提供了多种方法用于调度任务在未来某个时间点执行。
以下代码清单展示了 TaskScheduler 接口的定义:
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
最简单的方法是名为 schedule 的方法,它仅接受一个 Runnable 和一个 Date。
这将使任务在指定时间之后仅执行一次。所有其他方法
都能够调度任务重复运行。固定速率(fixed-rate)和固定延迟(fixed-delay)
方法适用于简单的周期性执行,而接受 Trigger 的方法则
灵活得多。
7.2.1. Trigger接口
Trigger 接口的设计在本质上受到 JSR-236 的启发,而截至 Spring 3.0 版本,JSR-236 尚未被官方实现。Trigger 的基本思想是:执行时间可以根据先前执行的结果甚至任意条件来确定。如果这些判断确实考虑了前一次执行的结果,那么该信息可通过 TriggerContext 获取。Trigger 接口本身非常简单,如下列代码所示:
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
TriggerContext 是最重要的部分。它封装了所有相关数据,并且在将来如有必要,还可以进行扩展。TriggerContext 是一个接口(默认使用 SimpleTriggerContext 实现)。以下列表展示了 Trigger 实现类可用的方法。
public interface TriggerContext {
Date lastScheduledExecutionTime();
Date lastActualExecutionTime();
Date lastCompletionTime();
}
7.2.2. Trigger实现
Spring 提供了 Trigger 接口的两种实现。其中最有趣的是 CronTrigger。它允许基于 cron 表达式来调度任务。例如,以下任务被安排在每个整点过 15 分钟时运行,但仅限于工作日的上午 9 点至下午 5 点“工作时间”内:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一种实现是 PeriodicTrigger,它接受一个固定的周期、一个可选的初始延迟值,以及一个布尔值来指示该周期应被解释为固定频率(fixed-rate)还是固定延迟(fixed-delay)。由于 TaskScheduler 接口已经定义了以固定频率或固定延迟调度任务的方法,因此只要可能,应直接使用这些方法。PeriodicTrigger 实现的价值在于,你可以在依赖于 Trigger 抽象的组件中使用它。例如,可以方便地让周期性触发器、基于 cron 表达式的触发器,甚至自定义的触发器实现相互替换使用。这样的组件可以利用依赖注入,使你可以从外部配置这些 Triggers,从而轻松地修改或扩展它们。
7.2.3. TaskScheduler实现
与 Spring 的 TaskExecutor 抽象类似,TaskScheduler 配置的主要优势在于,应用程序的调度需求与其部署环境解耦。这种抽象层级在部署到应用服务器环境时尤为相关,因为在该环境中应用程序不应直接创建线程。针对此类场景,Spring 提供了 TimerManagerTaskScheduler,它在 WebLogic 或 WebSphere 上委托给 CommonJ 的 TimerManager;此外,还提供了较新版本的 DefaultManagedTaskScheduler,它在 Java EE 7+ 环境中委托给 JSR-236 的 ManagedScheduledExecutorService。这两种调度器通常通过 JNDI 查找进行配置。
只要不需要外部线程管理,一种更简单的替代方案是在应用程序内部设置一个本地的 ScheduledExecutorService,并通过 Spring 的 ConcurrentTaskScheduler 进行适配。为方便起见,Spring 还提供了 ThreadPoolTaskScheduler,它在内部委托给 ScheduledExecutorService,以提供类似于 ThreadPoolTaskExecutor 的常见 Bean 风格配置。
这些变体在宽松的应用服务器环境中(尤其是 Tomcat 和 Jetty 上)用于本地嵌入式线程池设置时,也能很好地工作。
7.3. 用于调度和异步执行的注解支持
Spring 为任务调度和异步方法执行提供了注解支持。
7.3.1. 启用调度注解
要启用对 @Scheduled 和 @Async 注解的支持,您可以在其中一个 @EnableScheduling 类上添加 @EnableAsync 和
@Configuration,如下例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以为应用程序挑选并选择相关的注解。例如,
如果您只需要 @Scheduled 的支持,可以省略 @EnableAsync。若需更细粒度的控制,
您还可以额外实现 SchedulingConfigurer 接口、AsyncConfigurer 接口,或两者都实现。
请参阅 SchedulingConfigurer
和 AsyncConfigurer
的 Javadoc 以获取完整详情。
如果你更喜欢使用 XML 配置,可以使用 <task:annotation-driven> 元素,如下例所示:
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
请注意,通过上述 XML 配置,提供了执行器(executor)引用,用于处理带有 @Async 注解的方法所对应的任务;同时提供了调度器(scheduler)引用,用于管理带有 @Scheduled 注解的方法。
处理 @Async 注解的默认通知模式是 proxy,该模式仅允许通过代理拦截方法调用。同一类中的本地方法调用无法以这种方式被拦截。如需更高级的拦截模式,请考虑切换到 aspectj 模式,并结合编译时或加载时织入(weaving)使用。 |
7.3.2.@Scheduled注解
你可以将 @Scheduled 注解添加到一个方法上,并附带触发器元数据。例如,以下方法将以固定的延迟每五秒调用一次,这意味着周期是从每次前次调用完成的时间开始计算的:
@Scheduled(fixedDelay=5000)
public void doSomething() {
// something that should run periodically
}
如果你需要固定频率的执行,可以更改注解中指定的属性名称。以下方法每五秒调用一次(每次调用的开始时间之间间隔五秒):
@Scheduled(fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
对于固定延迟(fixed-delay)和固定速率(fixed-rate)任务,您可以通过指定在方法首次执行前等待的毫秒数来设置初始延迟,如下列 fixedRate 示例所示:
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
如果简单的周期性调度表达能力不足,您可以提供一个 cron 表达式。 以下示例仅在工作日运行:
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should run on weekdays only
}
你也可以使用 zone 属性来指定解析 cron 表达式所用的时区。 |
请注意,要被调度的方法必须具有 void 返回类型,并且不能接受任何参数。如果该方法需要与应用上下文中的其他对象进行交互,这些对象通常会通过依赖注入的方式提供。
|
从 Spring Framework 4.3 起, 请确保在运行时不要初始化多个相同的 |
7.3.3.@Async注解
你可以在一个方法上添加 @Async 注解,以使该方法的调用异步执行。换句话说,调用方在调用时会立即返回,而该方法的实际执行则会在一个已提交给 Spring TaskExecutor 的任务中进行。在最简单的情况下,你可以将该注解应用于一个返回 void 的方法,如下例所示:
@Async
void doSomething() {
// this will be run asynchronously
}
与使用 @Scheduled 注解的方法不同,这些方法可以接受参数,因为它们在运行时是由调用者以“常规”方式调用的,而不是由容器管理的定时任务所调用。例如,以下代码是对 @Async 注解的合法应用:
@Async
void doSomething(String s) {
// this will be run asynchronously
}
即使返回值的方法也可以被异步调用。然而,这类方法的返回值类型必须是 Future。这样仍然可以享受异步执行的好处,使得调用方可以在调用该 get() 的 Future 方法之前执行其他任务。以下示例展示了如何在返回值的方法上使用 @Async:
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
@Async 方法不仅可以声明常规的 java.util.concurrent.Future 返回类型,
还可以声明 Spring 的 org.springframework.util.concurrent.ListenableFuture,
或者从 Spring 4.2 起,声明 JDK 8 的 java.util.concurrent.CompletableFuture,
以便更丰富地与异步任务进行交互,并立即与后续处理步骤进行组合。 |
您不能将 @Async 与生命周期回调方法(例如 @PostConstruct)结合使用。若要异步初始化 Spring Bean,目前您必须使用一个单独的初始化 Spring Bean,由它来调用目标 Bean 上带有 @Async 注解的方法,如下例所示:
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
@Async 没有直接对应的 XML 配置,因为这类方法从一开始就应该被设计为异步执行,而不应通过外部方式重新声明为异步。
不过,你可以结合自定义切入点(pointcut),手动使用 Spring AOP 来配置 Spring 的 AsyncExecutionInterceptor。 |
7.3.4. 执行器限定条件与@Async
默认情况下,在方法上指定 @Async 注解时,所使用的执行器(executor)是在启用异步支持时配置的执行器,
即如果你使用 XML 配置,则为 “annotation-driven” 元素;或者如果你有自定义的 AsyncConfigurer 实现,则为该实现所配置的执行器。
然而,当你需要为某个特定方法指定使用非默认的执行器时,可以使用 value 注解的 @Async 属性。
以下示例展示了如何实现这一点:
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
在这种情况下,"otherExecutor" 可以是 Spring 容器中任意 Executor bean 的名称,也可以是与任意 Executor 关联的限定符(qualifier)的名称(例如,通过 <qualifier> 元素或 Spring 的 @Qualifier 注解指定的限定符)。
7.3.5. 使用异常管理@Async
当一个 @Async 方法具有 Future 类型的返回值时,很容易处理方法执行期间抛出的异常,因为该异常会在调用 get 结果的 Future 方法时抛出。然而,对于 void 返回类型,异常将不会被捕获,也无法传递出去。此时,你可以提供一个 AsyncUncaughtExceptionHandler 来处理此类异常。以下示例展示了如何实现这一点:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
默认情况下,异常仅会被记录。你可以通过使用 AsyncUncaughtExceptionHandler 或 AsyncConfigurer XML 元素来定义自定义的 <task:annotation-driven/>。
7.4.task命名空间
从 3.0 版本开始,Spring 提供了一个用于配置 TaskExecutor 和
TaskScheduler 实例的 XML 命名空间。它还提供了一种便捷的方式来配置任务,使其通过触发器进行调度。
7.4.1. 'scheduler' 元素
以下元素将创建一个具有指定线程池大小的 ThreadPoolTaskScheduler 实例:
<task:scheduler id="scheduler" pool-size="10"/>
为 id 属性提供的值将用作线程池中线程名称的前缀。scheduler 元素相对简单。如果您未提供 pool-size 属性,则默认线程池仅包含一个线程。调度器没有其他配置选项。
7.4.2.executor元素
以下代码将创建一个 ThreadPoolTaskExecutor 实例:
<task:executor id="executor" pool-size="10"/>
与上一节中所示的调度器一样,为id属性提供的值将用作池中线程名称的前缀。就池大小而言,executor元素比scheduler元素支持更多的配置选项。首先,ThreadPoolTaskExecutor的线程池本身具有更高的可配置性。执行器的线程池不仅可以设置单一大小,还可以为核心线程数和最大线程数分别指定不同的值。如果只提供单个值,则执行器将使用固定大小的线程池(核心大小和最大大小相同)。然而,executor元素的pool-size属性也接受形如min-max的范围值。以下示例将最小值设置为5,最大值设置为25:
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
在上述配置中,还提供了一个queue-capacity值。
线程池的配置还应结合执行器的队列容量来考虑。关于池大小与队列容量之间关系的完整描述,请参阅
ThreadPoolExecutor的文档。
其核心思想是:当提交一个任务时,如果当前活动线程数小于核心线程数,执行器会首先尝试使用空闲线程。
如果已达到核心线程数,只要队列容量尚未耗尽,该任务将被加入队列。
只有当队列容量已满时,执行器才会创建超出核心线程数的新线程。如果同时已达到最大线程数,则执行器将拒绝该任务。
默认情况下,队列是无界的,但这种配置很少是期望的,
因为当所有线程池中的线程都处于忙碌状态时,如果向该队列中添加了足够多的任务,就可能导致 OutOfMemoryErrors。
此外,如果队列是无界的,最大线程数(max size)将完全不起作用。
由于执行器在创建超出核心线程数的新线程之前总是先尝试将任务加入队列,
因此只有当队列具有有限容量时,线程池才能扩展到超过核心线程数
(这也是为什么在使用无界队列时,固定大小的线程池是唯一合理的选择)。
考虑上面提到的任务被拒绝的情况。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个 TaskRejectedException。然而,
拒绝策略实际上是可配置的。该异常在使用默认拒绝策略时抛出,即AbortPolicy的实现。对于一些在高负载情况下可以跳过的任务,您可以配置为DiscardPolicy或DiscardOldestPolicy。另一个在高负载情况下需要限制提交任务的应用程序可以使用的选项是CallerRunsPolicy。该策略不会抛出异常或丢弃任务,而是强制调用 submit 方法的线程自己执行该任务。这种设计的理念是,调用方在执行该任务时处于忙碌状态,无法立即提交其他任务。因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制。通常,这允许执行器在处理的任务上“追赶”进度,从而释放队列、线程池或两者中的一些容量。您可以在executor元素的rejection-policy属性上可用的值枚举中选择这些选项中的任何一个。
以下示例展示了一个带有多个属性的 executor 元素,用于指定各种行为:
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
最后,keep-alive 设置决定了线程在被停止之前可以保持空闲状态的时间限制(以秒为单位)。如果当前线程池中的线程数量超过核心线程数,在等待指定时间而未处理任何任务后,多余的线程将被停止。若将该时间值设为零,则多余的线程在执行完一个任务且任务队列中没有后续工作时会立即停止。
以下示例将 keep-alive 值设置为两分钟:
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
7.4.3. 'scheduled-tasks' 元素
Spring 任务命名空间最强大的功能是支持在 Spring 应用上下文中配置任务调度。这种方式类似于 Spring 中其他“方法调用器”(method-invokers)的实现,例如 JMS 命名空间所提供的用于配置消息驱动 POJO 的机制。基本上,ref 属性可以指向任意由 Spring 管理的对象,而 method 属性则指定要在该对象上调用的方法名称。以下代码清单展示了一个简单的示例:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
调度器由外层元素引用,每个单独的任务都包含其触发器元数据的配置。在前面的示例中,该元数据定义了一个具有固定延迟(fixed delay)的周期性触发器,表示每次任务执行完成后需等待的毫秒数。另一个选项是 fixed-rate,它表示无论前一次执行耗时多长,都按固定频率运行该方法。此外,对于 fixed-delay 和 fixed-rate 任务,你都可以指定一个 'initial-delay' 参数,用于指示在首次执行该方法之前需要等待的毫秒数。如果需要更精细的控制,也可以使用 cron 属性。以下示例展示了这些其他选项:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
7.5. 使用 Quartz 调度器
Quartz 使用 Trigger、Job 和 JobDetail 对象来实现各种作业的调度。有关 Quartz 背后的基本概念,请参阅
https://www.quartz-scheduler.org/。为方便起见,Spring 提供了几个类,以简化在基于 Spring 的应用程序中使用 Quartz。
7.5.1. 使用JobDetailFactoryBean
Quartz 的 JobDetail 对象包含运行一个作业所需的所有信息。Spring 提供了一个 JobDetailFactoryBean,它为 XML 配置提供了类似 Bean 的属性。
请看以下示例:
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
作业详细信息配置已包含运行该作业(ExampleJob)所需的全部信息。
超时时间在作业数据映射(job data map)中指定。作业数据映射可通过JobExecutionContext(在执行时传递给您)获取,但JobDetail也会将作业数据映射中的属性自动映射到作业实例的属性上。因此,在以下示例中,ExampleJob包含一个名为timeout的Bean属性,而JobDetail会自动将其应用:
package example;
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailFactoryBean (5)
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// do the actual work
}
}
作业数据映射(job data map)中的所有附加属性也同样可供您使用。
通过使用 name 和 group 属性,你可以分别修改作业的名称和组。默认情况下,作业的名称与 JobDetailFactoryBean 的 bean 名称一致(在上面的示例中为 exampleJob)。 |
7.5.2. 使用MethodInvokingJobDetailFactoryBean
通常,你只需要调用某个特定对象上的一个方法。通过使用 MethodInvokingJobDetailFactoryBean,你可以精确地实现这一点,如下例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
前面的示例会导致在 doIt 对象上调用 exampleBusinessObject 方法,如下例所示:
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
通过使用 MethodInvokingJobDetailFactoryBean,您无需再创建仅用于调用某个方法的单行作业。您只需创建实际的业务对象,并配置好详细信息对象即可。
默认情况下,Quartz 作业(Job)是无状态的,这可能导致多个作业相互干扰。如果你为同一个 JobDetail 指定了两个触发器(trigger),那么有可能在第一个作业尚未完成时,第二个作业就已经开始执行。如果 JobDetail 类实现了 Stateful 接口,则不会发生这种情况——第二个作业会等到第一个作业完成后才开始执行。若要使通过 MethodInvokingJobDetailFactoryBean 创建的作业不并发执行,可将 concurrent 标志设置为 false,如下例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
| 默认情况下,作业将以并发方式运行。 |
7.5.3. 使用触发器连接作业以及SchedulerFactoryBean
我们已经创建了作业详情(job details)和作业(jobs)。我们还回顾了那个便捷的 Bean,它允许你调用特定对象上的某个方法。当然,我们仍然需要对这些作业本身进行调度。这是通过使用触发器(triggers)和 SchedulerFactoryBean 来实现的。Quartz 内部提供了多种触发器,而 Spring 提供了两个 Quartz FactoryBean 的实现类,并带有便捷的默认配置:CronTriggerFactoryBean 和 SimpleTriggerFactoryBean。
触发器需要被调度。Spring 提供了一个 SchedulerFactoryBean,它将触发器作为属性暴露出来。SchedulerFactoryBean 使用这些触发器来调度实际的作业。
以下示例同时使用了 SimpleTriggerFactoryBean 和 CronTriggerFactoryBean:
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- see the example of method invoking job above -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 10 seconds -->
<property name="startDelay" value="10000"/>
<!-- repeat every 50 seconds -->
<property name="repeatInterval" value="50000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- run every morning at 6 AM -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
前面的示例设置了两个触发器:一个每隔50秒运行一次,初始延迟为10秒;另一个每天早上6点运行。为了完成全部配置,我们需要设置SchedulerFactoryBean,如下例所示:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>
SchedulerFactoryBean 提供更多属性,例如作业详情使用的日历、用于自定义 Quartz 的属性,以及 Spring 提供的 JDBC 数据源。请参阅 SchedulerFactoryBean javadoc 以获取更多信息。
SchedulerFactoryBean 还会识别类路径(classpath)中的 quartz.properties 文件,
该文件基于 Quartz 的属性键进行配置,与常规的 Quartz 配置方式相同。请注意,许多
SchedulerFactoryBean 的设置会与属性文件中的通用 Quartz 设置相互作用;
因此,不建议在两个地方同时指定这些值。例如,如果您打算使用 Spring 提供的 DataSource,
就不要设置 "org.quartz.jobStore.class" 属性。 |
8. 缓存抽象
从 3.1 版本开始,Spring 框架提供了对透明地为现有 Spring 应用程序添加缓存的支持。与事务支持类似,缓存抽象允许以对代码影响最小的方式一致地使用各种缓存解决方案。
在 Spring Framework 4.1 中,缓存抽象得到了显著扩展,增加了对 JSR-107 注解 的支持以及更多的自定义选项。
8.1. 理解缓存抽象
其核心在于,缓存抽象将缓存应用于 Java 方法,从而根据缓存中已有的信息减少方法的执行次数。也就是说,每次调用目标方法时,该抽象会应用一种缓存行为,检查对于给定的参数,该方法是否已经被调用过。如果已经调用过,则直接返回缓存的结果,而无需再次执行实际的方法;如果尚未调用过,则执行该方法,将其结果缓存并返回给用户,以便下次使用相同参数调用该方法时,可以直接返回缓存的结果。通过这种方式,对于给定的一组参数,那些开销较大的方法(无论是 CPU 密集型还是 I/O 密集型)只需执行一次,之后便可重复使用缓存结果,而无需再次实际调用该方法。整个缓存逻辑是透明应用的,对调用方没有任何干扰。
| 这种方法仅适用于那些对于给定输入(或参数)无论调用多少次都能保证返回相同输出(结果)的方法。 |
缓存抽象提供了其他与缓存相关的操作,例如更新缓存内容或移除一个或所有条目。如果缓存处理的数据在应用程序运行过程中可能发生更改,这些操作将非常有用。
与 Spring 框架中的其他服务一样,缓存服务是一种抽象(而非缓存实现),需要使用实际的存储来保存缓存数据——也就是说,该抽象使你无需编写缓存逻辑,但并不提供实际的数据存储。这种抽象通过 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口得以具体化。
Spring 提供了该抽象的几种实现:
基于 JDK java.util.concurrent.ConcurrentMap 的缓存、Ehcache 2.x、
Gemfire 缓存、Caffeine 以及符合 JSR-107
规范的缓存(例如 Ehcache 3.x)。有关集成其他缓存存储和提供程序的更多信息,请参见插入不同的后端缓存。
| 缓存抽象层对多线程和多进程环境没有特殊处理,因为此类功能由缓存实现本身负责处理。 |
如果你处于多进程环境(即应用程序部署在多个节点上), 你需要相应地配置你的缓存提供程序。根据你的使用场景,在多个节点上保存同一数据的副本可能就足够了。 然而,如果在应用程序运行过程中需要修改数据,则可能需要启用其他传播机制。
缓存特定项直接等同于在编程式缓存交互中常见的“先获取,若未找到则执行操作并最终放入缓存”的代码块。 不会应用任何锁机制,多个线程可能会并发地尝试加载同一项。 驱逐(eviction)操作也是如此。如果多个线程并发地尝试更新或驱逐数据,你可能会使用到过期的数据。某些缓存提供者在此方面提供了高级特性。更多详细信息,请参阅你所使用的缓存提供者的文档。
要使用缓存抽象,您需要注意两个方面:
-
缓存声明:标识需要缓存的方法及其缓存策略。
-
缓存配置:用于存储数据并从中读取数据的底层缓存。
8.2. 基于注解的声明式缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:
-
@Cacheable:触发缓存填充。 -
@CacheEvict:触发缓存清除。 -
@CachePut:在不干扰方法执行的情况下更新缓存。 -
@Caching:将多个缓存操作组合在一起,应用于一个方法。 -
@CacheConfig:在类级别共享一些通用的缓存相关设置。
8.2.1.@Cacheable注解
顾名思义,您可以使用 @Cacheable 来标记可缓存的方法——也就是说,这些方法的执行结果会被存储在缓存中,以便在后续调用(使用相同的参数)时,直接从缓存中返回值,而无需实际再次执行该方法。在最简单的形式中,注解声明需要指定与被注解方法关联的缓存名称,如下例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在上述代码片段中,findBook 方法与名为 books 的缓存相关联。
每次调用该方法时,都会检查缓存,以确定该调用是否已经执行过,从而无需重复执行。虽然大多数情况下只声明一个缓存,但该注解允许指定多个名称,以便同时使用多个缓存。在这种情况下,会在调用方法之前逐一检查每个缓存——只要至少有一个缓存命中,就会返回对应的值。
| 所有其他不包含该值的缓存也会被更新,即使缓存的方法实际上并未被调用。 |
以下示例在 @Cacheable 方法上使用了 findBook 注解,并指定了多个缓存:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
默认密钥生成
由于缓存本质上是键值存储,因此每次调用被缓存的方法都需要转换为适合缓存访问的键。缓存抽象使用了一个简单的KeyGenerator,其基于以下算法:
-
如果没有提供参数,则返回
SimpleKey.EMPTY。 -
如果只提供一个参数,则返回该实例。
-
如果提供了多个参数,则返回一个包含所有参数的
SimpleKey。
只要参数具有自然键并实现了有效的 hashCode() 和 equals() 方法,这种方法在大多数用例中都能很好地工作。如果情况并非如此,你就需要更改策略。
要提供一个不同的默认键生成器,你需要实现
org.springframework.cache.interceptor.KeyGenerator 接口。
|
随着 Spring 4.0 的发布,默认的键生成策略发生了变化。早期版本的 Spring 使用的键生成策略在处理多个键参数时,仅考虑参数的 如果你想继续使用之前的键策略,可以配置已弃用的
|
自定义密钥生成声明
由于缓存是通用的,目标方法很可能具有各种各样的签名,这些签名无法直接映射到缓存结构上。当目标方法包含多个参数,而其中只有一部分适合用于缓存(其余参数仅由方法逻辑使用)时,这一问题往往会变得尤为明显。请考虑以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍看之下,虽然这两个 boolean 参数会影响查找图书的方式,
但它们对缓存毫无用处。此外,如果其中只有一个参数重要,
而另一个不重要,又该怎么办呢?
对于这类情况,@Cacheable 注解允许你通过其 key 属性指定缓存键的生成方式。你可以使用 SpEL 来选择感兴趣的参数(或其嵌套属性)、执行操作,甚至在无需编写任何代码或实现任何接口的情况下调用任意方法。
相比于默认生成器,这是推荐的做法,因为随着代码库的增长,方法的签名往往会大不相同。尽管默认策略可能适用于某些方法,但很少能适用于所有方法。
以下示例使用了各种 SpEL 表达式声明(如果您不熟悉 SpEL, 请务必阅读Spring 表达式语言):
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
前面的代码片段展示了选择某个参数、该参数的某个属性,甚至任意(静态)方法是多么简单。
如果负责生成密钥的算法过于特定,或者需要被共享,你可以在操作上定义一个自定义的 keyGenerator。为此,请指定要使用的 KeyGenerator bean 实现的名称,如下例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key 和 keyGenerator 参数是互斥的,同时指定这两个参数的操作将导致异常。 |
默认缓存解析
缓存抽象使用一个简单的 CacheResolver,通过配置的 CacheManager 来获取在操作级别定义的缓存。
要提供一个不同的默认缓存解析器,您需要实现
org.springframework.cache.interceptor.CacheResolver 接口。
自定义缓存解析
默认的缓存解析方式非常适合那些仅使用单个 CacheManager 且没有复杂缓存解析需求的应用程序。
对于使用多个缓存管理器的应用程序,您可以为每个操作设置要使用的cacheManager,如下例所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
| 1 | 指定 anotherCacheManager。 |
你也可以完全替换 CacheResolver,其方式类似于替换键生成器。每次缓存操作都会请求解析,让实现根据运行时参数实际解析出要使用的缓存。以下示例展示了如何指定一个 CacheResolver:
@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
| 1 | 指定 CacheResolver。 |
|
从 Spring 4.1 开始,缓存注解的 与 |
同步缓存
在多线程环境中,某些操作可能会针对相同的参数被并发调用(通常发生在启动时)。默认情况下,缓存抽象不会进行任何锁定,同一个值可能会被多次计算,从而违背了缓存的初衷。
对于这些特定情况,您可以使用 sync 属性来指示底层缓存提供程序在计算值时锁定缓存条目。这样一来,只有一个线程会忙于计算该值,而其他线程则会被阻塞,直到该条目在缓存中更新为止。以下示例展示了如何使用 sync 属性:
@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
| 1 | 使用 sync 属性。 |
这是一个可选功能,您所选用的缓存库可能不支持该功能。
核心框架提供的所有 CacheManager 实现均支持此功能。更多详细信息,请参阅您所使用的缓存提供者的文档。 |
条件缓存
有时,某个方法可能并不总是适合进行缓存(例如,它可能依赖于传入的参数)。缓存注解通过 condition 参数支持此类使用场景,该参数接收一个 SpEL 表达式,表达式求值结果为 true 或 false。如果结果为 true,则对该方法进行缓存;否则,其行为就如同该方法未被缓存一样(即无论缓存中存在什么值或使用了什么参数,该方法每次都会被调用)。例如,以下方法仅在参数 name 的长度小于 32 时才会被缓存:
@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
| 1 | 在 @Cacheable 上设置条件。 |
除了 condition 参数外,你还可以使用 unless 参数来阻止将值添加到缓存中。condition 表达式与 unless 不同,它在方法调用之后进行求值。延续前面的例子,也许我们只想缓存平装书,如下例所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
| 1 | 使用 unless 属性来阻止精装书。 |
缓存抽象支持 java.util.Optional 返回类型。如果 Optional 值是存在的,它将被存储到关联的缓存中;如果 Optional 值不存在,则会在关联的缓存中存储 null。#result 始终引用业务实体本身,而不会引用任何受支持的包装器,因此前面的示例可以重写如下:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
请注意,#result 仍然引用的是 Book,而不是 Optional<Book>。由于它可能为 null,我们使用了 SpEL 的安全导航操作符。
可用的缓存 SpEL 评估上下文
每个 SpEL 表达式都会针对专用的 context 进行求值。
除了内置参数外,该框架还提供专用的缓存相关元数据,例如参数名称。下表描述了上下文中可用的各项内容,以便您将其用于键和条件计算:
| 姓名 | 位置 | 描述 | 例举 |
|---|---|---|---|
|
根对象 |
正在被调用的方法的名称 |
|
|
根对象 |
被调用的方法 |
|
|
根对象 |
被调用的目标对象 |
|
|
根对象 |
被调用目标的类 |
|
|
根对象 |
用于调用目标的参数(以数组形式) |
|
|
根对象 |
当前方法所运行的缓存集合 |
|
参数名称 |
评估上下文 |
任意方法参数的名称。如果这些名称不可用(可能是因为缺少调试信息),参数名称也可以通过 |
|
|
评估上下文 |
方法调用的结果(要被缓存的值)。仅在 |
|
8.2.2.@CachePut注解
当需要在不干扰方法执行的情况下更新缓存时,可以使用 @CachePut 注解。也就是说,该方法始终会被调用,并将其结果放入缓存中(根据 @CachePut 的配置选项)。它支持与 @Cacheable 相同的选项,但应主要用于填充缓存,而非优化方法执行流程。以下示例使用了 @CachePut 注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一方法上同时使用 @CachePut 和 @Cacheable 注解通常是强烈不推荐的,因为它们的行为不同。#result 注解会通过使用缓存来跳过方法的执行,而 3 注解则会强制执行方法以更新缓存。这会导致意外的行为。除非在特定的边缘情况下(例如注解带有互斥条件,使得它们彼此排除),否则应避免此类声明。另外请注意,这些条件不应依赖于结果对象(即 4 变量),因为这些条件会在执行前进行验证,以确认是否应排除某个注解。 |
8.2.3.@CacheEvict注解
缓存抽象不仅支持向缓存存储中填充数据,还支持清除数据。
这一过程有助于从缓存中移除过时或未使用的数据。与
@Cacheable 不同,@CacheEvict 用于标记执行缓存清除操作的方法
(即那些作为触发器、用于从缓存中移除数据的方法)。
与 @CacheEvict 类似,allEntries 也需要指定一个或多个受该操作影响的缓存,
允许自定义缓存和键的解析方式,或指定一个条件,并且提供了一个额外的参数
(books),用于指示是否需要执行整个缓存的清除操作,
而不仅仅是基于键清除某一条目。以下示例将从 5 缓存中清除所有条目:
@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
| 1 | 使用 allEntries 属性从缓存中清除所有条目。 |
当需要清除整个缓存区域时,此选项非常有用。 与逐个驱逐每个条目(这种方式效率低下且耗时较长)不同, 所有条目会在一次操作中被全部移除,如前面的示例所示。 请注意,在这种情况下,框架会忽略所指定的任何键,因为该键在此场景下不适用 (清除的是整个缓存,而不仅仅是一个条目)。
你还可以通过使用 beforeInvocation 属性来指定驱逐操作是在方法调用之后(默认行为)还是之前发生。前者与其他注解具有相同的语义:一旦方法成功执行完成,就会在缓存上执行一个操作(在本例中为驱逐)。如果方法未实际执行(例如可能已被缓存)或抛出了异常,则不会发生驱逐操作。而后者(beforeInvocation=true)则会导致驱逐操作总是在方法调用之前发生。这在驱逐操作无需与方法执行结果关联的情况下非常有用。
请注意,void 方法可以与 @CacheEvict 一起使用——因为这些方法仅作为触发器,其返回值会被忽略(因为它们不与缓存进行交互)。而 @Cacheable 则不同,它会向缓存中添加数据或更新缓存中的数据,因此需要一个返回结果。
8.2.4.@Caching注解
有时需要指定多个相同类型的注解(例如 @CacheEvict 或
@CachePut)——例如,因为不同缓存之间的条件或键表达式不同。@Caching 允许在同一方法上使用多个嵌套的
@Cacheable、@CachePut 和 @CacheEvict 注解。
以下示例使用了两个 @CacheEvict 注解:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
8.2.5.@CacheConfig注解
到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且可以为每个操作单独设置这些选项。然而,如果某些自定义选项适用于类中的所有操作,逐一配置就会显得繁琐。例如,可以使用一个类级别的定义来替代为类中每个缓存操作都指定所使用的缓存名称。这时,@CacheConfig 就派上用场了。以下示例使用 @CacheConfig 来设置缓存的名称:
@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
| 1 | 使用 @CacheConfig 设置缓存的名称。 |
@CacheConfig 是一个类级别的注解,用于共享缓存名称、自定义的 KeyGenerator、自定义的 CacheManager 以及自定义的 CacheResolver。
在类上添加此注解并不会启用任何缓存操作。
操作级别的自定义设置始终会覆盖在 @CacheConfig 上设置的自定义配置。
因此,这为每个缓存操作提供了三个级别的自定义选项:
-
全局配置,适用于
CacheManager、KeyGenerator。 -
在类级别上使用
@CacheConfig。 -
在操作级别上。
8.2.6. 启用缓存注解
需要注意的是,尽管声明了缓存注解,但并不会自动触发其对应的操作——与 Spring 中的许多功能一样,该特性必须通过声明式方式显式启用(这意味着,如果你怀疑缓存导致了问题,只需移除一行配置即可禁用它,而无需删除代码中的所有注解)。
要启用缓存注解,请在您的某个 @EnableCaching 类上添加 @Configuration 注解:
@Configuration
@EnableCaching
public class AppConfig {
}
或者,对于 XML 配置,您可以使用 cache:annotation-driven 元素:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
</beans>
cache:annotation-driven 元素和 @EnableCaching 注解都允许您指定各种选项,这些选项会影响通过 AOP 添加到应用程序的缓存行为。其配置有意与 @Transactional 的配置保持相似。
处理缓存注解的默认通知模式是 proxy,该模式仅允许通过代理拦截方法调用。同一类内部的本地方法调用无法通过这种方式被拦截。如需更高级的拦截模式,请考虑切换到 aspectj 模式,并结合编译时或加载时织入(weaving)使用。 |
有关实现 CachingConfigurer 所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅
javadoc。 |
| XML 属性 | 注解属性 | 默认 | 描述 |
|---|---|---|---|
|
不适用(请参阅 |
|
要使用的缓存管理器的名称。后台会使用此缓存管理器(如果未设置,则使用 |
|
不适用(请参阅 |
一个使用已配置的 |
用于解析底层缓存的 CacheResolver 的 Bean 名称。 此属性不是必需的,仅在需要替代 'cache-manager' 属性时才需指定。 |
|
不适用(请参阅 |
|
要使用的自定义键生成器的名称。 |
|
不适用(请参阅 |
|
要使用的自定义缓存错误处理器的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会直接抛回给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于使用 |
<cache:annotation-driven/> 仅在其所定义的同一应用上下文中的 bean 上查找 @Cacheable/@CachePut/@CacheEvict/@Caching 注解。
这意味着,如果你将 <cache:annotation-driven/> 放在用于 WebApplicationContext 的 DispatcherServlet 中,
它只会检查控制器(controller)中的 bean,而不会检查服务(service)中的 bean。
更多信息请参见MVC 章节。 |
Spring 建议您仅对具体类(以及具体类中的方法)使用 @Cache* 注解,而不是对接口进行注解。
当然,您也可以在接口(或接口方法)上放置 @Cache* 注解,但这仅在使用代理模式(mode="proxy")时才有效。
如果您使用基于织入的切面(mode="aspectj"),织入基础设施将无法识别在接口级别声明的缓存设置。 |
在代理模式(默认模式)下,只有通过代理传入的外部方法调用才会被拦截。这意味着即使被调用的方法标记了 @Cacheable,自调用(即目标对象内部的一个方法调用该目标对象的另一个方法)在运行时也不会真正触发缓存。在这种情况下,请考虑使用 aspectj 模式。此外,代理必须完全初始化后才能提供预期的行为,因此你不应在初始化代码(例如 @PostConstruct)中依赖此功能。 |
8.2.7. 使用自定义注解
缓存抽象机制允许你使用自定义注解来标识哪些方法会触发缓存的填充或清除。作为一种模板机制,这非常方便,因为它避免了重复声明缓存注解,尤其在指定了 key 或 condition,或者代码库中不允许引入外部包(org.springframework)时特别有用。与其余的构造型(stereotype)注解类似,你可以将 @Cacheable、@CachePut、@CacheEvict 和 @CacheConfig 用作元注解(即可以用来注解其他注解的注解)。在下面的示例中,我们使用自定义注解替换了常见的 @Cacheable 声明:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
在前面的示例中,我们定义了自己的 SlowService 注解,该注解本身使用了 @Cacheable 进行标注。现在我们可以替换以下代码:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
以下示例展示了我们可以用来替换上述代码的自定义注解:
@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
尽管 @SlowService 不是 Spring 的注解,但容器会在运行时自动识别其声明并理解其含义。请注意,如前文所述,需要启用基于注解的驱动行为。
8.3. JCache (JSR-107) 注解
自 4.1 版本起,Spring 的缓存抽象完全支持 JCache 标准(JSR-107)注解:@CacheResult、@CachePut、@CacheRemove 和 @CacheRemoveAll,以及配套的 @CacheDefaults、@CacheKey 和 @CacheValue。
即使您尚未将缓存存储迁移到 JSR-107,也可以使用这些注解。
其内部实现基于 Spring 的缓存抽象,并提供了符合规范的默认 CacheResolver 和 KeyGenerator 实现。
换句话说,如果您已经在使用 Spring 的缓存抽象,则可以切换到这些标准注解,而无需更改您的缓存存储(或相应配置)。
8.3.1. 功能摘要
对于熟悉 Spring 缓存注解的用户,下表描述了 Spring 注解与其对应的 JSR-107 注解之间的主要区别:
| Spring | JSR-107 | 备注 |
|---|---|---|
|
|
非常相似。 |
|
|
虽然 Spring 使用方法调用的结果来更新缓存,但 JCache 要求将该结果作为参数传递,并使用 |
|
|
非常相似。 |
|
|
参见 |
|
|
让你以类似的方式配置相同的概念。 |
JCache 引入了 javax.cache.annotation.CacheResolver 的概念,它与 Spring 的 CacheResolver 接口类似,不同之处在于 JCache 仅支持单个缓存。默认情况下,一个简单的实现会根据注解中声明的名称来获取要使用的缓存。需要注意的是,如果注解中未指定缓存名称,则会自动生成一个默认名称。更多信息请参见 @CacheResult#cacheName() 的 javadoc。
CacheResolver 实例由 CacheResolverFactory 获取。可以为每个缓存操作自定义该工厂,如下例所示:
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1)
public Book findBook(ISBN isbn)
| 1 | 为此操作自定义工厂。 |
| 对于所有被引用的类,Spring 会尝试查找具有给定类型的 bean。 如果存在多个匹配项,则会创建一个新实例,并可以使用常规的 bean 生命周期回调方法,例如依赖注入。 |
键由 javax.cache.annotation.CacheKeyGenerator 生成,其作用与 Spring 的 KeyGenerator 相同。默认情况下,所有方法参数都会被纳入考虑,除非至少有一个参数使用了 @CacheKey 注解。这与 Spring 的自定义键生成声明类似。例如,以下两个操作是等效的,一个使用 Spring 的抽象,另一个使用 JCache:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)
你也可以在操作上指定 CacheKeyResolver,这与指定 CacheResolverFactory 的方式类似。
JCache 可以管理由带注解的方法抛出的异常。这不仅可以防止缓存被更新,还可以将异常本身缓存起来,作为失败的标识,从而避免再次调用该方法。假设当 ISBN 结构无效时,会抛出 InvalidIsbnNotFoundException 异常。这是一种永久性失败(使用这样的参数永远无法检索到任何图书)。以下配置会将该异常缓存起来,使得后续使用相同无效 ISBN 的调用直接抛出已缓存的异常,而不再重新调用该方法:
@CacheResult(cacheName="books", exceptionCacheName="failures"
cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)
8.4. 基于 XML 的声明式缓存
如果无法使用注解(例如由于无法访问源代码或外部代码),你可以使用 XML 进行声明式缓存。因此,与其在方法上添加缓存注解,不如在外部指定目标方法和缓存指令(类似于声明式事务管理的通知)。前一节中的示例可以转换为以下示例:
<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
<cache:caching cache="books">
<cache:cacheable method="findBook" key="#isbn"/>
<cache:cache-evict method="loadBooks" all-entries="true"/>
</cache:caching>
</cache:advice>
<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
<!-- cache manager definition omitted -->
在上述配置中,bookService 被设置为可缓存的。要应用的缓存语义封装在 cache:advice 定义中,该定义使得 findBooks 方法用于将数据放入缓存,而 loadBooks 方法用于从缓存中移除数据。这两个定义都作用于名为 books 的缓存。
aop:config 定义通过使用 AspectJ 切点表达式,将缓存通知(advice)应用到程序中的相应位置(更多相关信息请参见 Spring 中的面向切面编程)。在上述示例中,BookService 中的所有方法都会被考虑,并对其应用缓存通知。
声明式的 XML 缓存支持所有基于注解的模型,因此在这两种方式之间切换应该相当容易。此外,二者可以在同一个应用程序中同时使用。
基于 XML 的方法不会触及目标代码。然而,它本质上更为冗长。当处理包含重载方法且这些方法需要被缓存的类时,由于 method 参数不是一个良好的区分依据,因此要准确识别合适的方法就需要额外的工作量。在这种情况下,您可以使用 AspectJ 切点来精确选择目标方法,并应用相应的缓存功能。
不过,通过 XML 配置,更容易在包、组或接口范围内应用缓存(同样得益于 AspectJ 切点),也更容易创建类似模板的定义(正如我们在前面的示例中通过 cache:definitions 的 cache 属性来定义目标缓存那样)。
8.5. 配置缓存存储
缓存抽象提供了多种存储集成选项。要使用这些选项,您需要声明一个合适的CacheManager(一个用于控制和管理Cache实例的实体,并可用于检索这些实例以进行存储)。
8.5.1. JDKConcurrentMap基于缓存
基于 JDK 的 Cache 实现位于
org.springframework.cache.concurrent 包中。它允许你使用 ConcurrentHashMap
作为底层的 Cache 存储。以下示例展示了如何配置两个缓存:
<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
上述代码片段使用 SimpleCacheManager 创建了一个 CacheManager,用于管理两个名为 ConcurrentMapCache 和 default 的嵌套 books 实例。请注意,每个缓存的名称是直接配置的。
由于缓存由应用程序创建,因此其生命周期与应用程序绑定,适用于基本用例、测试或简单应用程序。该缓存具有良好的可扩展性且速度非常快,但不提供任何管理功能、持久化能力或淘汰策略。
8.5.2. 基于 Ehcache 的缓存
| Ehcache 3.x 完全符合 JSR-107 标准,无需为其提供专门的支持。 |
Ehcache 2.x 的实现位于 org.springframework.cache.ehcache 包中。同样,要使用它,你需要声明相应的 CacheManager。
以下示例展示了如何进行配置:
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
<!-- EhCache library setup -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
此配置通过 ehcache bean 在 Spring IoC 容器内部引导 ehcache 库,然后将其注入到专用的 CacheManager 实现中。请注意,所有特定于 Ehcache 的配置均从 ehcache.xml 文件中读取。
8.5.3. Caffeine 缓存
Caffeine 是 Guava 缓存库基于 Java 8 的重写版本,其实现位于 org.springframework.cache.caffeine 包中,并提供了对 Caffeine 多项特性的访问支持。
以下示例配置了一个按需创建缓存的CacheManager:
<bean id="cacheManager"
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
你也可以显式地指定要使用的缓存。在这种情况下,管理器仅提供这些缓存。以下示例展示了如何实现这一点:
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="caches">
<set>
<value>default</value>
<value>books</value>
</set>
</property>
</bean>
Caffeine 的 CacheManager 还支持自定义的 Caffeine 和 CacheLoader。
有关这些内容的更多信息,请参阅Caffeine 文档。
8.5.4. 基于 GemFire 的缓存
GemFire 是一种以内存为中心、磁盘持久化、弹性可扩展、持续可用的数据库,支持主动模式(内置基于模式的订阅通知),并提供全局复制功能,同时还具备完整的边缘缓存特性。有关如何将 GemFire 用作 CacheManager(及其他更多内容)的详细信息,请参阅Spring Data GemFire 参考文档。
8.5.5. JSR-107 缓存
Spring 的缓存抽象也可以使用符合 JSR-107 规范的缓存。JCache 实现位于 org.springframework.cache.jcache 包中。
同样,要使用它,你需要声明相应的CacheManager。
以下示例展示了如何进行声明:
<bean id="cacheManager"
class="org.springframework.cache.jcache.JCacheCacheManager"
p:cache-manager-ref="jCacheManager"/>
<!-- JSR-107 cache manager setup -->
<bean id="jCacheManager" .../>
8.5.6. 处理无后端存储的缓存
有时,在切换环境或进行测试时,你可能会有缓存声明,但并未配置实际的底层缓存。由于这是一种无效的配置,运行时会抛出异常,因为缓存基础设施无法找到合适的存储。在这种情况下,与其移除缓存声明(这可能很繁琐),不如注入一个简单的虚拟缓存,它不执行任何缓存操作——也就是说,它会强制每次调用被缓存的方法。 以下示例展示了如何实现这一点:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
前面示例中的 CompositeCacheManager 会将多个 CacheManager 实例串联起来,并通过 fallbackToNoOpCache 标志,为所有未被已配置的缓存管理器处理的缓存定义添加一个无操作(no-op)缓存。也就是说,任何在 jdkCache 或 gemfireCache(在示例中先前已配置)中都找不到的缓存定义,都将由这个无操作缓存处理——该缓存不会存储任何信息,从而导致目标方法每次都会被调用。
8.6. 插入不同的后端缓存
显然,市面上有许多缓存产品可用作底层存储。对于那些不支持 JSR-107 的产品,你需要提供一个 CacheManager 和一个 Cache 的实现。这听起来可能比实际情况更复杂,因为在实践中,这些类通常只是简单的适配器,将缓存抽象框架映射到具体的存储 API 上,就像 ehcache 类所做的那样。大多数 CacheManager 类都可以使用 org.springframework.cache.support 包中的类(例如 AbstractCacheManager),它负责处理样板代码,你只需完成实际的映射即可。
9. 附录
9.1. XML 模式
本附录的这一部分列出了与集成技术相关的 XML Schema。
9.1.1.jee架构
jee 元素用于处理与 Java EE(Java 企业版)配置相关的问题,例如查找 JNDI 对象和定义 EJB 引用。
要使用 jee 命名空间中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导声明。以下代码片段中的文本引用了正确的 schema,从而使 jee 命名空间中的元素对您可用:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd">
<!-- bean definitions here -->
</beans>
<jee:jndi-lookup/>(简单)
以下示例展示了如何在不使用 jee 命名空间的情况下,通过 JNDI 查找数据源:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
</bean>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- Spring will do the cast automatically (as usual) -->
<property name="dataSource" ref="dataSource"/>
</bean>
以下示例展示了如何使用 JNDI 通过 jee 命名空间查找数据源:
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- Spring will do the cast automatically (as usual) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<jee:jndi-lookup/>(使用单一 JNDI 环境设置)
以下示例展示了如何在不使用 jee 的情况下,通过 JNDI 查找环境变量:
<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="jndiEnvironment">
<props>
<prop key="ping">pong</prop>
</props>
</property>
</bean>
以下示例展示了如何使用 JNDI 通过 jee 查找环境变量:
<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<jee:environment>ping=pong</jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/>(带有多个 JNDI 环境设置)
以下示例展示了如何在不使用 jee 的情况下,通过 JNDI 查找多个环境变量:
<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="jndiEnvironment">
<props>
<prop key="sing">song</prop>
<prop key="ping">pong</prop>
</props>
</property>
</bean>
以下示例展示了如何使用 JNDI 通过 jee 查找多个环境变量:
<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<!-- newline-separated, key-value pairs for the environment (standard Properties format) -->
<jee:environment>
sing=song
ping=pong
</jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/>(复杂)
以下示例展示了如何使用 JNDI 查找数据源以及多个不同的属性,而不使用 jee:
<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
<property name="cache" value="true"/>
<property name="resourceRef" value="true"/>
<property name="lookupOnStartup" value="false"/>
<property name="expectedType" value="com.myapp.DefaultThing"/>
<property name="proxyInterface" value="com.myapp.Thing"/>
</bean>
以下示例展示了如何使用 JNDI 查找数据源以及通过 jee 查找多个不同的属性:
<jee:jndi-lookup id="simple"
jndi-name="jdbc/MyDataSource"
cache="true"
resource-ref="true"
lookup-on-startup="false"
expected-type="com.myapp.DefaultThing"
proxy-interface="com.myapp.Thing"/>
<jee:local-slsb/>(简单)
<jee:local-slsb/> 元素用于配置对本地 EJB 无状态会话 Bean 的引用。
以下示例展示了如何在不使用 jee 的情况下配置对本地 EJB 无状态会话 Bean 的引用:
<bean id="simple"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>
以下示例展示了如何使用 jee 配置对本地 EJB 无状态会话 Bean 的引用:
<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"/>
<jee:local-slsb/>(复杂)
<jee:local-slsb/> 元素用于配置对本地 EJB 无状态会话 Bean 的引用。
以下示例展示了如何在不使用 jee 的情况下,配置对本地 EJB 无状态会话 Bean 的引用以及若干属性:
<bean id="complexLocalEjb"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.example.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
</bean>
以下示例展示了如何使用 jee 配置对本地 EJB 无状态会话 Bean 的引用以及若干属性:
<jee:local-slsb id="complexLocalEjb"
jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true">
<jee:remote-slsb/>
<jee:remote-slsb/> 元素用于配置对一个 remote EJB 无状态会话 Bean 的引用。
以下示例展示了如何在不使用 jee 的情况下配置对远程 EJB 无状态会话 Bean 的引用:
<bean id="complexRemoteEjb"
class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/MyRemoteBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
<property name="homeInterface" value="com.foo.service.RentalService"/>
<property name="refreshHomeOnConnectFailure" value="true"/>
</bean>
以下示例展示了如何使用 jee 配置对远程 EJB 无状态会话 Bean 的引用:
<jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"
home-interface="com.foo.service.RentalService"
refresh-home-on-connect-failure="true">
9.1.2.jms架构
jms 元素用于配置与 JMS 相关的 Bean,例如 Spring 的消息监听器容器(Message Listener Containers)。这些元素在JMS 章节中题为JMS 命名空间支持(JMS Namespace Support)的部分有详细说明。有关此支持功能及 jms 元素本身的完整详情,请参阅该章节。
为了保证完整性,若要在您的 Spring XML 配置文件中使用 jms 命名空间中的元素,您需要在配置文件顶部包含以下前导声明。以下代码片段中的文本引用了正确的 schema,从而使 jms 命名空间中的元素可供您使用:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd">
<!-- bean definitions here -->
</beans>
9.1.3. 使用<context:mbean-export/>
此元素在配置基于注解的 MBean 导出中有详细说明。
9.1.4.cache架构
您可以使用 cache 元素来启用对 Spring 的 @CacheEvict、@CachePut 和 @Caching 注解的支持。它还支持基于 XML 的声明式缓存。详情请参见
启用缓存注解 和
基于 XML 的声明式缓存。
要使用 cache 命名空间中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导声明。以下代码片段中的文本引用了正确的 schema,以便您可以使用 cache 命名空间中的元素:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- bean definitions here -->
</beans>