集成
1. REST 端点
Spring 框架为调用 REST 端点提供了两种选择:
-
RestTemplate: 原始的 Spring REST 客户端,具有同步的模板方法 API。 -
WebClient: 一个非阻塞的、响应式的替代方案 ,支持同步和异步以及流式处理场景。
从5.0版本开始,RestTemplate处于维护模式,仅接受少量更改请求和错误修复。请考虑使用WebClient,它提供了一个更现代化的API,并支持同步、异步和流式场景。 |
1.1. RestTemplate
RestTemplate 在HTTP客户端库之上提供了更高级的API。它使得在一行代码中调用REST端点变得非常简单。它公开了以下几组重载方法:
| 方法组 | 描述 |
|---|---|
|
通过 GET 方法获取表示。 |
|
通过使用GET方法获取一个<code>0</code>(即状态、标题和正文)。 |
|
通过 HEAD 方法检索资源的所有标头。 |
|
通过使用 POST 创建新资源,并从响应中返回 |
|
通过使用 POST 创建一个新资源,并从响应中返回表示。 |
|
通过使用 POST 创建一个新资源,并从响应中返回表示。 |
|
通过 PUT 方法创建或更新资源。 |
|
通过使用 PATCH 更新资源并从响应中返回表示。
请注意,JDK |
|
通过使用DELETE方法删除指定URI处的资源。 |
|
通过使用 ALLOW 来获取资源允许的 HTTP 方法。 |
|
前面方法的更通用(且更不具倾向性)版本,在需要时提供额外的灵活性。它接受一个 这些方法允许使用 |
|
执行请求的最通用方式,通过回调接口对请求准备和响应提取进行完全控制。 |
1.1.1. 初始化
默认构造函数使用 java.net.HttpURLConnection 来执行请求。您可以使用 ClientHttpRequestFactory 的实现切换到不同的 HTTP 库。
内置支持以下内容:
-
Apache HttpComponents
-
Netty 翻译为:内特
-
OkHttp
例如,要切换到 Apache HttpComponents,可以使用以下方式:
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
每个 ClientHttpRequestFactory 都提供了特定于底层 HTTP 客户端库的配置选项 — 例如,用于凭据、连接池和其他细节。
注意,HTTP 请求的 java.net 实现在访问表示错误(如 401)的响应状态时可能会抛出异常。如果这是个问题,请改用其他 HTTP 客户端库。 |
统一资源标识符
许多 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 属性来自定义 URIs 的编码方式。或者,您可以准备一个 java.net.URI 并将其传递到接受 URI 的其中一个 RestTemplate 方法中。
有关使用和编码URI的更多详细信息,请参阅 URI链接。
标题
您可以使用 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方法变体获取响应头。
1.1.2. 正文
通过 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 注册所有内置的
消息转换器,根据类路径检查来确定哪些可选的转换库存在。您也可以显式设置要使用的消息
转换器。
1.1.3. 消息转换
spring-web 模块包含通过 InputStream 和 OutputStream 读取和写入 HTTP 请求和响应正文的 HttpMessageConverter 接口。
HttpMessageConverter 实例在客户端(例如,在 RestTemplate 中)和服务器端(例如,在 Spring MVC REST 控制器中)使用。
框架中提供了主要媒体(MIME)类型的具体实现,并且默认情况下在客户端注册为RestTemplate,在服务器端注册为RequestMappingHandlerAdapter(参见
配置消息转换器)。
HttpMessageConverter 的实现方法在以下章节中进行了描述。
对于所有转换器,都会使用默认的媒体类型,但您可以通过设置
supportedMediaTypes bean 属性来覆盖它。下表描述了每种实现:
| MessageConverter | 描述 |
|---|---|
|
一个可以从HTTP请求和响应中读取和写入 |
|
一个可以从HTTP请求和响应中读取和写入表单数据的 |
|
一个可以从HTTP请求和响应中读取和写入字节数组的 |
|
一个 |
|
一个可以使用 Jackson 的 |
|
一个可以使用Jackson XML扩展的 |
|
一个可以读取和写入HTTP请求和响应中 |
|
一个可以读取和写入HTTP请求和响应中的 |
1.1.4. 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);
1.1.5. 多部分
要发送多部分数据,您需要提供一个 MultiValueMap<String, Object>,其值可以是 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 的值,则由 FormHttpMessageConverter 将 Content-Type 设置为 multipart/form-data。如果 MultiValueMap 有 String 个值,则 Content-Type 默认设置为 application/x-www-form-urlencoded。
如有需要,也可以显式设置 Content-Type。
1.2. 使用 AsyncRestTemplate(已弃用)
AsyncRestTemplate 已弃用。对于所有可能考虑使用 AsyncRestTemplate 的用例,请改用 WebClient。
2. 远程调用和Web服务
Spring 提供了对多种技术的远程调用支持。 远程调用支持简化了远程启用服务的开发,通过 Java 接口和对象作为输入和输出实现。 目前,Spring 支持以下远程调用技术:
-
Java Web 服务: Spring 通过 JAX-WS 为 Web 服务提供远程支持。
-
AMQP: 通过 AMQP 作为底层协议的远程调用由单独的 Spring AMQP 项目支持。
| 从 Spring Framework 5.3 开始,由于安全原因和更广泛的行业支持,对几种远程处理技术的支持已被弃用。Spring Framework 下一主要版本中将移除相关支持基础设施。 |
以下远程处理技术现已弃用,将不再替换:
-
远程方法调用(RMI): 通过使用
RmiProxyFactoryBean和RmiServiceExporter,Spring 支持传统的 RMI(带有java.rmi.Remote接口和java.rmi.RemoteException)以及通过 RMI 调用器实现的透明远程调用(带有任何 Java 接口)。 -
Spring HTTP Invoker(已弃用): Spring 提供了一种特殊的远程调用策略,可以通过 HTTP 进行 Java 序列化,支持任何 Java 接口(如同 RMI 调用器一样)。相应的支持类是
HttpInvokerProxyFactoryBean和HttpInvokerServiceExporter。 -
Hessian: 通过使用 Spring 的
HessianProxyFactoryBean和HessianServiceExporter,您可以使用 Caucho 提供的轻量级基于 HTTP 的二进制协议透明地公开您的服务。 -
JMS(已弃用): 通过
JmsInvokerServiceExporter和JmsInvokerProxyFactoryBean类在spring-jms模块中支持通过JMS作为底层协议的远程调用。
在讨论 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作为协议的示例。
2.1. AMQP
通过AMQP作为底层协议的远程调用在Spring AMQP项目中得到支持。 有关更多详细信息,请访问 Spring 远程调用 部分的Spring AMQP参考文档。
|
远程接口的自动检测未实现。 自动检测已实现接口的功能在远程接口上不适用的主要原因是避免向远程调用者开放过多的访问途径。目标对象可能实现了内部回调接口,例如 提供一个包含目标所有接口的代理在本地情况下通常无关紧要。然而,当你导出一个远程服务时,应该暴露一个特定的服务接口,该接口具有专为远程使用而设计的操作。除了内部回调接口外,目标可能实现多个业务接口,其中只有一个接口是用于远程暴露的。出于这些原因,我们需要指定这样的服务接口。 这在配置的便利性和意外暴露内部方法的风险之间做出权衡。始终指定一个服务接口并不是太大的负担,并且在特定方法的可控暴露方面会让你更安全。 |
2.2. 选择技术时的注意事项
每一种在这里提到的技术都有其缺点。在选择技术时,您应该仔细考虑您的需求、您提供的服务以及您在通信中发送的对象。
使用 RMI 时,除非你对 RMI 流量进行中继,否则无法通过 HTTP 协议访问这些对象。RMI 是一种较为重量级的协议,因为它支持完整的对象序列化,这在使用需要通过网络进行序列化的复杂数据模型时非常重要。然而,RMI-JRMP 仅适用于 Java 客户端,它是一种 Java 到 Java 的远程调用解决方案。
Spring的HTTP调用器在需要基于HTTP的远程调用但同时也依赖Java序列化的情况下是一个不错的选择。它与RMI调用器共享基本的基础架构,但使用HTTP作为传输协议。请注意,HTTP调用器不仅限于Java到Java的远程调用,也适用于客户端和服务器端都使用Spring的情况。(后者也适用于Spring的RMI调用器用于非RMI接口的情况。)
Hessian在异构环境中操作时可能提供重要价值, 因为它们明确允许非Java客户端。然而,非Java支持仍然有限。 已知的问题包括与延迟初始化集合结合的Hibernate对象的序列化。 如果你有这样的数据模型,建议使用RMI或HTTP调用器而不是Hessian。
JMS 可以用于为服务集群提供支持,并让 JMS 代理负责负载均衡、发现和自动故障转移。默认情况下,Java 序列化用于 JMS 远程调用,但 JMS 提供商可以使用不同的机制进行数据格式化,例如 XStream,以便服务器可以用其他技术实现。
最后但同样重要的是,EJB相较于RMI具有优势,因为它支持标准的角色基于的认证和授权以及远程事务传播。同样可以使用RMI调用者或HTTP调用者来支持安全上下文传播,尽管这并不是核心Spring所提供的功能。Spring仅提供适当的钩子以便于集成第三方或自定义解决方案。
2.3. Java Web 服务
Spring 提供对标准 Java Web 服务 API 的完整支持:
-
使用 JAX-WS 暴露 Web 服务
-
使用 JAX-WS 访问 Web 服务
除了Spring Core中对JAX-WS的原生支持外,Spring产品组合还包含Spring Web Services,这是一个用于契约优先、文档驱动的Web服务的解决方案——对于构建现代的、未来兼容的Web服务来说,是非常推荐的选择。
2.3.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 服务教程。
2.3.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功能(如显式依赖注入)应用于端点实例。通过<code>0</code>的注解驱动注入同样有效。下面的示例展示了如何定义这些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);
}
}
2.3.3. 使用 JAX-WS RI 的 Spring 支持导出 Web 服务
Oracle的JAX-WS RI,作为GlassFish项目的一部分开发,作为其JAX-WS Commons项目的一部分提供Spring支持。这允许将JAX-WS端点定义为Spring管理的Bean,类似于在上一节中讨论的独立模式 — 但这次是在Servlet环境中。
| 这在 Java EE 环境中不可移植。它主要用于非 EE 环境,例如嵌入 JAX-WS RI 作为 Web 应用程序一部分的 Tomcat。 |
与基于servlet的端点的标准导出样式不同之处在于,端点实例的生命周期由Spring管理,并且在web.xml中只定义了一个JAX-WS servlet。按照标准的Java EE样式(如上所示),每个服务端点都有一个servlet定义,每个端点通常通过使用@Autowired(如上所示)委托给Spring beans。
查看 https://jax-ws-commons.java.net/spring/ 了解有关设置和使用方式的详细信息。
2.3.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 在启动时需要此 URL 来创建 JAX-WS 服务。namespaceUri 对应 targetNamespace 在 .wsdl 文件中的内容。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 文档。 |
2.4. RMI(已弃用)
| 从 Spring Framework 5.3 开始,RMI 支持已弃用,将不会被替代。 |
通过使用 Spring 对 RMI 的支持,你可以通过 RMI 基础设施透明地公开你的服务。在设置好之后,你基本上会有一个类似于远程 EJB 的配置,只是没有标准的安全上下文传播或远程事务传播支持。当您使用 RMI 调用器时,Spring 为此类附加调用上下文提供了钩子,因此您可以例如插入安全框架或自定义安全凭据。
2.4.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)。这意味着将使用匿名端口与服务进行通信。 |
2.4.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 来引入它。
2.5. 使用 Hessian 通过 HTTP 远程调用服务(已弃用)
| 从 Spring Framework 5.3 开始,Hessian 支持已弃用,将不会被替代。 |
Hessian 提供了一种基于二进制 HTTP 的远程调用协议。它由 Caucho 开发,您可以在 https://www.caucho.com/ 找到有关 Hessian 本身的更多信息。
2.5.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的Spring容器配置资源(根据您的servlet名称命名),放在WEB-INF目录中。
应用程序上下文将在下一节中使用。
或者,考虑使用Spring的更简单的<code>0</code>。这样可以将远程导出器定义嵌入到根应用程序上下文中(默认情况下在<code>1</code>中),各个servlet定义指向特定的导出器bean。在这种情况下,每个servlet名称需要与目标导出器bean的名称匹配。
2.5.2. 通过 HessianServiceExporter 暴露您的 Bean
在名为 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>
2.5.3. 在客户端链接服务
通过使用 HessianProxyFactoryBean,我们可以在客户端链接服务。同样的原则适用于 RMI 示例。我们创建一个单独的 bean 工厂或应用上下文,并通过使用 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>
2.5.4. 将HTTP基本身份验证应用于通过Hessian公开的服务
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 并设置了一个拦截器,以允许只有管理员和操作员调用此应用程序上下文中提到的 beans。
| 前面的示例没有展示一种灵活的安全基础设施。关于安全方面的更多选项,请查看 Spring Security 项目 at https://spring.io/projects/spring-security/。 |
2.6. Spring HTTP Invoker(已弃用)
| 从 Spring Framework 5.3 开始,HTTP Invoker 支持已弃用,且不会被替代。 |
与Hessian相反,Spring HTTP调用器是轻量级协议,它们使用自己的精简序列化机制,并使用标准的Java序列化机制通过HTTP暴露服务。如果你的参数和返回类型是Hessian使用的序列化机制无法序列化的复杂类型,这将具有巨大优势(有关选择远程处理技术的更多考虑,请参阅下一节)。
在内部,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。 |
2.6.1. 暴露服务对象
为服务对象设置HTTP调用器基础设施与使用Hessian时的方式非常相似。由于Hessian支持提供
HessianServiceExporter,Spring的HttpInvoker支持提供
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。
要在 Spring Web MVC 的 DispatcherServlet 中公开之前提到的 AccountService,需要在调度器的应用程序上下文中进行以下配置,如下例所示:
<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>
此外,您还可以为这个导出器在<code>0</code>中定义一个对应的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>
2.6.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 功能,但您也可以通过设置 httpInvokerRequestExecutor 属性来使用 Apache HttpComponents 客户端。
以下示例显示了如何操作:
<property name="httpInvokerRequestExecutor">
<bean class="org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor"/>
</property>
2.7. JMS(已弃用)
| 从 Spring Framework 5.3 开始,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 基础设施 beans:
<?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>
2.7.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");
}
}
2.7.2. 客户端配置
客户端只需创建一个实现商定接口的客户端代理(CheckingAccountService)。
以下示例定义了您可以注入到其他客户端对象中的 beans(代理会通过 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));
}
}
3. 企业级Bean(EJB)集成
作为轻量级容器,Spring通常被视为EJB的替代品。我们相信,对于许多甚至大多数应用和使用场景,作为容器的Spring,结合其在事务、ORM和JDBC访问方面的丰富支持功能,比通过EJB容器和EJB实现相同功能要好。
然而,需要注意的是,使用Spring并不会阻止你使用EJBs。 实际上,Spring使访问EJBs并实现EJBs及其功能变得更容易。 此外,使用Spring来访问由EJBs提供的服务,允许以后在不更改客户端代码的情况下,透明地在本地EJB、远程EJB或POJO(普通Java对象)版本之间切换这些服务的实现。
在本章中,我们将探讨Spring如何帮助您访问和实现EJB。当访问无状态会话Bean(SLSB)时,Spring特别有用,因此我们首先讨论这个主题。
3.1. 访问 EJB
本节介绍如何访问EJB。
3.1.1. 概念
要调用本地或远程无状态会话Bean上的方法,客户端代码通常必须执行JNDI查找以获取(本地或远程)EJB Home对象,然后对该对象使用create方法调用来获取实际的(本地或远程)EJB对象。然后在EJB上调用一个或多个方法。
为了避免重复的底层代码,许多EJB应用程序使用服务定位器和业务委托模式。这些模式比在客户端代码中到处使用JNDI查找要好,但它们通常的实现方式存在明显的缺点:
-
通常,使用 EJB 的代码依赖于 Service Locator 或 Business Delegate 单例,这使得测试变得困难。
-
在不使用业务代理(Business Delegate)的情况下使用服务定位器(Service Locator)模式, 应用程序代码仍然需要调用EJB的
create()方法并处理由此引发的异常。因此,它仍然依赖于EJB API以及EJB编程模型的复杂性。 -
实现业务委托模式通常会导致大量的代码重复,我们需要编写许多方法,这些方法在调用 EJB 上的相同方法。
Spring 的方法是允许创建和使用代理对象(通常在 Spring 容器中配置),这些对象作为无代码的业务委托。除非你在这样的代码中实际增加了价值,否则不需要编写另一个服务定位器、另一个 JNDI 查找,或在手动编码的业务委托中重复方法。
3.1.2. 访问本地 SLSB
假设我们有一个需要使用本地 EJB 的 Web 控制器。我们遵循最佳实践,使用 EJB 业务方法接口模式,这样 EJB 的本地接口就扩展了一个非 EJB 特定的业务方法接口。我们称这个业务方法接口为 MyComponent。下面的示例显示了这样的接口:
public interface MyComponent {
...
}
使用业务方法接口模式的主要原因之一是确保本地接口和Bean实现类中的方法签名之间的同步是自动的。另一个原因是,如果有必要的话,这使我们以后更容易将服务切换到POJO(普通Java对象)实现。我们还需要实现本地主页接口,并提供一个实现SessionBean和MyComponent业务方法接口的实现类。现在,我们需要编写的唯一Java代码是,在控制器上公开一个类型为MyComponent的setter方法,以将我们的Web层控制器连接到EJB实现。这会将引用保存为控制器中的实例变量。下面的示例展示了如何操作:
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本地主页在启动时被缓存,因此只需一次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容器中,此类通常最好用作单例(没有理由将其作为原型使用)。然而,如果该Bean容器预先实例化单例(如各种XML ApplicationContext变体所做的那样),那么在Bean容器加载之前EJB容器尚未加载目标EJB时,可能会出现问题。这是因为JNDI查找是在该类的init()方法中执行并缓存的,但此时目标位置尚未绑定EJB。解决方法是不要预先实例化这个工厂对象,而是在首次使用时才创建它。在XML容器中,您可以通过使用lazy-init属性来控制此行为。
虽然对大多数Spring用户来说并不重要,但那些使用EJB进行编程AOP工作的用户可能想查看LocalSlsbInvokerInterceptor。
3.1.3. 访问远程 SLSB
访问远程 EJB 与访问本地 EJB 基本相同,只是使用了 SimpleRemoteStatelessSessionProxyFactoryBean 或 <jee:remote-slsb> 配置元素。当然,无论是否使用 Spring,远程调用语义都适用:对另一台计算机上的另一个 VM 中的对象的方法调用在使用场景和故障处理方面有时必须有所不同。
Spring的EJB客户端支持为Spring方法相比非Spring方法增加了另一个优势。
通常,EJB客户端代码在调用本地EJB或远程EJB之间进行切换时会遇到问题。
这是因为远程接口方法必须声明它们抛出 RemoteException,而客户端代码必须处理这一点,
而本地接口方法则不需要。通常需要将为本地EJB编写的客户端代码移动到远程EJB时,
必须修改代码以添加对远程异常的处理,而为远程EJB编写的客户端代码需要移动到本地EJB时,
可以保持不变但会做很多不必要的远程异常处理,或者修改代码以删除这些代码。使用Spring远程EJB代理,
您可以不在业务方法接口和实现EJB代码中声明任何抛出的 RemoteException,
拥有一个与之相同的远程接口(除了它确实抛出 RemoteException),并依靠代理动态地将这两个接口视为相同。
也就是说,客户端代码不必处理已检查的 RemoteException 类。在EJB调用期间实际抛出的 RemoteException 会被重新抛出为非已检查的 RemoteAccessException 类,
该类是 RuntimeException 的子类。然后,您可以随意在本地EJB或远程EJB(甚至普通Java对象)实现之间切换目标服务,
而客户端代码无需知道或关心。当然,这是可选的:没有什么能阻止您在业务接口中声明 RemoteException。
3.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> 设施,在运行时会自动适应实际的组件。如果找到主接口(EJB 2.x 方式),它们会处理主接口;如果无法找到主接口(EJB 3 方式),它们将直接调用组件。
注意:对于 EJB 3 会话 Bean,您也可以有效地使用 JndiObjectFactoryBean /
<jee:jndi-lookup>,因为在普通的 JNDI 查找中会暴露完全可用的组件引用。定义显式的 <jee:local-slsb> 或 <jee:remote-slsb>
查找可以提供一致且更明确的 EJB 访问配置。
4. JMS(Java 消息服务)
Spring 提供了一个 JMS 集成框架,其简化 JMS API 的使用方式与 Spring 对 JDBC API 的集成方式非常相似。
JMS可以大致分为两个功能区域,即消息的生成和消费。JmsTemplate类用于消息生成和同步消息接收。对于类似于Java EE的消息驱动Bean风格的异步接收,Spring提供了若干消息监听器容器,您可以使用它们来创建消息驱动的POJO(MDP)。Spring还提供了一种声明式的方式来创建消息监听器。
org.springframework.jms.core 包提供了使用 JMS 的核心功能。它包含简化 JMS 使用的 JMS 模板类,通过处理资源的创建和释放,就像 JdbcTemplate 对 JDBC 所做的那样。Spring 模板类共有的设计原则是提供辅助方法来执行常见操作,并且对于更复杂的用法,将处理任务的核心部分委托给用户实现的回调接口。JMS 模板遵循相同的設計。這些類為發送消息、同步消費消息以及向用戶公開 JMS 會話和消息生產者提供了多種便利方法。
org.springframework.jms.support 包提供了 JMSException 翻译功能。该翻译将已检查的 JMSException 类层次结构转换为未检查异常的镜像层次结构。如果存在任何特定于提供者的已检查 javax.jms.JMSException 的子类,此异常将被封装在未检查的 UncategorizedJmsException 中。
org.springframework.jms.support.converter 包提供了一个 MessageConverter
抽象,用于在 Java 对象和 JMS 消息之间进行转换。
The org.springframework.jms.support.destination package provides various strategies
for managing JMS destinations, such as providing a service locator for destinations
stored in JNDI.
org.springframework.jms.annotation 包提供了通过使用 @JmsListener 支持注解驱动的监听器端点所需的基础设施
org.springframework.jms.config 包提供了 jms 命名空间的解析器实现,以及用于配置监听器容器和创建监听器端点的 Java 配置支持。
最后,org.springframework.jms.connection 包提供了一个适用于独立应用程序的ConnectionFactory的实现。它还包含 Spring 的 PlatformTransactionManager 对 JMS 的实现(巧妙地命名为 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包放在类路径中,但仅对驱动程序使用与JMS 1.1兼容的API。Spring的JMS支持默认遵循JMS 1.1的约定,因此通过相应的配置它确实支持这种场景。然而,请仅在迁移场景中考虑此方法。 |
4.1. 使用 Spring JMS
本节介绍如何使用Spring的JMS组件。
4.1.1. 使用 JmsTemplate
JmsTemplate 类是 JMS 核心包中的核心类。它简化了 JMS 的使用,因为它在发送或同步接收消息时会处理资源的创建和释放。
使用 JmsTemplate 的代码只需实现提供明确高级契约的回调接口。MessageCreator 回调接口在给定调用代码提供的 Session 时会创建一条消息,该消息位于 JmsTemplate 中。为了支持对 JMS API 更复杂的使用,SessionCallback 提供了 JMS 会话,ProducerCallback 暴露了一个 Session 和 MessageProducer 对。
JMS API 暴露了两种发送方法,一种接受传递模式、优先级和生存时间作为服务质量(QOS)参数,另一种不接受 QOS 参数并使用默认值。由于 JmsTemplate 有多种发送方法,设置 QOS 参数已被暴露为 bean 属性,以避免发送方法数量的重复。同样,同步接收调用的超时值通过使用 setReceiveTimeout 属性来设置。
一些JMS提供程序允许通过配置ConnectionFactory从管理上设置默认QOS值。这会导致对MessageProducer实例的send方法(send(Destination destination, Message message))的调用使用的QOS默认值与JMS规范中指定的不同。为了提供一致的QOS值管理,因此必须通过将布尔属性isExplicitQosEnabled设置为true来专门启用JmsTemplate以使用其自身的QOS值。
为方便起见,JmsTemplate 还提供了一个基本的请求-回复操作,允许在临时队列上发送消息并等待回复,该临时队列是在操作过程中创建的。
JmsTemplate 类的实例是线程安全的,一旦配置完成。这一点很重要,因为这意味着您可以配置一个 JmsTemplate 的实例,然后将这个共享引用安全地注入到多个协作对象中。明确地说,JmsTemplate 是有状态的,因为它维护对 ConnectionFactory 的引用,但此状态不是会话状态。 |
从Spring框架4.1开始,JmsMessagingTemplate建立在JmsTemplate之上,并提供了与消息抽象的集成——即org.springframework.messaging.Message。这使您能够以通用的方式创建要发送的消息。
4.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,它在所有
createConnection() 调用中返回相同的 Connection,并忽略对 close() 的调用。这在测试和独立环境中很有用,以便可以将同一连接用于多个
JmsTemplate 调用,这些调用可能跨越任意数量的事务。SingleConnectionFactory
获取对标准 ConnectionFactory 的引用,通常来自 JNDI。
使用 CachingConnectionFactory
CachingConnectionFactory 扩展了 SingleConnectionFactory 的功能,并添加了 Session、MessageProducer 和 MessageConsumer 实例的缓存。初始缓存大小设置为 1。您可以使用 sessionCacheSize 属性来增加缓存的会话数。请注意,实际缓存的会话数比该数字更多,因为会话是根据其确认模式进行缓存的,因此当 sessionCacheSize 设置为 1 时,最多可以有四个缓存的会话实例(每个确认模式一个)。MessageProducer 和 MessageConsumer 实例在其所属的会话内进行缓存,并在缓存时考虑生产者和消费者独特的属性。MessageProducers 根据其目标地址进行缓存。MessageConsumers 根据由目标地址、选择器、noLocal 传递标志和持久订阅名称(如果创建持久化消费者)组成的键进行缓存。
|
用于临时队列和主题(TemporaryQueue/TemporaryTopic)的消息生产者和消息消费者永远不会被缓存。不幸的是,WebLogic JMS在其常规目标实现上实现了临时队列/主题接口,这会错误地表明其任何目标都不能被缓存。请在WebLogic上使用不同的连接池/缓存,或为WebLogic目的自定义 |
4.1.3. 目标管理
目标对象,作为 ConnectionFactory 实例,是 JMS 管理对象,您可以将其存储和从 JNDI 中检索。在配置 Spring 应用程序上下文时,可以使用 JNDI JndiObjectFactoryBean 工厂类或 <jee:jndi-lookup> 来对您的对象的 JMS 目标引用进行依赖注入。然而,如果应用程序中有大量目标对象,或者如果存在 JMS 提供商特有的高级目标管理功能,这种策略通常会很繁琐。此类高级目标管理的示例包括动态目标的创建或对目标分层命名空间的支持。JmsTemplate 将目标名称的解析委托给实现 DestinationResolver 接口的 JMS 目标对象。DynamicDestinationResolver 是 JmsTemplate 默认使用的实现,并支持解析动态目标。还提供了一个 JndiDestinationResolver,用于作为包含在 JNDI 中的目标的服务定位器,并且可选地回退到 DynamicDestinationResolver 中包含的行为。
在JMS应用程序中,目标通常只能在运行时知道,因此在应用程序部署时无法被管理创建。这通常是因为在交互式系统组件之间有共享的应用程序逻辑,这些组件根据一个众所周知的命名约定在运行时创建目标。尽管动态目标的创建不属于JMS规范的一部分,但大多数提供商都提供了此功能。动态目标使用用户定义的名称进行创建,这使它们与临时目标区分开来,并且通常不会在JNDI中注册。用于创建动态目标的API因提供商而异,因为与目标相关的属性是提供商特定的。
然而,有时提供商会做出一个简单的实现选择,忽略JMS规范中的警告,并使用方法TopicSession
createTopic(String topicName)或QueueSession createQueue(String
queueName)方法来创建具有默认目标属性的新目标。根据提供商的实现,DynamicDestinationResolver也可以创建物理目标,而不仅仅是解析一个目标。
布尔属性 pubSubDomain 用于配置 JmsTemplate,以便了解正在使用的 JMS 域。默认情况下,此属性的值为 false,表示将使用点对点域 Queues。此属性(由 JmsTemplate 使用)通过 DestinationResolver 接口的实现确定动态目标解析的行为。
您也可以通过属性 JmsTemplate 将 defaultDestination 配置为默认目标。默认目标是指不引用特定目标的发送和接收操作。
4.1.4. 消息监听器容器
在EJB世界中,JMS消息最常见的用途是驱动消息驱动
Bean(MDB)。Spring提供了一种解决方案,可以创建消息驱动的POJO(MDP),而不会将用户绑定到EJB容器。(有关Spring对MDP支持的详细内容,请参阅异步接收:消息驱动的POJO。)自Spring框架4.1以来,端点方法可以用@JmsListener进行注解——有关更多细节,请参阅基于注解的监听器端点。
一个消息监听器容器用于从JMS消息队列接收消息并驱动注入到其中的MessageListener。监听器容器负责所有消息接收的线程处理,并将消息分发给监听器进行处理。消息监听器容器是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环境的兼容性之间取得了良好的平衡。
你可以自定义容器的缓存级别。请注意,当未启用缓存时,每次接收消息都会创建一个新的连接和一个新的会话。将此与高负载下的非持久化订阅结合使用可能导致消息丢失。在这种情况下,请确保使用适当的缓存级别。
此容器在消息代理关闭时也具有可恢复的功能。默认情况下,一个简单的 BackOff 实现每五秒重试一次。您可以指定一个自定义的 BackOff 实现以获得更精细的恢复选项。有关示例,请参阅 ExponentialBackOff。
与其兄弟(SimpleMessageListenerContainer)类似,
DefaultMessageListenerContainer 支持原生的 JMS 事务,并允许自定义确认模式。如果您的场景可行,建议优先使用内部管理的事务——也就是说,如果您可以接受在 JVM 崩溃时偶尔出现重复消息的情况。您可以在业务逻辑中自定义重复消息检测步骤来处理此类情况——例如,通过检查业务实体是否存在或检查协议表。任何此类安排都比另一种方法更高效:通过将您的
DefaultMessageListenerContainer 配置为使用 JtaTransactionManager,将整个处理过程包装在 XA 事务中,以涵盖 JMS 消息的接收以及消息监听器中的业务逻辑执行(包括数据库操作等)。 |
默认的 AUTO_ACKNOWLEDGE 模式不提供适当的可靠性保证。
当监听器执行失败时消息可能会丢失(因为提供者在监听器调用后自动确认每条消息,没有异常可以传播到提供者)或者当监听器容器关闭时(可以通过设置 acceptMessagesWhileStopping 标志来配置)。如果需要可靠性,请使用事务性会话(例如,用于可靠队列处理和持久主题订阅)。 |
4.1.5. 事务管理
Spring 提供了一个 JmsTransactionManager,用于管理单个 JMS 的事务
ConnectionFactory。这使得 JMS 应用程序可以利用 Spring 的托管事务
功能,如在
数据访问章节的事务管理部分 所描述的那样。
JmsTransactionManager 执行本地资源事务,将指定的 ConnectionFactory 中的 JMS
连接/会话对绑定到线程。
JmsTemplate 会自动检测这些事务性资源并相应地操作它们。
在 Java EE 环境中,ConnectionFactory 会池化 Connection 和 Session 实例,
因此这些资源可以在事务之间高效地重复使用。在独立环境中,
使用 Spring 的 SingleConnectionFactory 会导致共享的 JMS Connection,每个事务都有自己的独立 Session。或者,可以考虑使用提供商特定的池化适配器,例如 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一起时,模板始终会获得一个事务性JMSSession。
4.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) 会将消息发送到该目标。
4.2.1. 使用消息转换器
为了便于发送领域模型对象,JmsTemplate提供了多种send方法,这些方法将Java对象作为消息数据内容的参数。重载方法convertAndSend()和receiveAndConvert()在JmsTemplate中将转换过程委托给MessageConverter接口的一个实例。此接口定义了在Java对象和JMS消息之间进行转换的简单契约。默认实现(SimpleMessageConverter)支持String和TextMessage、byte[]和BytesMessage以及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}
}
}
4.3. 接收消息
这描述了如何在Spring中使用JMS接收消息。
4.3.1. 同步接收
虽然JMS通常与异步处理相关联,但你可以同步地接收消息。重载的<code>0</code>方法提供了此功能。在同步接收过程中,调用线程会阻塞直到消息可用。这可能是一个危险的操作,因为调用线程可能会被无限期地阻塞。<code>1</code>属性指定接收器在放弃等待消息之前应该等待的时间。
4.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>
查看各种消息监听器容器的 Spring javadoc(所有容器均实现 MessageListenerContainer) 以获得每个实现支持的功能的完整描述。
4.3.3. 使用 SessionAwareMessageListener 接口
SessionAwareMessageListener 接口是一个 Spring 特有的接口,它提供了与 JMS MessageListener 接口类似的契约,但还允许消息处理方法访问接收 Message 的 JMS Session。
以下列表显示了 SessionAwareMessageListener 接口的定义:
package org.springframework.jms.listener;
public interface SessionAwareMessageListener {
void onMessage(Message message, Session session) throws JMSException;
}
您可以选择让您的MDP实现此接口(优先于标准的JMS MessageListener接口),如果您希望您的MDP能够响应任何接收到的消息(通过使用在Session中提供的onMessage(Message, Session)方法)。Spring自带的所有消息监听器容器实现都支持实现MessageListener或SessionAwareMessageListener接口的MDP。实现SessionAwareMessageListener接口的类有一个注意事项,即它们将通过接口与Spring绑定。是否使用该接口的选择完全由您作为应用程序开发人员或架构师决定。
请注意,onMessage(..)接口的SessionAwareMessageListener方法会抛出JMSException。与标准JMS的MessageListener接口不同,当使用SessionAwareMessageListener接口时,客户端代码需要处理任何抛出的异常。
4.3.4. 使用 MessageListenerAdapter
MessageListenerAdapter 类是 Spring 异步消息传递支持中的最终组件。简而言之,它允许您将几乎任何类作为 MDP(尽管有一些限制)公开。
考虑以下接口定义:
public interface MessageDelegate {
void handleMessage(String message);
void handleMessage(Map message);
void handleMessage(byte[] message);
void handleMessage(Serializable message);
}
请注意,尽管该接口既不继承 MessageListener 也不继承 SessionAwareMessageListener 接口,您仍然可以通过使用 MessageListenerAdapter 类将其用作 MDP。还要注意各种消息处理方法是如何根据它们可以接收和处理的各个 Message 类型的内容进行强类型的。
现在考虑以下 MessageDelegate 接口的实现:
public class DefaultMessageDelegate implements MessageDelegate {
// implementation elided for clarity...
}
特别是要注意前面实现的 MessageDelegate 接口(DefaultMessageDelegate 类)完全没有 JMS 依赖。它确实是一个 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>
下一个示例显示了另一个只能处理接收JMS
TextMessage消息的MDP。请注意消息处理方法实际上被调用为
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接收到一个类型不是TextMessage的JMS Message,则会抛出一个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 会被发送到 JMS Reply-To 属性中定义的 Destination(如果存在的话),该属性来自原始 Message 或者在 MessageListenerAdapter 上设置的默认 Destination(如果已配置的话)。
如果没有找到 Destination,则会抛出 InvalidDestinationException(注意,此异常不会被吞没,并会传播到调用堆栈中)。
4.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 | 我们的事务管理器。 |
4.4. 对 JCA 消息端点的支持
从版本2.5开始,Spring还提供了对基于JCA的MessageListener容器的支持。 JmsMessageEndpointManager 会尝试从提供者的ResourceAdapter类名中自动确定ActivationSpec类名。 因此,通常可以提供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 或更高版本)中,您可以通过使用 <jee:jndi-lookup> 从 JNDI 获取整个 ResourceAdapter 对象。基于 Spring 的消息监听器可以与服务器上的 ResourceAdapter 进行交互,该对象也使用服务器内置的 WorkManager。
查看 Javadoc 中 JmsMessageEndpointManager,
JmsActivationSpecConfig,
和 ResourceAdapterFactoryBean
以获取更多信息。
Spring 还提供了一个通用的 JCA 消息端点管理器,它不依赖于 JMS:
org.springframework.jca.endpoint.GenericMessageEndpointManager。此组件允许
使用任何消息监听器类型(例如 JMS MessageListener)和任何
提供商特定的 ActivationSpec 对象。有关您的连接器的实际功能,请参阅您的 JCA 提供商的文档,并查看
GenericMessageEndpointManager
Spring 特定的配置细节的 javadoc。
| 基于JCA的消息端点管理与EJB 2.1消息驱动Bean非常相似。 它使用相同的底层资源提供者契约。与EJB 2.1 MDB类似,你也可以在Spring上下文中使用JCA提供者支持的任何消息监听器接口。 然而,Spring仍然为JMS提供了明确的“便利”支持,因为JMS是与JCA端点管理契约一起使用的最常见端点API。 |
4.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 目标与同一方法关联。 |
4.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 基础设施设置),您可以调用 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>
4.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 以编程方式仅注册您的端点。
4.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注解参数来明确这一点。您还可以通过添加额外的@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;
}
}
4.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值,例如优先级或生存时间,可以相应地配置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;
}
}
4.6. JMS 命名空间支持
Spring 提供了一个 XML 命名空间,用于简化 JMS 配置。要使用 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 模式。 |
命名空间包含三个顶级元素:<code>0</code>、<code>1</code> 和<code>2</code>。<code>3</code> 用于启用 <a t="C7">基于注解的监听器端点</a>。<code>4</code> 和 <code>5</code> 定义共享的监听器容器配置,并可以包含 <code>6</code> 个子元素。 以下示例显示了两个监听器的基本配置:
<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 设置和资源引用进行自定义。通过使用这些属性,您可以在仍受益于命名空间便利性的同时,定义高度自定义的监听器容器。
您可以通过对 id 属性指定要通过 factory-id 属性公开的 bean 的 JmsListenerContainerFactory,从而自动将此类设置公开。
如下面的示例所示:
<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 还提供了关于事务选择和消息重新传递场景的讨论。
| 属性 | 描述 |
|---|---|
|
此监听器容器的类型。可用选项为 |
|
作为全限定类名的自定义监听器容器实现类。
默认情况下,根据 |
|
将此元素定义的设置作为 |
|
对 JMS |
|
对Spring |
|
对用于解析JMS |
|
对将JMS消息转换为监听器方法参数的 |
|
对在 |
|
此监听器的JMS目标类型: |
|
响应的JMS目标类型: |
|
此监听器容器的 JMS 客户端 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 |
|
为每个监听器启动的并发会话或消费者数量。它可以是一个简单的数字,表示最大数量(例如 |
|
单个会话中要加载的最大消息数。请注意,提高此数值可能导致并发消费者出现饥饿现象。 |
5. JMX
Spring 中的 JMX(Java 管理扩展)支持提供了功能,使您能够轻松且透明地将您的 Spring 应用程序集成到 JMX 基础架构中。
具体来说,Spring的JMX支持提供了四个核心功能:
-
任何Spring Bean作为JMX MBean的自动注册。
-
一个用于控制你的Bean管理接口的灵活机制。
-
通过远程 JSR-160 连接器对 MBeans 的声明式暴露。
-
本地和远程 MBean 资源的简单代理。
这些功能设计为无需将应用程序组件耦合到Spring或JMX接口和类中。实际上,大部分情况下,您的应用程序类无需了解Spring或JMX即可利用Spring JMX功能。
5.1. 将您的 Bean 导出到 JMX
Spring 的 JMX 框架中的核心类是 MBeanExporter。该类负责将您的 Spring beans 注册到 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 属性都会作为属性公开,所有 public 方法(除从 Object 类继承的方法外)都会作为操作公开。
MBeanExporter 是一个 Lifecycle bean(参见 启动和关闭回调)。默认情况下,MBeans 会在应用程序生命周期尽可能晚的时候导出。您可以配置 phase 来确定导出的时间点,或者通过设置 autoStartup 标志来禁用自动注册。 |
5.1.1. 创建 MBeanServer
在上一节中显示的配置假设应用程序正在一个已经运行了一个(且仅有一个)MBeanServer的环境中运行。在这种情况下,Spring会尝试找到正在运行的MBeanServer,并将您的bean注册到该服务器(如果有的话)。当您的应用程序在具有自己MBeanServer的容器(如Tomcat或IBM WebSphere)中运行时,这种行为很有用。
然而,在独立环境中或在不提供MBeanServer的容器中运行时,这种方法没有用处。为了解决这个问题,可以通过将org.springframework.jmx.support.MBeanServerFactoryBean类的实例添加到配置中,以声明方式创建一个MBeanServer实例。
您还可以通过将MBeanExporter实例的server属性设置为由MBeanServerFactoryBean返回的MBeanServer值,来确保使用特定的MBeanServer,如下例所示:
<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 的实例,并通过 server 属性提供给 MBeanExporter。当你提供自己的 MBeanServer 实例时,MBeanExporter 不会尝试查找正在运行的 MBeanServer,而是使用提供的 MBeanServer 实例。为了正确工作,你必须在类路径上有 JMX 实现。
5.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,应使用 工厂方法,如下例所示:
<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>
5.1.3. 懒初始化 MBeans
如果使用一个配置了 MBeanExporter 的 Bean,该 Bean 也配置为延迟初始化,那么 MBeanExporter 不会破坏此契约,并避免实例化该 Bean。相反,它会将代理注册到 MBeanServer,并推迟从容器中获取 Bean 直到第一次调用代理时。
5.1.4. MBeans 的自动注册
通过 MBeanExporter 导出的任何 bean,如果已经是有效的 MBean,则会直接注册到 MBeanServer,而无需 Spring 的进一步干预。您可以通过将 autodetect 属性设置为 true,使 MBeanExporter 自动检测 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 实例 中所述。
5.1.5. 控制注册行为
考虑这样一个场景,其中 Spring MBeanExporter 尝试通过使用 ObjectName bean:name=testBean1 注册一个 MBean,带有 MBeanServer。如果该相同的 ObjectName 下已经注册了一个 MBean 实例,则默认行为是失败(并抛出 InstanceAlreadyExistsException)。
您可以精确控制当一个 MBean 与一个 MBeanServer 注册时发生的情况。Spring 的 JMX 支持允许三种不同的注册行为,用于在注册过程中发现同一 ObjectName 下已注册了一个 MBean 时,控制注册行为。下表总结了这些注册行为:
| 注册行为 | 说明 |
|---|---|
|
这是默认的注册行为。如果在相同的 |
|
如果在相同的 |
|
如果在相同的 |
前面表格中的值定义在RegistrationPolicy类上。
如果您想更改默认的注册行为,需要将您的MBeanExporter定义上的registrationPolicy属性的值设置为这些值之一。
以下示例显示了如何从默认注册行为更改为 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>
5.2. 控制您的 Bean 的管理接口
在上一节的示例中,
您对Bean的管理接口的控制很少。每个导出Bean的所有public
属性和方法分别作为JMX属性和操作公开。为了更精细地控制哪些
属性和方法实际上作为JMX属性和操作公开,Spring JMX提供了全面且可扩展的机制来
控制您的Bean的管理接口。
5.2.1. 使用 MBeanInfoAssembler 接口
在幕后,MBeanExporter 会委托给 org.springframework.jmx.export.assembler.MBeanInfoAssembler 接口的一个实现,该接口负责定义每个暴露的 bean 的管理接口。
默认实现 org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler 定义了一个管理接口,该接口公开所有公共属性和方法(如前面各节中的示例所示)。Spring 提供了 MBeanInfoAssembler 接口的另外两个实现,它们允许您通过使用源级元数据或任何任意接口来控制生成的管理接口。
5.2.2. 使用源代码级元数据:Java 注解
通过使用 MetadataMBeanInfoAssembler,您可以使用源级元数据定义 beans 的管理接口。元数据的读取由 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必须是公共的,以及暴露操作或属性的方法也必须是公共的。 |
以下示例显示了我们在 创建 MBeanServer 中使用的 JmxTestBean 类的注释版本:
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()方法则没有。这导致当你使用MetadataMBeanInfoAssembler时,管理接口中只包含一个操作(add(int, int))。
以下配置展示了如何配置 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 类的实例,并通过装配属性传递给 MBeanExporter。这就是利用 Spring 暴露的 MBeans 的元数据驱动管理接口所需的一切。
5.2.3. 源代码级别元数据类型
下表描述了可用于 Spring JMX 的源级别元数据类型:
| 目的 | 注解 | 注解类型 |
|---|---|---|
将所有 |
|
类 |
将方法标记为 JMX 操作。 |
|
方法 |
将getter或setter标记为JMX属性的一半。 |
|
方法(仅getter和setter) |
为操作参数定义描述。 |
|
方法 |
下表描述了可用于这些源级元数据类型的配置参数:
| 参数 | 描述 | 适用于 |
|---|---|---|
|
由 |
|
|
设置资源、属性或操作的友好描述。 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置 |
|
|
设置操作参数的显示名称。 |
|
|
设置操作参数的索引。 |
|
5.2.4. 使用 AutodetectCapableMBeanInfoAssembler 接口
为了进一步简化配置,Spring包含了一个
AutodetectCapableMBeanInfoAssembler 接口,该接口扩展了 MBeanInfoAssembler
接口以添加对MBean资源的自动检测支持。如果您将
MBeanExporter 配置为 AutodetectCapableMBeanInfoAssembler 的实例,则可以“投票”决定是否将 beans 包含到 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 的名称现在具有业务含义。您可以通过更改 ObjectName 创建的默认行为来解决此问题,如 控制 ObjectName 实例的创建 中所定义的那样。
5.2.5. 使用 Java 接口定义管理接口
除了 MetadataMBeanInfoAssembler 之外,Spring 还包括
InterfaceBasedMBeanInfoAssembler,它可以根据一组接口中定义的方法来限制公开的方法和属性。
尽管暴露MBeans的标准机制是使用接口和简单的命名方案,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>
在前面的示例中,当为任何 bean 构建管理接口时,InterfaceBasedMBeanInfoAssembler 被配置为使用 IJmxTestBean 接口。理解这一点很重要:由 InterfaceBasedMBeanInfoAssembler 处理的 bean 不需要实现用于生成 JMX 管理接口的接口。
在前面的情况下,IJmxTestBean 接口用于构建所有 bean 的管理接口。在许多情况下,这不是期望的行为,您可能希望为不同的 bean 使用不同的接口。在这种情况下,可以通过 interfaceMappings 属性传递一个 Properties 实例,其中每个条目的键是 bean 名称,每个条目的值是用于该 bean 的接口名称的逗号分隔列表。
如果通过managedInterfaces或interfaceMappings属性未指定管理接口,InterfaceBasedMBeanInfoAssembler将对bean进行反射,并使用该bean实现的所有接口来创建管理接口。
5.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 名称映射到方法名称列表。
5.3. 为您的 Bean 控制 ObjectName 实例
在幕后,MBeanExporter 会将任务委托给 ObjectNamingStrategy 的实现,以获取每个它注册的 bean 的 ObjectName 实例。
默认情况下,默认实现 KeyNamingStrategy 使用 beans Map 的键作为 ObjectName。此外,KeyNamingStrategy 可以将 beans Map 的键映射到 Properties 文件(或文件)中的条目来解析 ObjectName。除了 KeyNamingStrategy 外,Spring 还提供了两个额外的 ObjectNamingStrategy 实现: IdentityNamingStrategy(根据 bean 的 JVM 身份构建 ObjectName)和 MetadataNamingStrategy(使用源级元数据来获取 ObjectName)。
5.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实例,该实例由映射属性定义的Properties实例和映射属性中定义路径的属性文件合并而成。在此配置中,testBean bean 被赋予一个ObjectName,其值为bean:name=testBean1,因为这是与 bean 键对应的Properties实例中的条目。
如果在 Properties 实例中找不到条目,则使用 bean 键名称作为 ObjectName。
5.3.2. 使用 MetadataNamingStrategy
MetadataNamingStrategy 使用 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"/>
5.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"/>
不要将基于接口的AOP代理与你的bean类中的JMX注解自动检测功能结合使用。基于接口的代理会“隐藏”目标类,这也会隐藏JMX管理的资源注解。因此,在这种情况下,你应该使用基于目标类的代理(通过在<aop:config/>、<tx:annotation-driven/>等上设置'proxy-target-class'标志)。否则,你的JMX bean可能在启动时被静默忽略。 |
5.4. 使用 JSR-160 连接器
对于远程访问,Spring JMX模块在org.springframework.jmx.support包中提供了两种FactoryBean实现,用于创建服务器端和客户端连接器。
5.4.1. 服务器端连接器
要让Spring JMX创建、启动并公开一个JSR-160 JMXConnectorServer,可以使用以下配置:
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"/>
默认情况下,ConnectorServerFactoryBean 创建一个绑定到 service:jmx:jmxmp://localhost:9875 的 JMXConnectorServer。因此,serverConnector bean 通过 JMXMP 协议在本地主机的端口 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)以便完成名称注册。如果您通过RMI让Spring为您导出远程服务,Spring已经
构建了一个RMI注册表。如果没有,您可以使用以下配置片段轻松启动一个注册表:
<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
<property name="port" value="1099"/>
</bean>
5.4.2. 客户端连接器
要创建一个到远程 JSR-160 启用的 MBeanServerConnection,可以使用
MBeanServerConnectionFactoryBean,如下例所示:
<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
<property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>
5.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 文档。
5.5. 通过代理访问MBeans
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下注册的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>
在前面的示例中,我们创建了一个指向使用 MBeanServerConnectionFactoryBean 的远程计算机的 MBeanServerConnection。此 MBeanServerConnection 然后通过 server 属性传递给 MBeanProxyFactoryBean。创建的代理通过此 MBeanServerConnection 将所有调用转发到 MBeanServer。
5.6. 通知
Spring的JMX功能包括对JMX通知的全面支持。
5.6.1. 为通知注册监听器
Spring的JMX支持使注册任意数量的
NotificationListeners 与任意数量的MBeans(这包括由Spring的MBeanExporter导出的MBeans以及通过其他机制注册的MBeans)变得非常容易。例如,考虑这样一个场景:每次目标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(bean:name=testBean1)广播一个JMX Notification时,通过notificationListenerMappings属性注册为监听器的ConsoleLoggingNotificationListener 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以及它要注册到的ObjectName(或ObjectNames)的MBeanServer。NotificationListenerBean还封装了其他一些属性,例如NotificationFilter和任意的handback对象,该对象可以在高级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时都获得一个回调对象,并且希望通过提供NotificationFilter来过滤掉多余的Notifications。下面的示例实现了这些目标:
<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对象的完整讨论,以及实际上有关NotificationFilter的讨论,请参阅JMX规范(1.2)中名为“JMX通知模型”的章节。)
5.6.2. 发布通知
Spring 不仅支持注册以接收 Notifications,还支持发布 Notifications。
本节仅适用于通过 MBeanExporter 暴露为 MBeans 的 Spring 管理的 Bean。任何现有的用户定义的 MBeans 应该使用标准的 JMX API 进行通知发布。 |
Spring的JMX通知发布支持中的关键接口是
NotificationPublisher接口(在
org.springframework.jmx.export.notification包中定义)。任何要通过
MBeanExporter实例导出为MBean的bean都可以实现相关的
NotificationPublisherAware接口,以获得对
NotificationPublisher
实例的访问权限。该
NotificationPublisherAware接口通过一个简单的setter方法向实现该接口的bean提供一个
NotificationPublisher的实例,该bean随后可以发布
Notifications。
如 javadoc 中所述,
NotificationPublisher
接口,通过 NotificationPublisher
机制发布事件的托管 Bean 不负责通知监听器的状态管理。
Spring 的 JMX 支持会处理所有 JMX 基础设施问题。
作为应用程序开发人员,您需要实现
NotificationPublisherAware 接口并通过使用提供的
NotificationPublisher 实例开始发布事件。请注意,
NotificationPublisher
是在托管 Bean 注册到 MBeanServer 之后设置的。
使用 NotificationPublisher 实例非常简单。您需要创建一个 JMX
Notification 实例(或适当的 Notification 子类的实例),
将与要发布的事件相关的数据填充到通知中,
然后在 sendNotification(Notification) 上调用
NotificationPublisher 实例,并传入 Notification。
在下面的示例中,导出的 JmxTestBean 实例在每次调用 add(int, int) 操作时发布一个 NotificationEvent:
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 的耦合,那么就可以这么做。
5.7. 更多资源
本节包含有关JMX的其他资源链接:
-
The JMX 主页 at Oracle.
-
Java JMX 规范 (JSR-000003).
-
The JMX Remote API specification (JSR-000160).
-
该 MX4J 主页。(MX4J 是各种 JMX 规范的开源实现。)
6. 邮件
本节介绍如何使用 Spring 框架发送电子邮件。
Spring框架提供了一个有助于发送电子邮件的实用工具库,它使您免于处理底层邮件系统的细节,并代表客户端负责低级别的资源管理。
org.springframework.mail 包是 Spring 框架电子邮件支持的根级包。发送电子邮件的核心接口是 MailSender
接口。一个简单的值对象,用于封装简单邮件的属性,如 from 和 to(以及许多其他属性)的是 SimpleMailMessage 类。此包还包含一组检查异常,这些异常为底层邮件系统异常提供了更高层次的抽象,根异常是 MailException。有关丰富的邮件异常层次结构的更多信息,请参阅 javadoc。
The org.springframework.mail.javamail.JavaMailSender interface adds specialized
JavaMail features, such as MIME message support to the MailSender interface
(from which it inherits). JavaMailSender also provides a callback interface called
org.springframework.mail.javamail.MimeMessagePreparator for preparing a 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. 使用 JavaMail MimeMessageHelper
在处理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 添加到 MimeMessage 中(在上面的例子中为 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
(自 1.3 版本起包含在 JDK 中)和 Quartz 调度器( https://www.quartz-scheduler.org/ )进行调度。
您可以使用带有对 FactoryBean 的可选引用的 FactoryBean 来设置这两个调度器,
分别引用 Timer 或 Trigger 实例。此外,还提供了一个方便的类,用于 Quartz 调度器和 Timer,
该类允许您调用现有目标对象的方法(类似于正常的 MethodInvokingFactoryBean
操作)。
7.1. Spring TaskExecutor 抽象
线程池在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: 此实现最常被使用。它公开 bean 属性以配置一个java.util.concurrent.ThreadPoolExecutor并将其包装在一个TaskExecutor中。 如果您需要适应不同类型的java.util.concurrent.Executor,我们建议您改用ConcurrentTaskExecutor。 -
WorkManagerTaskExecutor: 此实现使用一个CommonJWorkManager作为其后备服务提供者 并且是设置基于CommonJ的线程池 集成在Spring应用程序上下文中的核心便利类,适用于WebLogic或WebSphere。 -
DefaultManagedTaskExecutor: 此实现在一个与JSR-236兼容的运行时环境(如Java EE 7+应用服务器)中使用通过JNDI获取的ManagedExecutorService,以此替换用于该目的的CommonJ WorkManager。
7.1.2. 使用 TaskExecutor
Spring 的 TaskExecutor 实现用作简单的 JavaBeans。在下面的示例中,
我们定义了一个使用 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. Spring TaskScheduler 抽象
除了 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。
这会导致任务在指定时间后运行一次。所有其他方法都可以安排任务重复运行。固定速率和固定延迟的方法适用于简单的周期性执行,但接受一个Trigger的方法要灵活得多。
7.2.1. Trigger 接口
Trigger 接口本质上是受 JSR-236 启发的,截至 Spring 3.0 时,它尚未被正式实现。 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,它接受一个固定周期、一个可选的初始延迟值以及一个布尔值,以指示该周期应被解释为固定频率还是固定延迟。由于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,会提供一个执行器引用,用于处理带有@Async注解的方法,以及提供一个调度器引用,用于管理带有@Scheduled注解的方法。
默认的处理 @Async 注解的建议模式是 proxy,这允许通过代理拦截调用。同一类中的本地调用无法以这种方式被拦截。为了更高级的拦截模式,可以考虑结合编译时或加载时织入,切换到 aspectj 模式。 |
7.3.2. @Scheduled 注解
您可以将 @Scheduled 注解添加到一个方法中,同时添加触发器元数据。例如,以下方法每隔五秒(5000 毫秒)以固定延迟调用,这意味着每次先前调用完成之后开始计算周期。
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// something that should run periodically
}
|
默认情况下,固定延迟、固定速率和初始延迟值将使用毫秒作为时间单位。如果您希望使用其他时间单位,例如秒或分钟,可以通过 例如,前面的示例也可以如下编写。
|
如果您需要固定速率执行,可以在注解中使用fixedRate属性。以下方法每隔五秒被调用一次(从每次调用的开始时间之间计算)。
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
对于固定延迟和固定速率的任务,您可以通过指示在方法第一次执行之前等待的时间量来指定初始延迟,如下所示的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 开始,支持任何作用域的 bean 上的 确保在运行时没有初始化同一注解类的多个实例,除非您确实希望为每个此类实例安排回调。与此相关的是,确保不要在使用 |
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();
}
}
没有直接的XML等价物用于@Async,因为此类方法首先应设计为异步执行,而不是外部重新声明为异步。
但是,您可以手动设置Spring的AsyncExecutionInterceptor与Spring AOP结合使用,
再加上一个自定义的切点。 |
7.3.4. 使用 @Async 进行执行器资格评定
默认情况下,当在方法上指定@Async时,使用的执行器是
启用异步支持时配置的
执行器,即如果你使用的是XML,则为“基于注解的”元素,或者如果你有任何AsyncConfigurer实现的话。但是,当你需要指示在执行给定方法时应使用除默认值以外的执行器时,可以使用value属性的@Async
注解。下面的示例显示了如何做到这一点:
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
在这种情况下,"otherExecutor"可以是Spring容器中任何Executor bean的名称,或者是与任何Executor相关联的限定符的名称(例如,如<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。此外,如果队列是无界的,那么最大大小将完全不起作用。由于执行器总是先尝试队列,然后再创建超过核心大小的新线程,因此为了使线程池超出核心大小,队列必须具有有限的容量(这就是为什么使用无界队列时,固定大小的池才是唯一合理的案例)。
考虑上面提到的情况,当一个任务被拒绝时。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个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中的“方法调用器”类似,例如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-rate,表示无论之前执行花费多长时间,方法应运行的频率。此外,对于fixed-delay和fixed-rate任务,您可以指定一个“initial-delay”参数,表示在方法第一次执行前要等待的毫秒数。为了更精确的控制,您可以改用cron属性来提供一个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. Cron 表达式
所有 Spring cron 表达式都必须符合相同的格式,无论您是在
@Scheduled 注解,
task:scheduled-tasks 元素,
或其他地方使用。
一个格式正确的 cron 表达式,例如 * * * * * *,由六个以空格分隔的时间和日期字段组成,每个字段都有自己的有效值范围:
┌───────────── second (0-59) │ ┌───────────── minute (0 - 59) │ │ ┌───────────── hour (0 - 23) │ │ │ ┌───────────── day of the month (1 - 31) │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) │ │ │ │ │ ┌───────────── day of the week (0 - 7) │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) │ │ │ │ │ │ * * * * * *
有一些规则适用:
-
一个字段可以是星号(
*),它始终表示“第一个到最后一个”。 对于日期或星期几的字段,可以用问号(?)代替星号。 -
逗号(
,)用于分隔列表中的项目。 -
两个用短横线(
-)分隔的数字表示一个数字范围。 指定的范围是包含在内的。 -
在范围(或
*)后使用/表示通过该范围指定数字值的区间。 -
英文名称也可以用于月份和星期几字段。 使用特定的星期几或月份的前三个字母(大小写无关)。
-
月份的日期和星期几字段可以包含一个
L字符,其含义不同-
在日期字段中,
L表示 月份的最后一天。 如果后面跟着一个负偏移量(即,L-n),它表示n号前的最后一天。 -
在星期几字段中,
L表示 一周的最后一天。 如果前面带有数字或三个字母的名称(dL或DDDL),则表示 当月的最后一天(d或DDD)。
-
-
日期字段可以是
nW,表示最接近月份第n日的星期几。 如果n是星期六,这将返回之前的星期五。 如果n是星期日,这将返回之后的星期一,当n是1且是星期六时也会发生这种情况(即:1W表示月份的第一个星期几)。 -
如果日期字段为
LW,则表示 该月的最后一个工作日。 -
星期几字段可以是
d#n(或DDD#n),表示 该月的第n天星期d(或DDD)。
这里是一些示例:
| cron 表达式 | 含义 |
|---|---|
|
每小时的开始 |
|
每十秒 |
|
8、9 和 10 点钟每天 |
|
早上6点和下午7点每天 |
|
8:00,8:30,9:00,9:30,10:00 和 10:30 每天 |
|
每小时工作日的九点到五点 |
|
每个圣诞节的午夜 |
|
当月最后一天的午夜 |
|
本月最后一天前的第三天午夜 |
|
每月最后一个星期五午夜 |
|
每月最后一个星期四的午夜 |
|
每月第一个星期一的午夜 |
|
当月最后一个工作日的午夜 |
|
每月的第二个星期五午夜 |
|
每月第一个星期一的午夜 |
7.5.1. 宏
像 0 0 * * * * 这样的表达式对人类来说很难解析,因此在出现错误时也很难修复。
为了提高可读性,Spring 支持以下宏,这些宏代表常用的序列。
您可以使用这些宏代替六位数字值,例如: @Scheduled(cron = "@hourly")。
| 宏 | 含义 |
|---|---|
|
每年一次( |
|
每月一次( |
|
每周一次( |
|
每天一次( |
|
每小时一次,( |
7.6. 使用 Quartz 调度程序
Quartz 使用 Trigger、Job 和 JobDetail 对象来实现各种作业的调度。有关 Quartz 的基本概念,请参见
https://www.quartz-scheduler.org/。为了方便起见,Spring 提供了一些类,可简化在基于 Spring 的应用程序中使用 Quartz。
7.6.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)。
超时时间在作业数据映射中指定。作业数据映射可通过
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
}
}
作业数据映射中的所有其他属性也可供您使用。
通过使用 name 和 group 属性,您可以分别修改作业的名称和组。默认情况下,作业的名称与 JobDetailFactoryBean 的 bean 名称匹配(在上面的示例中为 exampleJob)。 |
7.6.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 作业是无状态的,这可能导致作业之间相互干扰。如果您为同一个 JobDetail 指定两个触发器,那么在第一个作业完成之前,第二个作业可能会开始。如果 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.6.3. 使用 Triggers 和 SchedulerFactoryBean 连接作业
我们已经创建了作业详细信息和作业。我们还审查了方便的bean,它允许您在特定对象上调用方法。当然,我们仍然需要安排作业本身。这是通过使用触发器和一个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 DataSource。有关更多信息,请参阅
SchedulerFactoryBean
javadoc。
SchedulerFactoryBean 还会识别类路径中的 quartz.properties 文件,
基于 Quartz 属性键,与常规 Quartz 配置相同。请注意许多
SchedulerFactoryBean 设置会与属性文件中的常见 Quartz 设置相互作用;
因此不建议在两个级别上同时指定值。例如,如果您希望依赖 Spring 提供的 DataSource,
则不要设置 "org.quartz.jobStore.class" 属性,
或指定一个 org.springframework.scheduling.quartz.LocalDataSourceJobStore 变体,
它是对标准 org.quartz.impl.jdbcjobstore.JobStoreTX 的完整替代。 |
8. 缓存抽象
从版本3.1开始,Spring框架提供了将缓存透明地添加到现有Spring应用程序的支持。类似于事务支持,缓存抽象允许以最小的代码影响一致地使用各种缓存解决方案。
在 Spring Framework 4.1 中,缓存抽象功能得到了显著扩展,支持 JSR-107 注解 和更多的自定义选项。
8.1. 了解缓存抽象
在核心层面,缓存抽象将缓存应用于Java方法,从而根据缓存中可用的信息减少执行次数。也就是说,每次调用目标方法时,抽象会应用缓存行为,以检查该方法是否已针对给定参数被调用过。如果已被调用,则直接返回缓存的结果,而无需调用实际的方法。如果方法尚未被调用,则会调用它,并将结果缓存并返回给用户,这样下次调用该方法时就会直接返回缓存的结果。这样,对于给定的参数集,耗时的方法(无论是CPU密集型还是IO密集型)只需调用一次,结果即可重复使用,而无需再次实际调用该方法。缓存逻辑是透明应用的,不会对调用者造成任何干扰。
| 这种方法仅适用于那些对于给定输入(或参数)保证返回相同输出(结果)的方法,无论调用多少次。 |
缓存抽象还提供了其他与缓存相关的操作,例如更新缓存内容或删除一个或所有条目。如果缓存处理的数据在应用程序运行过程中可能会发生变化,这些操作会非常有用。
与其他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)。有关更多关于插入其他缓存存储和提供者的相关信息,请参见 插入不同的后端缓存。
| 缓存抽象对多线程和多进程环境没有特殊处理,因为这些功能由缓存实现来处理。 |
如果你有一个多进程环境(即部署在多个节点上的应用程序),你需要相应地配置你的缓存提供者。根据你的使用场景,同一数据在多个节点上的副本可能已经足够。然而,如果在应用程序运行过程中修改了数据,你可能需要启用其他传播机制。
缓存某个特定项相当于典型的“如果未找到则继续执行并最终放入”的代码块,这在程序化缓存交互中很常见。 不会应用锁,多个线程可能会同时尝试加载相同的项。 撤出也是如此。如果多个线程同时尝试更新或撤出数据,你可能会使用过时的数据。某些缓存提供方在该方面提供了高级功能。有关更多详细信息,请参阅你的缓存提供方的文档。
要使用缓存抽象,你需要关注两个方面:
-
缓存声明:确定需要缓存的方法及其策略。
-
缓存配置:数据存储和读取的后备缓存。
8.2. 声明式注解式缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:
-
@Cacheable: 触发缓存填充。 -
@CacheEvict: 触发缓存驱逐。 -
@CachePut: 在不干扰方法执行的情况下更新缓存。 -
@Caching: 将多个缓存操作分组,以应用于方法。 -
@CacheConfig: 在类级别上共享一些通用的缓存相关设置。
8.2.1. @Cacheable 注解
顾名思义,你可以使用@Cacheable来标记可缓存的方法——也就是说,对于这些方法,其结果会被存储在缓存中,这样在后续调用(使用相同参数)时,会直接返回缓存中的值,而无需实际调用该方法。在最简单的情况下,注解声明需要指定与带注解的方法相关联的缓存名称,如下例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的代码片段中,findBook 方法与名为 books 的缓存相关联。
每次调用该方法时,都会检查缓存以查看该调用是否已经执行过,不需要重复执行。虽然在大多数情况下只声明了一个缓存,但注解允许指定多个名称,以便使用多个缓存。在这种情况下,调用方法之前会检查每个缓存——如果至少有一个缓存命中,就会返回相关的值。
| 所有不包含该值的其他缓存也会被更新,尽管实际并未调用该缓存方法。 |
以下示例在具有多个缓存的 findBook 方法中使用 @Cacheable:
@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 变量),因为这些条件会在事先进行验证以确认排除。 |
8.2.3. @CacheEvict 注解
缓存抽象不仅允许填充缓存存储,还允许驱逐。
此过程有助于从缓存中删除过时或未使用的数据。与
@Cacheable 相反,@CacheEvict 标记执行缓存
驱逐的方法(即,作为从缓存中移除数据的触发器的方法)。
与它的兄弟标签类似,@CacheEvict 需要指定一个或多个
受该操作影响的缓存,允许指定自定义缓存和键解析或条件,
并且还有一个额外的参数(
allEntries)用于指示是否需要执行整个缓存的驱逐,
而不是仅基于键进行条目驱逐。以下示例将从
books 缓存中驱逐所有条目:
@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 模式。 |
有关实现 CachingConfigurer 所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅
javadoc。 |
| XML 属性 | 注解属性 | 默认 | 描述 |
|---|---|---|---|
|
未提供(参见 |
|
要使用的缓存管理器的名称。默认情况下会在此缓存管理器后初始化 |
|
未提供(参见 |
一个使用配置的 |
用于解析后备缓存的CacheResolver的Bean名称。 此属性不是必需的,仅在作为'cache-manager'属性的替代时才需要指定。 |
|
未提供(参见 |
|
自定义密钥生成器的名称 |
|
未提供(参见 |
|
自定义缓存错误处理程序的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会返回给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义了应用于标注有 |
<cache:annotation-driven/> 仅在它定义的同一应用程序上下文中查找 @Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,
如果你在 <cache:annotation-driven/> 中为 WebApplicationContext 添加了 DispatcherServlet,它只会检查你的控制器中的 bean,而不会检查你的服务。
有关更多信息,请参阅 MVC 部分。 |
Spring 建议您仅使用 @Cache* 注解标注具体类(以及具体类的方法),而不是标注接口。您当然可以在接口(或接口方法)上放置 @Cache* 注解,但只有在使用代理模式(mode="proxy")时才有效。如果您使用基于编织的切面(mode="aspectj"),则编织基础设施不会识别接口级别声明的缓存设置。 |
在代理模式(默认模式)下,只有通过代理进入的外部方法调用才会被拦截。这意味着自调用(实际上,目标对象中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的缓存,即使被调用的方法标记有@Cacheable。在这种情况下,建议使用aspectj模式。此外,代理必须完全初始化才能提供预期的行为,因此你不应在初始化代码中依赖此功能(即@PostConstruct)。 |
8.2.7. 使用自定义注解
缓存抽象机制允许您使用自己的注解来标识哪些方法会触发缓存的生成或清除。作为一种模板机制,这非常方便,因为它消除了重复声明缓存注解的需要,这在键或条件被指定的情况下,或者在您的代码库中不允许使用外部导入(org.springframework)时尤其有用。与其余的 构造型 注解类似,您可以使用 @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 切入点表达式将缓存建议应用到程序的适当位置(有关更多信息,请参阅 使用 Spring 的面向方面的编程)。在前面的例子中,所有来自 BookService 的方法都会被考虑,并将缓存建议应用于它们。
声明式XML缓存支持所有基于注解的模型,因此在两者之间切换应该相当容易。此外,两者都可以在同一应用程序中使用。
基于XML的方法不会影响目标代码。然而,它本质上更加冗长。在处理具有重载方法的类时,这些方法被用于缓存,识别正确的方法需要额外的努力,因为method参数不是一个好的区分器。在这些情况下,您可以使用AspectJ切入点来挑选目标方法并应用适当的缓存功能。
但是,通过XML,更容易应用包或组或接口范围的缓存(再次由于AspectJ切入点)以及创建类似模板的定义(就像我们在前面的例子中通过cache:definitions cache属性定义目标缓存那样)。
8.5. 配置缓存存储
缓存抽象提供了几种存储集成选项。要使用它们,你需要声明一个适当的 CacheManager(一个控制和管理 Cache 实例的实体,并且可以用来检索这些实例以进行存储)。
8.5.1. 基于 JDK ConcurrentMap 的缓存
基于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 为两个名为 default 和 books 的嵌套 ConcurrentMapCache 实例创建了一个 CacheManager。请注意,名称是直接为每个缓存配置的。
当缓存由应用程序创建时,它与其生命周期绑定,这使其适用于基本用例、测试或简单应用。缓存扩展性良好且非常快速,但不提供任何管理、持久化能力或逐出协议。
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"/>
此设置在Spring IoC中引导ehcache库(通过ehcache bean),然后将其连接到专用的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="cacheNames">
<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 标志为所有未被配置的缓存管理器处理的定义添加了一个无操作缓存。也就是说,每个未在 jdkCache 或 gemfireCache(在示例中之前配置的)中的缓存定义都由无操作缓存处理,该缓存不存储任何信息,导致目标方法每次都被调用。
8.6. 集成不同的后端缓存
显然,市场上有许多可以作为后备存储的缓存产品。对于不支持 JSR-107 的产品,你需要提供一个 CacheManager 和一个 Cache 实现。这听起来可能比实际更复杂,因为实际上这些类通常是简单的 适配器,它们在存储 API 之上映射缓存抽象框架,正如 ehcache 类所做的那样。大多数 CacheManager 类可以使用 org.springframework.cache.support 包中的类(例如 AbstractCacheManager,它负责处理样板代码,只需完成实际的映射即可)。
9. 附录
9.1. XML 模式
附录的这一部分列出了与集成技术相关的XML模式。
9.1.1. The jee Schema
该 jee 元素处理与 Java EE(Java 企业版)配置相关的问题,
例如查找 JNDI 对象和定义 EJB 引用。
要使用 jee 架构中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导内容。以下片段中的文本引用了正确的架构,以便您可以使用 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/>(简单)
以下示例显示了如何使用JNDI在没有<code>0</code>架构的情况下查找数据源:
<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环境设置)
以下示例显示了如何使用JNDI查找环境变量而无需
jee:
<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环境设置)
以下示例显示了如何使用 JNDI 查找多个环境变量,而无需 jee:
<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>
以下示例显示了如何将对本地EJB无状态会话Bean的引用配置为
jee:
<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"/>
<jee:local-slsb/> (复杂)
<jee:local-slsb/> 元素配置对本地 EJB 无会话状态会话 Bean 的引用。
以下示例显示了如何配置对本地EJB无状态会话Bean的引用以及一些不带jee的属性:
<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>
以下示例显示了如何配置对本地EJB无状态会话Bean的引用以及一些属性的值为jee:
<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/>
该<code>0</code>元素配置对<code>1</code> 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>
以下示例显示了如何将对远程EJB无状态会话Bean的引用配置为
jee:
<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. 0 架构
jms 元素涉及配置与 JMS 相关的 bean,例如 Spring 的
消息监听器容器。这些元素在
JMS 章节 中有详细说明,该章节标题为 JMS 命名空间支持。有关此支持的完整细节以及 jms 元素本身的详情,请参阅该章节。
为了完整性,要使用jms模式中的元素,你需要在Spring XML配置文件的顶部包含以下引言。以下代码片段中的文本引用了正确的模式,以便您能够使用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 配置文件的顶部包含以下前导内容。以下片段中的文本引用了正确的架构,以便您可以使用 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>