这部分参考文档涵盖了 Spring Framework 与许多技术的集成.

1. REST Endpoints

Spring Framework 为调用 REST 端点提供了两种选择:

  • RestTemplate: 具有 synchronous, template method API 的原始的 Spring REST 客户端.

  • WebClient: 一种非阻塞的, 响应式的替代方案, 它支持同步,异步以及 streaming 场景.

从 5.0 版本开始, RestTemplate 处于维护模式, 以后只有很小的更改和错误请求被接受. 请考虑使用更现代的支持同步,异步和流方案的 WebClient API.

1.1. RestTemplate

RestTemplate 在 HTTP 客户端库上提供了更高级别的 API,它使得在一行中调用 REST 端点变得容易. 它暴露了以下重载方法组:

Table 1. RestTemplate methods
Method group Description

getForObject

通过 GET 获取响应.

getForEntity

使用 GET 获取 ResponseEntity (即status, headers和body)

headForHeaders

使用 HEAD 获取所有 headers

postForLocation

使用 POST 创建新资源,并从响应中返回 Location 请求头.

postForObject

使用 POST 创建新资源并从响应中返回表示.

postForEntity

使用 POST 创建新资源并从响应中返回表示.

put

使用 PUT 创建或更新资源.

patchForObject

使用 PATCH 更新资源并从响应中返回表示. 请注意,JDK HttpURLConnection 不支持 PATCH,但Apache HttpComponents 和其他一样.

delete

使用 DELETE 删除指定 URI 处的资源.

optionsForAllow

使用 ALLOW 检索资源的允许 HTTP 方法.

exchange

上述方法的更通用 (且不太固定) 的版本,在需要时提供额外的灵活性. 它接受 RequestEntity (包括 HTTP 方法,URL,headers 和正文作为输入) 并返回 ResponseEntity.

这些方法允许使用 ParameterizedTypeReference 而不是 Class 来指定具有泛型的响应类型.

execute

执行请求的最通用方式,通过回调接口完全控制请求准备和响应提取.

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 客户端库.
URIs

许多 RestTemplate 方法都接受 URI 模板和 URI 模板变量, 作为 String 变量参数或 Map<String,String>.

以下示例使用 String 变量参数:

String result = restTemplate.getForObject(
        "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");

以下示例使用 Map<String, String>:

Map<String, String> vars = Collections.singletonMap("hotel", "42");

String result = restTemplate.getForObject(
        "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

请注意, URI 模板是自动编码的, 如以下示例所示:

restTemplate.getForObject("https://example.com/hotel list", String.class);

// Results in request to "https://example.com/hotel%20list"

您可以使用 RestTemplateuriTemplateHandler 属性来自定义 URI 的编码方式. 或者, 您可以创建一个 java.net.URI , 并且使用 RestTemplate 中接受 URI 参数的方法之一.

有关使用和编码 URI 的更多详细信息, 请参阅 URI Links.

Headers

你可以使用 exchange() 方法指定特殊的请求头, 如下:

String uriTemplate = "https://example.com/hotels/{hotel}";
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);

RequestEntity<Void> requestEntity = RequestEntity.get(uri)
        .header("MyRequestHeader", "MyValue")
        .build();

ResponseEntity<String> response = template.exchange(requestEntity, String.class);

String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();

您可以通过许多返回 ResponseEntityRestTemplate 方法来获取响应头.

1.1.2. Body

HttpMessageConverter 的帮助下, 传递到 RestTemplate 方法和从 RestTemplate 方法返回的对象与原始内容进行转换.

在 POST 请求中, 输入对象被序列化到请求主体, 如以下示例所示:

URI location = template.postForLocation("https://example.com/people", person);

您无需显式设置请求的 Content-Type 头. 在大多数情况下, 您可以找到基于源对象类型的兼容消息转换器, 并且所选消息转换器会相应地设置内容类型. 如有必要, 可以使用 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 模块包含 HttpMessageConverter , 用于通过 InputStreamOutputStream 读取和写入 HTTP 请求和响应正文. 在客户端 (例如, 在 RestTemplate 中) 和服务器端 (例如, 在 Spring MVC REST 控制器中) 使用 HttpMessageConverter 实例.

框架中提供了主要的媒体 (MIME) 类型的具体实现, 默认情况下, 这些实现在客户端的 RestTemplate 和服务器的 RequestMappingHandlerAdapter 进行了注册 (请参阅 配置消息转换器) .

以下各节介绍了 HttpMessageConverter 的实现. 对于所有转换器, 都使用默认的媒体类型, 但是您可以通过设置 supportedMediaTypes bean 属性来覆盖它. 下表描述了每种实现:

Table 2. HttpMessageConverter Implementations
MessageConverter Description

StringHttpMessageConverter

一个 HttpMessageConverter 实现,可以从 HTTP 请求和响应中读取和写入 String 实例. 默认情况下,此转换器支持所有文本媒体类型 (text/*) ,并使用 Content-Typetext/plain 进行写入.

FormHttpMessageConverter

一个 HttpMessageConverter 实现,可以从 HTTP 请求和响应中读取和写入表单数据. 默认情况下,此转换器读取和写入 application/x-www-form-urlencoded 媒体类型. 表单数据从 MultiValueMap<String, String> 读取并写入. 转换器还可以将 MultiValueMap<String, String> 写入 (但不能读) multipart 数据, 从 "MultiValueMap<String,Object>" 读取的数据. 默认情况下, multipart/form-data 是支持的. 从 Spring Framework 5.2 开始,可以支持其他的多部分子类型. 编写表格数据. 有关更多详细信息,请查阅 Javadoc 中的 FormHttpMessageConverter.

ByteArrayHttpMessageConverter

一个 HttpMessageConverter 实现,可以从 HTTP 请求和响应中读取和写入字节数组. 默认情况下,此转换器支持所有媒体类型 (/) ,并使用 Content-Typeapplication/octet-stream 进行写入. 您可以通过设置 supportedMediaTypes 属性并覆盖 getContentType(byte[]) 来覆盖它.

MarshallingHttpMessageConverter

一个 HttpMessageConverter 实现,可以使用 org.springframework.oxm 包中的 Spring 的 MarshallerUnmarshaller 抽象来读写 XML. 该转换器需要 MarshallerUnmarshaller 才能使用. 您可以通过构造函数或 bean 属性注入这些. 默认情况下,此转换器支持 text/xmlapplication/xml.

MappingJackson2HttpMessageConverter

一个 HttpMessageConverter 实现,可以使用 Jackson 的 ObjectMapper 读写JSON. 您可以根据需要通过使用 Jackson 提供的注解来自定义 JSON 映射. 当您需要进一步控制时 (对于需要为特定类型提供自定义 JSON 序列化器/反序列化器的情况) ,您可以通过 ObjectMapper 属性注入自定义 ObjectMapper. 默认情况下,此转换器支持 application/json.

MappingJackson2XmlHttpMessageConverter

一个 HttpMessageConverter 实现,可以使用 Jackson XML 扩展的 XmlMapper 读写XML. 您可以根据需要通过使用 JAXB 或 Jackson 提供的注解来自定义XML映射. 当您需要进一步控制时 (对于需要为特定类型提供自定义 XML 序列化器/反序列化器的情况) ,您可以通过 ObjectMapper 属性注入自定义 XmlMapper. 默认情况下,此转换器支持 application/xml.

SourceHttpMessageConverter

一个 HttpMessageConverter 实现,可以从 HTTP 请求和响应中读取和写入 javax.xml.transform.Source. 仅支持 DOMSource,SAXSourceStreamSource. 默认情况下,此转换器支持 text/xmlapplication/xml.

BufferedImageHttpMessageConverter

一个 HttpMessageConverter 实现,可以从 HTTP 请求和响应中读取和写入 java.awt.image.BufferedImage. 此转换器读取和写入 Java I/O API 支持的媒体类型.

1.1.4. Jackson JSON Views

您可以指定 Jackson JSON View 来序列化对象属性的一部分, 如以下示例所示:

MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);

RequestEntity<MappingJacksonValue> requestEntity =
    RequestEntity.post(new URI("https://example.com/user")).body(value);

ResponseEntity<String> response = template.exchange(requestEntity, String.class);
Multipart

要发送 multipart data, 您需要提供一个 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 值, 则 FormHttpMessageConverterContent-Type 设置为 multipart/form-data. 如果 MultiValueMap 具有字符串值, 则 Content-Type 默认为 application/x-www-form-urlencoded. 如有必要, 还可以显式设置 Content-Type.

1.2. Using AsyncRestTemplate (已弃用)

不推荐使用 AsyncRestTemplate. 对于您可能考虑使用 AsyncRestTemplate 的所有用例, 请改用 WebClient.

2. 远程处理和 Web 服务

Spring 为使用各种远程技术支持的集成提供了便捷,远程处理支持可轻松使用常用 (Spring)pojo 实现的、由您提供的服务开发. 目前,Spring 支持以下远程技术:

  • Java Web Services: Spring 通过 JAX-WS 提供对 Web 服务的远程支持.

  • AMQP: 单独的 Spring AMQP 项目支持使用 AMQP 作为底层协议进行远程处理.

从 Spring Framework 5.3 开始, 出于安全原因和更广泛的行业支持, 现已不支持多种远程技术. 支持的基础结构将从 Spring Framework 的下一个主要版本中删除.

现在不推荐使用以下远程处理技术, 并且不会被取代:

  • 远程方法调用(RMI): 通过使用 RmiProxyFactoryBeanRmiServiceExporter. Spring 支持传统的 RMI (使用 java.rmi.Remote 接口和 java.rmi.RemoteException) 以及通过 RMI 调用程序 (使用任何 Java 接口) 进行透明的远程处理.

  • Spring的 HTTP 调用: Spring 提供了一种特殊的远程处理策略,允许通过 HTTP 进行 Java 序列化,支持任何 Java 接口 (如 RMI 调用者所做的那样) . 相应的支持类是 HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter.

  • Hessian: 通过使用 Spring 的 HessianProxyFactoryBeanHessianServiceExporter,您可以使用 Caucho 提供的轻量级二进制 http 协议透明地暴露您的服务.

  • JMS: 通过 spring-jms 模块中的 JmsInvokerServiceExporterJmsInvokerProxyFactoryBean 类支持使用 JMS 作为底层协议进行远程处理.

在讨论 Spring 的远程处理功能时,我们使用以下 domain 模型和相应的服务:

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

Spring AMQP 项目支持通过 AMQP 作为基础协议进行远程处理. 有关更多详细信息, 请访问 Spring AMQP 参考的 Spring Remoting.

远程接口未实现自动检测

远程接口不发生自动检测实现的接口的主要原因是避免向远程调用方打开太多的门,目标对象可能实现内部回调接口,例如 InitializingBeanDisposableBean,它们不希望向调用者暴露.

在本地情况下, 向代理提供由目标实现的所有接口通常并不重要. 但是, 当暴露远程服务时, 应暴露特定的服务接口, 并使用特定的操作来进行远程使用. 除内部回调接口外, 目标可能实现多个业务接口, 其中仅有一个用于远程暴露. 由于这些原因, 我们需要指定这样的服务接口.

这是配置方便和意外暴露内部方法风险之间的权衡,总是指定一个服务接口并不费劲, 并把你放在安全方面的控制暴露的特定方法.

2.2. 选择技术时的注意事项

这里介绍的每项技术都有其缺点. 在选择技术时,您应该仔细考虑您的需求,您暴露的服务以及通过网络发送的对象.

当使用 RMI ,不可能通过 HTTP 协议来访问到这些对象,除非你使用了 RMI 的隧道. RMI 是一个重量级的协议,支持全功能的序列化策略,这在使用需要在线上进行序列化的复杂数据模型时非常重要. 但是, RMI-JRMP 与 Java 客户端绑定在一起,它是一种 Java-to-Java 远程处理解决方案.

如果您需要基于 HTTP 的远程处理,而且还依赖于 Java 序列化,那么 Spring 的 HTTP 调用程序是一个不错的选择. 它与 RMI 调用共享基本的基础结构, 只是使用 HTTP 作为传输. 请注意,HTTP 调用程序不仅限于 Java-to-Java 远程处理,还包括客户端和服务器端的 Spring . (后者也适用于 Spring 的非 RMI 接口的 RMI 调用程序. )

在跨平台中使用时,Hessian 是很好的选择,因为它们明确允许非 Java 客户端. 但是,非 Java 支持仍然有限. 已知问题包括 Hibernate 对象的序列化以及延迟初始化的集合. 如果您有这样的数据模型,请考虑使用 RMI 或 HTTP 调用程序而不是 Hessian.

JMS 可用于提供服务群集, 并允许 JMS 代理处理负载平衡、发现和自动故障转移. 默认情况下, 在使用 JMS 远程处理时使用 Java 序列化, 但 JMS 提供程序可以对格式使用不同的机制, 例如 XStream 以允许在其他技术中实现服务器.

最后但并非最不重要的是,EJB 具有优于 RMI 的优势,因为它支持标准的基于角色的身份验证和授权以及远程事务传播. 有可能让 RMI 调用者或 HTTP 调用者也支持安全上下文传播,尽管核心 Spring 没有提供. Spring 仅提供适当的挂钩,用于插入第三方或自定义解决方案.

2.3. Java Web Services

Spring 提供对标准 Java Web Services API 的完全支持:

  • 使用 JAX-WS 暴露服务

  • 使用 JAX-WS 访问 Web 服务

除了在 Spring 的 core 中支持 JAX-WS 外,Spring 的 portfolio 也具有 Spring Web Services 的特点. 这是一种以文档驱动的 Spring 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 功能可以应用到端点实例. 当然, 通过 @Autowired 的注解驱动的注入也会起作用. 以下示例显示了如何定义这些 bean:

<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter">
    <property name="baseAddress" value="http://localhost:8080/"/>
</bean>

<bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint">
    ...
</bean>

...

AccountServiceEndpoint 可以不用继承 Spring 的 SpringBeanAutowiringSupport,因为此示例中的端点是完全由 Spring 管理的 bean. 这意味着端点实现可以如下 (没有声明任何超类 - 而且 Spring 的 @Autowired 配置注解仍然被可用) :

@WebService(serviceName="AccountService")
public class AccountServiceEndpoint {

    @Autowired
    private AccountService biz;

    @WebMethod
    public void insertAccount(Account acc) {
        biz.insertAccount(acc);
    }

    @WebMethod
    public List<Account> getAccounts(String name) {
        return biz.getAccounts(name);
    }
}

2.3.3. 使用 JAX-WS RI 的 Spring 支持暴露 Web 服务

Oracle 的 JAX-WS RI, 作为 GlassFish 项目的一部分,通过 Spring 的支持也作为 JAX-WS Commons 项目的一部分. 这允许将 JAX-WS 端点定义为 Spring 管理 bean, 类似于 上一节 中讨论的独立模式. 但这一次是在 Servlet 环境中进行的

这在 Java EE 环境中不可移植. 它主要用于非 EE 环境,例如 Tomcat,它将 JAX-WS RI 作为 Web 应用程序的一部分嵌入.

与 Servlet 端的标准样式的不同之处在于,端实例本身的生命周期将由 Spring 来管理,并且在 web.xml 中将只定义一个JAX-WS Servlet. 使用标准的 Java EE 样式 (如上所述), 每个服务端都有一个 Servlet 定义, 每个端通常委派给 Spring bean(通过使用 @Autowired,如上所示).

有关设置和使用方式的详细信息,请参阅 https://jax-ws-commons.java.net/spring/.

2.3.4. 使用 JAX-WS 访问 Web 服务

Spring 提供了两个工厂 bean 来创建 JAX-WS Web 服务代理,即 LocalJaxWsServiceFactoryBeanJaxWsPortProxyFactoryBean. 前者只能返回一个 JAX-WS 服务类供我们使用. 后者是完整版本,可以返回实现我们的业务服务接口的代理. 在以下示例中,我们使用 JaxWsPortProxyFactoryBeanAccountService 端点创建代理 (再次) :

<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
    <property name="serviceInterface" value="example.AccountService"/> (1)
    <property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/>
    <property name="namespaceUri" value="https://example/"/>
    <property name="serviceName" value="AccountService"/>
    <property name="portName" value="AccountServiceEndpointPort"/>
</bean>
1 其中 serviceInterface 是客户端使用的业务接口.

wsdlDocumentUrl 是 WSDL 文件的URL. Spring 需要这样的启动时间来创建 JAX-WS 服务. namespaceUri 对应于 .wsdl 文件中的 targetNamespace . serviceName 对应于 .wsdl 文件中的服务名称. portName 对应于 .wsdl 文件中的端口名称.

访问Web服务现在非常容易, 因为我们有一个 bean 工厂, 它将暴露它作为 AccountService 接口. 我们可以在 Spring 中配置:

<bean id="client" class="example.AccountClientImpl">
    ...
    <property name="service" ref="accountWebService"/>
</bean>

从客户端代码中, 我们可以访问 Web 服务,就好像它是一个普通类一样:

public class AccountClientImpl {

    private AccountService service;

    public void setService(AccountService service) {
        this.service = service;
    }

    public void foo() {
        service.insertAccount(...);
    }
}
上述情况略有简化, 因为 JAX-WS 需要端点接口和实现类来注解 @WebService@SOAPBinding 等注解. 这意味着您不能(很容易)使用普通的 Java 接口和实现类作为 JAX-WS 端点 artifacts ; 首先您需要相应地对它们进行注解. 有关这些要求的详细信息 请参阅 JAX-WS 文档.

2.4. RMI (已弃用)

从 Spring Framework 5.3 开始, RMI 支持已被弃用, 不会被取代.

使用 Spring 对 RMI 的支持, 您可以通过 RMI 架构透明地暴露服务. 在用此设置之后, 您基本上拥有一个类似于远程 EJB 的配置, 但不存在安全上下文传播或远程事务传播的标准支持这一事实. Spring 在使用 RMI 调用器时提供了这样的附加调用上下文的钩子, 因此您可以在此处插入安全框架或自定义安全凭据.

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 开发,您可以在 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 原则,您可能知道现在必须在 WEB-INF 目录中创建一个名为 remoting-servlet.xml 的 Spring 容器配置资源 (在您的 servlet 名称之后) . 应用程序上下文将在下一节中使用.

或者, 考虑使用 Spring 更简单的 HttpRequestHandlerServlet. 这使您可以在根应用程序上下文中嵌入远程暴露定义(默认情况下为 WEB-INF/applicationContext.xml), 并将单个 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 映射(如上所述)的 URL 中通过其 bean 名称指示的网址暴露: 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 工厂或应用程序上下文,并提到以下 bean,其中 SimpleObject 是通过使用 AccountService 来管理帐户,如以下示例所示:

<bean class="example.SimpleObject">
    <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
    <property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

2.5.4. 通过 Hessian 暴露的服务来应用 HTTP 基本的验证

Hessian 的一个优点是我们可以轻松应用 HTTP 基本身份验证,因为这两种协议都是基于 HTTP 的. 例如,可以通过使用 web.xml 安全功能来应用常规 HTTP 服务器安全性机制. 通常,您无需在此处使用每用户安全凭据. 相反,您可以使用在 HessianProxyFactoryBean 级别定义的共享凭证 (类似于JDBC DataSource) ,如以下示例所示:

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="interceptors" ref="authorizationInterceptor"/>
</bean>

<bean id="authorizationInterceptor"
        class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
    <property name="authorizedRoles" value="administrator,operator"/>
</bean>

在前面的示例中,我们明确提到了 BeanNameUrlHandlerMapping 并设置了一个拦截器,只允许管理员和操作员调用此应用程序上下文中提到的 bean.

前面的示例并没有显示一种灵活的安全基础结构. 有关安全性的更多选项,请查看 spring.io/projects/spring-security/ 上的 Spring Security 项目.

2.6. Spring HTTP 调用 (已弃用)

从 Spring Framework 5.3 开始, HTTP 调用已经弃用, 不会被取代.

与 Hessian 相反,Spring HTTP 调用者都是轻量级协议,Hessian 使用自身序列化机制,而 Spring HTTP 调用者使用标准的 Java 序列化机制通过 HTTP 暴露服务. 如果您的参数和返回类型是使用 Hessian 使用的序列化机制无法序列化的复杂类型,那么这具有巨大的优势 (当您选择远程处理技术时,请参阅下一节以了解更多注意事项) .

在底层, Spring 使用 JDK 或 Apache HttpComponents 提供的标准工具来执行 HTTP 调用. 如果您需要更高级且更易于使用的功能,请使用后者. 有关更多信息,请参阅 hc.apache.org/httpcomponents-client-ga/.

由于不安全的 Java 反序列化而造成的漏洞:操作的输入流可能导致在反序列化步骤中在服务器上执行不需要的代码. 因此, 不要将 HTTP 调用方端点暴露给不受信任的客户端, 而只应该在您自己的服务之间. 通常, 我们强烈推荐任何其他消息格式(例如 JSON).

如果您担心由于 Java 序列化引起的安全漏洞, 请考虑核心 JVM 级别的 generalpurpose 序列化过滤器机制, 它最初是为 JDK 9 开发的. 但同时又向后移植到 JDK 8,7 和 6. 请参阅 blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_aopenjdk.java.net/jeps/290.

2.6.1. 暴露服务对象

为服务对象设置 HTTP 调用的架构类似于使用 Hession,方式也是相同的. 正如 Hession 支持提供 是 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 的标准映射设备, 如在关于 Hession的章节中解释.

或者,您可以在根应用程序上下文中创建 HttpInvokerServiceExporter (例如,在 'WEB-INF/applicationContext.xml' 中) ,如以下示例所示:

<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property name="service" ref="accountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

此外,在 web.xml 中为该暴露定义一个对应的 Servlet, 其 Servlet 名称与目标暴露的 bean 名称相匹配. 如下所示:

<servlet>
    <servlet-name>accountExporter</servlet-name>
    <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>accountExporter</servlet-name>
    <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>

2.6.2. 在客户端链接服务

同样,从客户端链接服务非常类似于使用 Hessian 时的方式. 通过使用代理,Spring 可以将对 HTTP POST 请求的调用转换为指向暴露服务的 URL. 以下示例显示如何配置:

<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 作为底层通信协议透明地暴露服务. JMS 的远程支持在 Spring Framework 中是非常基础. 它在同一个线程和同一个非事务性会话中发送和接收. 并且这样的吞吐量将非常依赖于实现, 请注意, 这些单线程和非事务性约束仅适用于 Spring 的 JMS 远程处理支持. 有关 Spring 对 JMS 消息传递的丰富支持的信息, 请参见 JMS (Java 消息服务) (Java Message Service)

以下接口用于服务器端和客户端:

public interface CheckingAccountService {

    public void cancelAccount(Long accountId);
}

在服务器端使用上述接口的以下简单实现:

public class SimpleCheckingAccountService implements CheckingAccountService {

    public void cancelAccount(Long accountId) {
        System.out.println("Cancelling account [" + accountId + "]");
    }
}

此配置文件包含在客户端和服务器上共享的 JMS 基础结构 bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="tcp://ep-t43:61616"/>
    </bean>

    <bean id="queue" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg value="mmm"/>
    </bean>

</beans>

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>
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) 的客户端代理.

以下示例定义了可以注入其他客户端对象的 bean (代理负责通过 JMS 将调用转发到服务器端对象) :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="checkingAccountService"
            class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean">
        <property name="serviceInterface" value="com.foo.CheckingAccountService"/>
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="queue" ref="queue"/>
    </bean>

</beans>
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. 企业级JavaBean(EJB) 集成

作为轻量级容器, Spring 通常被视为 EJB 的替代者. 我们确实认为, 对于许多应用程序和用例来说, Spring 作为一个容器, 加上其在事务、ORM 和 JDBC 访问方面的丰富支持功能, 比通过 EJB 容器和 EJB 实现等效功能更有选择.

但是, 重要的是要注意, 使用 Spring 不会阻止您使用 EJB. 实际上, Spring 使访问 EJB 和在其中实现 EJB 和功能变得更加容易. 此外, 通过使用 Spring 来访问 EJB 提供的服务, 可以在本地 EJB、远程 EJB 或 POJO(普通的 Java 对象) 变体之间切换这些服务的实现, 而无需更改客户端代码.

在本章中, 我们来看看 Spring 如何帮助您访问和实现 EJB. Spring 在访问无状态会话 bean(SLSBs)时提供了特定的值, 因此我们将从讨论这个开始.

3.1. 访问 EJB

本节介绍如何访问 EJB.

3.1.1. 概念

若要在本地或远程无状态会话 bean 上调用方法, 客户端代码通常必须执行JNDI查找以获取(本地或远程)EJB主对象, 然后对该对象使用 create 方法调用以获取实际(本地或远程) EJB 对象,然后在 EJB 上调用一个或多个方法.

为避免重复的低级代码,许多 EJB 应用程序使用 Service Locator (服务定位器模式) 和 Business Delegate (业务委托模式) . 这些都比在整个客户端代码中充满 JNDI 查找更好,但它们通常的实现具有明显的缺点:

  • 通常,使用 EJB 的代码依赖于 Service Locator 或 Business Delegate 单例,这使得很难进行测试.

  • 对于没有业务代表使用的服务定位器模式,应用程序代码仍然必须在 EJB 主目录上调用 create() 方法并处理生成的异常. 因此,它仍然与 EJB API 和 EJB 编程模型的复杂性联系在一起.

  • 实现业务委托模式通常会导致严重的代码重复,我们必须编写大量在 EJB 上调用相同方法的方法.

    Spring 方法是允许创建和使用代理对象, 通常在 Spring 容器中配置, 作为无需代码的业务委派. 您不需要在手动编码的业务委派中编写另一个服务定位器、另一个 JNDI 查找或重复方法, 除非实际上在这些代码中添加了实际值.

3.1.2. 访问本地的 SLSBs

假设我们有一个需要使用本地 EJB 的 Web 控制器. 我们将遵循最佳实践并使用 EJB 业务方法接口模式, 以便 EJB 的本地接口继承非 EJB 特定的业务方法接口. 我们将此业务方法称为 MyComponent 接口. 以下示例显示了这样的接口:

public interface MyComponent {
    ...
}

使用业务方法接口模式的主要原因之一是确保本地接口中的方法签名和 bean 实现类之间的同步是自动的. 另一个原因是, 它后来使我们更容易切换到 POJO(普通、旧的Java对象)的服务实现,如果它有意义的这样做. 当然, 我们还需要实现本地 home 接口, 并提供实现 SessionBeanMyComponent 业务方法接口的实现类. 现在, 我们需要做的唯一的 Java 编码来将我们的 Web 层控制器与 EJB 实现挂钩, 就是在控制器上暴露一个类型 MyComponent 的 setter 方法. 这将将引用保存为控制器中的实例变量:

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
    this.myComponent = myComponent;
}

随后, 我们可以在控制器中的任何业务方法中使用此实例变量. 现在假设我们从 Spring 容器中获取控制器对象, 我们可以 (在同一上下文中)配置一个 LocalStatelessSessionProxyFactoryBean 实例, 它将是 EJB 代理对象. 代理的配置以及控制器的 myComponent 属性的设置是通过配置项完成的, 例如:

<bean id="myComponent"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/myBean"/>
    <property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>

<bean id="myController" class="com.mycom.myController">
    <property name="myComponent" ref="myComponent"/>
</bean>

有很多工作发生在幕后, 这得益于 Spring 的 AOP 框架, 虽然你不被迫使用 AOP 的概念来享受结果. myComponent bean 定义为 EJB 创建了一个代理, 它实现了业务方法接口. EJB 本地主页是在启动时缓存的, 因此只有一个JNDI查找. 每次调用 EJB 时, 代理都调用本地 AOP 上的 classname 方法, 并调用 AOP 上相应的业务方法.

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 的使用. 如果要用 POJO 或 mock对象或其他测试存根替换此 EJB 引用, 我们只需更改 myComponent bean 定义, 而不更改一行 Java 代码. 此外, 我们还不必编写一行 JNDI 查找或其他 EJB 管道代码作为应用程序的一部分.

在实际应用中的基准和经验表明, 这种方法(涉及对目标 EJB 的反射调用)的性能开销是最小的, 通常在典型的使用中是无法检测到的. 请记住, 无论如何, 我们不希望对 EJB 进行 fine-grained 调用, 因为在应用程序服务器中存在与 EJB 基础结构相关的成本.

关于 JNDI 查找有一个警告. 在 bean 容器中, 此类通常作为单一实例最好使用(根本没有理由使其成为原型). 但是, 如果该 bean 容器 pre-instantiates 单变量(与各种XML ApplicationContext 变体一样), 则在 EJB 容器加载目标 EJB 之前装载 bean容器时可能会出现问题. 这是因为 JNDI 查找将在该类的 init() 方法中执行, 然后缓存, 但 EJB 还没有被绑定到目标位置.解决方案是不 pre-instantiate 此工厂对象, 但允许它在首次使用时创建在 XML 容器中, 这是通过 lazy-init 属性来控制的.

虽然大多数 Spring 用户不感兴趣,但那些使用 EJB 编程 AOP 的人可能希望查看 LocalSlsbInvokerInterceptor.

3.1.3. 访问远程的 SLSBs

访问远程 EJB 与访问本地 EJB 基本相同, 只是使用了 SimpleRemoteStatelessSessionProxyFactoryBean<jee:remote-slsb> 配置元素. 当然, 无论有无 Spring , 远程调用语义都适用. 在另一台计算机的另一个 VM 中对某个对象的方法的调用有时必须在使用方案和失败处理方面得到不同的处理.

Spring 的 EJB 客户端支持在非 Spring 方法上增加了一个优势. 通常, EJB 客户端代码在本地或远程调用 EJB 之间很容易来回切换是有问题的. 这是因为远程接口方法必须声明它们抛出 RemoteException, 而客户端代码必须处理此问题, 而本地接口方法则不这样做. 为本地 EJB 编写的客户端代码需要移动到远程 EJB, 通常必须进行修改, 以便为远程异常添加处理, 为需要移动到本地 EJB 的远程 EJB 编写的客户端代码, 可以保持不变, 但会对远程异常进行大量不必要的处理, 或者需要修改以删除该代码. 使用 Spring 远程 EJB 代理, 您可以不在业务方法接口中声明任何抛出的 RemoteException, 也不能实现 EJB 代码, 它具有一个完全相同的远程接口, 但它确实抛出了 RemoteException, 并且依赖于代理来动态处理这两个接口, 就好像它们是相同的一样. 即 客户端代码不必处理已检查的 RemoteException 类. 在 EJB 调用期间抛出的任何实际 RemoteException 都将引发为 non-checked RemoteAccessException 类, 这是 RuntimeException 的一个子类别. 然后, 可以将目标服务切换到本地 EJB 或远程 EJB(甚至是普通 Java 对象) 实现之间, 而无需客户端代码知道或关心. 当然, 这是可选的. 没有什么能阻止您在业务界面中声明 RemoteExceptions.

3.1.4. 访问 EJB 2.x SLSBs 和 EJB 3 SLSBs

通过 Spring 访问 EJB 2.x 会话 bean 和 EJB 3 会话 bean 在很大程度上是透明的. Spring 的 EJB 访问器(包括 <jee:local-slsb><jee:remote-slsb> 功能) 在运行时透明地适应实际组件. 如果找到了一个主接口(EJB 2.x 样式), 或者如果没有可用的主接口(EJB 3 样式), 则执行直接组件调用.

Note: 对于 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(MDPs) 的消息监听器容器. Spring 还提供了一种创建消息监听器的声明方式.

org.springframework.jms.core 提供了使用JMS的核心功能. 包含 JMS 模板类, 通过处理资源的创建和释放来简化 JMS 的使用, 就像 JdbcTemplate 对 JDBC 所做的那样. Spring 模板类的通用设计原则是提供帮助器方法来执行常见操作和更复杂的用法, 将处理任务的本质委托给用户实现的回调接口. JMS 模板遵循相同的设计. 这些类为发送消息提供了各种方便方法, 同步地使用消息, 并向用户暴露 JMS 会话和消息生成器.

org.springframework.jms.support 提供 JMSException 的转换功能. 转换将选中的 JMSException 层次结构转换为未选中的异常的镜像层次结构. 如果已选中的 javax.jms.JMSException 有任何提供程序特定的子类, 则此异常将包装在未选中的 UncategorizedJmsException 中 (选中: checked,未选中: unchecked) .

org.springframework.jms.support.converter 提供了一个 MessageConverter 的抽象, 用于在 Java 对象和 JMS 消息之间进行转换.

org.springframework.jms.support.destination 为管理 JMS 目的地提供了各种策略, 例如为存储在 JNDI 中的目的地提供服务定位器.

org.springframework.jms.annotation 提供了必要的基础架构, 以支持使用 @JmsListener 的注解驱动的监听器端点.

org.springframework.jms.configjms 命名空间提供解析器实现, 以及 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 回调接口在 JmsTemplate 的调用代码提供的会话中创建一条消息. 为了允许更复杂地使用 JMS API, 回调 SessionCallback 为用户提供了 JMS 会话, 回调 ProducerCallback 暴露了一个会话和 MessageProducer 配对.

JMS API 暴露了两种类型的发送方法, 一种是将传递模式、优先级和生存作为服务质量 (Qos) 参数, 另一个不采用使用默认值的 Qos 参数. 由于 JmsTemplate 中有许多发送方法, 所以 QoS 参数的设置已作为 bean 属性暴露, 以避免发送方法的数量重复. 同样, 使用属性 setReceiveTimeout 设置同步接收调用的超时值.

某些JMS提供程序允许通过 ConnectionFactory 的配置管理默认 QoS 值的设置. 这会影响到对 MessageProducer 的 send 方法(send(Destination destination, Message message))的调用将使用与 JMS 规范中指定的不同的 QoS 默认值. 为了提供对 QoS 值的一致管理,因此必须通过将布尔属性 isExplicitQosEnabled 设置为 true 来明确地启用 JmsTemplate 以使用其自身的 QoS 值.

为方便起见, JmsTemplate 还暴露了一个基本的请求-答复操作, 允许发送消息并等待在作为操作一部分创建的临时队列上的答复.

JmsTemplate 类的实例在配置后是线程安全的. 这很重要, 因为这意味着您可以配置 JmsTemplate 的单个实例, 然后将此共享引用安全地插入到多个协作者中. 要清楚, JmsTemplate 是有状态的, 因为它维护对 ConnectionFactory 的引用, 但此状态不是会话状态.

在 Spring 4.1 框架中, JmsMessagingTemplate 是在 JmsTemplate 之上构建的, 它提供了与消息抽象(即 org.springframework.messaging.Message)的集成.这使您可以创建以通用方式发送的消息.

4.1.2. Connections

JmsTemplate 需要 ConnectionFactory 的引用,ConnectionFactory 是 JMS 规范的一部分,是使用 JMS 的入口点. 它由客户端应用程序用作工厂来创建与 JMS 提供程序的连接,并封装各种配置参数,其中许多是特定于供应商的,如 SSL 配置选项.

在 EJB 中使用 JMS 时, 供应商提供 JMS 接口的实现, 以便它们可以参与声明性事务管理并执行连接和会话池. 为了使用此实现, Java EE 容器通常要求您将 JMS 连接工厂声明为 EJB 或 Servlet 部署描述符内的 resource-ref. 为了确保将这些功能与 EJB 中的 JmsTemplate 一起使用, 客户端应用程序应确保它引用 ConnectionFactory 的托管实现.

缓存消息资源

标准 API 涉及创建许多中间对象,要发送消息, 请执行以 'API' 步骤:

ConnectionFactory->Connection->Session->MessageProducer->send

ConnectionFactorySend 操作之间, 有三个中间对象被创建和销毁. 为优化资源使用和提高性能, 提供了两个 ConnectionFactory 的实现.

使用 SingleConnectionFactory

Spring 提供了 ConnectionFactory 接口的实现, SingleConnectionFactory 将在所有 createConnection() 调用中返回相同的 Connection, 并忽略 close() 的调用. 这对于测试和独立环境非常有用, 以便可以将相同的连接用于可能跨越任意数量事务的多个 JmsTemplate 调用,SingleConnectionFactory 引用的标准 ConnectionFactory 通常来自 JNDI.

使用 CachingConnectionFactory

CachingConnectionFactory 继承了 SingleConnectionFactory 的功能, 并添加了 Session, MessageProducer, 和 MessageConsumer 的缓存. 初始缓存大小设置为 1, 使用属性 sessionCacheSize 增加缓存会话的数量. 请注意, 实际缓存会话的数目将超过该数目, 因为会话是根据其确认模式缓存的, 因此, 当 sessionCacheSize 被设置为一个、每个确认模式为一个时, 最多可以有 4 个缓存会话实例. MessageProducerMessageConsumer 在其所属会话中被缓存, 并且在缓存时也考虑了生产者和使用者的独特属性. MessageProducers 根据其目的地进行缓存,MessageConsumers 是根据由目的地、选择器、noLocal传递标志和持久性订阅名称(如果创建耐用的使用者)组成的键进行缓存的.

临时队列和主题(TemporaryQueue/TemporaryTopic)的 MessageProducers 和 MessageConsumers 永远不会被缓存。 不幸的是,WebLogic JMS 恰好在其常规目标实现上实现了临时队列/主题接口,错误地表明它的任何目标都不能被缓存。 请在 WebLogic 上使用不同的连接池/缓存,或为 WebLogic 目的自定义 CachingConnectionFactory

4.1.3. 目的地管理

目的地,如 ConnectionFactory,是 JMS 管理的对象,可以在 JNDI 中进行存储和检索. 配置 Spring 应用程序上下文时,可以使用 JNDI 工厂类 JndiObjectFactoryBean/<jee:jndi-lookup> 对对象对 JMS 目标的引用执行依赖注入. 但是,如果应用程序中有大量目的地,或者 JMS 提供程序具有唯一的高级目的地管理功能,则此策略通常很麻烦. 此类高级目的地管理的示例包括创建动态目的地或支持目的地的分层命名空间.JmsTemplate将目标名称的解析委托给 JMS 目标对象,以实现接口 DestinationResolver. DynamicDestinationResolverJmsTemplate 使用的默认实现,可容纳解析动态目标. 还提供了一个 JndiDestinationResolver ,它充当 JNDI 中包含的目标的服务定位器,并可选择退回到 DynamicDestinationResolver 中包含的行为.

在 JMS 应用程序中使用的目的地通常只在运行时才知道,因此在部署应用程序时无法进行管理创建. 这通常是因为根据一个众所周知的命名约定,在运行时创建目的地的交互系统组件之间存在共享的应用程序逻辑. 尽管创建动态目的地不是 JMS 规范的一部分,但大多数供应商都提供了此功能. 动态目的地是使用用户定义的名称来创建的,它们将它们与临时目的地区分开来,并且通常不在JNDI中注册. 由于与目的地关联的属性是特定于供应商的,因此用于创建动态目的地的 API 因提供程序而异. 但是,供应商有时所做的简单实现选择是忽略JMS规范中的警告, 并使用 TopicSession 方法的 createTopic(String topicName) 字符串 topicNameQueueSession 方法的 createQueue(String queueName) 字符串 queueName 来创建具有默认目的地属性的新目的地. 根据供应商实施情况, DynamicDestinationResolver 可能还会创建物理目的地,而不是仅解析一个.

布尔属性 pubSubDomain 用于配置 JmsTemplate, 并了解所使用的 JMS 域. 默认情况下, 此属性的值为 false, 表示将使用点到点域( Queues). JmsTemplate 使用的此属性通过 DestinationResolver 接口的实现确定动态目的地解析的行为.

还可以通过属性 defaultDestinationJmsTemplate 配置为默认目的地,默认目的地将与不引用特定目的地的发送和接收操作一起使用.

4.1.4. 消息监听容器

在 EJB 世界中,JMS 消息最常见的一种用法是驱动消息驱动 bean(mdb). Spring 提供了一种以不将用户与EJB容器绑定的方式创建消息驱动的 pojo(mdp) 的解决方案 (请查看 异步接收: 消息驱动的 POJO:消息驱动的 POJOs,用于详细报道 Spring 的 MDP 支持) . 在 Spring 4.1 框架后,端点方法可以简单地用 @JmsListener 注解,请参见 注解驱动监听器端点.

消息监听器容器用于接收来自 JMS 消息队列的消息,并驱动向其注入的 MessageListener. 监听器容器负责消息接收的所有线程处理,并将其分派到监听器中以进行操作. 消息监听器容器是 MDP 和消息传递提供程序之间的中介,它负责注册接收消息、参与事务、获取和释放资源、异常转换和类似的操作. 这样,您就可以作为应用程序开发人员编写与接收消息(可能响应)相关的(可能是复杂的)业务逻辑,并将样板 JMS 基础结构的关注委托给框架.

有两个标准的 JMS 消息监听器容器与 Spring 一起打包, 每个都有其专门的功能集

使用 SimpleMessageListenerContainer

SimpleMessageListenerContainer 消息监听器容器是两种标准中比较简单的一种,它在启动时创建一个固定数量的 JMS 会话和用户, 使用标准的 JMS 的 MessageConsumer.setMessageListener() 方法注册监听器, 并将它留在 JMS 提供程序中以执行监听器回调. 此变量不允许动态适应运行时要求或参与外部托管事务. 兼容性方面,它非常接近独立JMS规范的精神,但通常与 Java EE 的 JMS 限制不兼容.

虽然 SimpleMessageListenerContainer 不允许参与外部管理的事务,它支持本地 JMS 事务: 只需将 sessionTransacted 标志切换为 true ,或在命名空间中,将 acknowledge 属性设置为 transacted. 从您的监听器抛出的异常然后导致回滚,并重新传递消息. 或者,考虑使用 CLIENT_ACKNOWLEDGE 模式,该模式在异常的情况下也提供重新传递,但不使用事务会话实例,因此不包括事务协议中的任何其他会话操作 (例如发送响应消息) .
默认的 AUTO_ACKNOWLEDGE 模式不提供适当的可靠性保证. 当监听器执行失败时 (由于提供程序在监听器调用后自动确认每条消息,没有异常传播到提供程序) 或监听器容器关闭时 (可以通过设置 acceptMessagesWhileStopping 标志来配置) ,消息可能会丢失. 确保在可靠性需求的情况下使用事务处理会话 (例如,用于可靠的队列处理和持久主题订阅) .
使用 DefaultMessageListenerContainer

DefaultMessageListenerContainer 消息监听器容器是大多数情况下使用的. 与 SimpleMessageListenerContainer 相比,此容器变量允许动态地适应运行时需求,并能够参与外部托管事务. 当配置了 JtaTransactionManager 时,每个收到的消息都在XA事务中注册. 因此,处理可以利用 XA 事务语义. 此监听器容器在 JMS 提供程序的低需求、高级功能(如参与外部托管事务以及与 Java EE 环境的兼容性)之间取得了良好的平衡.

您可以自定义容器的缓存级别. 请注意,如果未启用缓存,则会为每个消息接收创建新连接和新会话. 将其与具有高负载的非持久性订阅相结合可能会导致消息丢失. 请确保在这种情况下使用适当的缓存级别.

当代理发生故障时,此容器还具有可恢复的功能. 默认情况下,简单的 BackOff 实现每五秒重试一次. 您可以为更细粒度的恢复选项指定自定义 BackOff 实现. 有关示例,请参阅 ExponentialBackOff.

与它的同级的SimpleMessageListenerContainer一样,DefaultMessageListenerContainer 支持本地JMS事务并允许自定义确认模式. 如果您的方案可行,则强烈建议在外部管理的事务上进行此操作; 即,如果您可以在 JVM 死亡的情况下使用偶尔重复的消息. 业务逻辑中的自定义重复消息检测步骤可能涵盖此类情况,例如以业务实体存在检查或协议表检查的形式. 任何此类安排都将比替代方案更有效. 用XA事务包装您的整个处理(通过配置您的 DefaultMessageListenerContainerJtaTransactionManager,涵盖 JMS 消息的接收以及消息监听器(包括数据库操作等)中业务逻辑的执行.
默认的 AUTO_ACKNOWLEDGE 模式不提供适当的可靠性保证. 当监听器执行失败时 (由于提供程序在监听器调用后自动确认每条消息,没有异常传播到提供程序) 或监听器容器关闭时 (可以通过设置 acceptMessagesWhileStopping 标志来配置) ,消息可能会丢失. 确保在可靠性需求的情况下使用事务处理会话 (例如,用于可靠的队列处理和持久主题订阅) .

4.1.5. 事务管理

Spring 提供了一个 JmsTransactionManager,用于管理单个 JMS ConnectionFactory 事务. 这使得 JMS 应用程序能够利用 如数据访问章节的事务管理部分所述. JmsTransactionManager 执行本地资源事务,将 JMSConnection/Session 对从指定的 ConnectionFactory 绑定到线程. JmsTemplate 自动检测此类事务性资源,并据此对其进行操作.

在 Java EE 环境中, ConnectionFactory 将建立连接池和会话池,因此这些资源可以在事务之间有效地重用. 在独立的环境中,使用 Spring 的 SingleConnectionFactory 将导致共享的JMS Connection,每个事务都有自己的独立 Session. 或者,考虑使用提供程序特定的池适配器,如 ActiveMQ 的 PooledConnectionFactory 类.

JmsTemplate 还可以与 JtaTransactionManager 和具有 XA 能力的 JMSConnectionFactory 一起使用,用于执行分布式事务. 请注意,这需要使用 JTA 事务管理器以及正确配置XA的 ConnectionFactory. (查看 Java EE 服务器或 JMS 提供程序的文档. )

使用 JMS API 从 Connection 创建 Session 时,在托管和非托管事务环境中重用代码可能会造成混淆. 这是因为 JMS API 只有一个工厂方法来创建 Session,它需要事务和确认模式的值. 在托管环境中,设置这些值是环境的事务基础结构的责任,因此供应商的JMS连接包装器会忽略这些值. 在非托管环境中使用 JmsTemplate 时,可以通过使用属性 sessionTransactedsessionAcknowledgeMode 来指定这些值. 当您将 PlatformTransactionManagerJmsTemplate 一起使用时,模板始终会被赋予事务性JMS Session.

4.2. 发送消息

JmsTemplate 包含许多发送消息的便捷方法. Send 方法使用 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");
            }
        });
    }
}

在前面的示例中,通过将引用传递给 ConnectionFactory 来构造 JmsTemplate . 作为替代方法,提供了无参数构造函数和 connectionFactory,可用于在 JavaBean 样式(使用 BeanFactory 或纯 Java 代码)中构造实例. 或者,考虑从 Spring 的 JmsGatewaySupport 方便基类扩展,它为 JMS 配置提供了预构建的 bean 属性.

send(String destinationName, MessageCreator creator) 方法允许您使用目的地的字符串名称发送消息. 如果这些名称是在 JNDI 中注册的,则应将该模板的 destinationResolver 属性设置为 JndiDestinationResolver 的实例.

如果创建了 JmsTemplate 并指定了默认目的地, 则 send(MessageCreator c) 将向该目的地发送一条消息.

4.2.1. 使用消息转换器

为了便于发送 domain 模型对象, JmsTemplate 有多种发送方法,它们将 Java 对象作为消息的数据内容的参数. JmsTemplate 中的重载方法 convertAndSend()receiveAndConvert() 将转换过程委托给 MessageConverter 接口的实例. 此接口定义在 Java 对象和 JMS 消息之间转换的简单协定. 默认实现 SimpleMessageConverter 支持 StringTextMessage, byte[]BytesMessage, 和 java.util.MapMapMessage

沙箱当前包含一个 MapMessageConverter,它使用反射在 JavaBean 和 MapMessage 之间进行转换. 您可能自己实现的其他流行实现选择是使用现有 XML 编组包 (例如JAXB,Castor或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.2.2. 使用 SessionCallbackProducerCallback

虽然发送操作涉及许多常见的使用方案,但在需要对 JMS 会话或 MessageProducer 执行多个操作时,也会出现这种情况. SessionCallbackProducerCallback 分别暴露 JMS SessionSession / MessageProducer 对. JmsTemplate 上的 execute() 方法执行这些回调方法.

4.3. 接收消息

这描述了如何在 Spring 中使用 JMS 接收消息.

4.3.1. 同步接收

虽然 JMS 通常与异步处理相关联, 但可以同步使用消息. 重载 receive(..) 方法可提供此功能. 在同步接收过程中, 调用线程会一直阻塞, 直到消息变为可用. 这可能是一个危险的操作, 因为调用线程可能会无限期地被阻塞. receiveTimeout 属性指定接收方在放弃等待消息之前应等待的时间.

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 接口类似的约定,但也提供消息处理方法访问接收消息的 JMS 会话. 以下清单显示了 SessionAwareMessageListener 接口的定义:

public interface SessionAwareMessageListener {

    void onMessage(Message message, Session session) throws JMSException;
}

如果希望您的 MDP 能够响应任何收到的消息(通过使用 onMessage(Message, Session) 方法中提供的会话),您可以选择让您的 MDP 实现此接口(优先于标准的 JMS MessageListener 接口). 与 Spring 一起发送的所有消息监听器容器实现都支持实现 MessageListenerSessionAwareMessageListener 接口的 MDP. 实现 SessionAwareMessageListener 的类随之而来的警告是,它们将通过接口绑定到 Spring . 选择是否使用它完全由您作为应用程序开发人员或架构师来决定.

请注意,SessionAwareMessageListener 接口的 onMessage(..) 方法会抛出 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. 还请注意各种消息处理方法是如何根据它们可以接收和处理的各种消息类型的内容来强类型化的.

现在考虑 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>

下面是另一个 MDP 的示例,它只能处理 JMS TextMessage 消息的接收. 请注意,消息处理方法实际上被称为 receive (MessageListenerAdapter 中的消息处理方法的名称默认为 handleMessage),但它是可配置的(如下所示). 还请注意, receive(..) 方法是如何强类型化的,以便只接收和响应 JMS TextMessage 消息. 以下清单显示了 TextMessageDelegate 接口的定义:

public interface TextMessageDelegate {

    void receive(TextMessage message);
}

以下清单显示了一个实现 TextMessageDelegate 接口的类:

public class DefaultTextMessageDelegate implements TextMessageDelegate {
    // implementation elided for clarity...
}

随后 MessageListenerAdapter 的配置如下:

<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
    <constructor-arg>
        <bean class="jmsexample.DefaultTextMessageDelegate"/>
    </constructor-arg>
    <property name="defaultListenerMethod" value="receive"/>
    <!-- we don't want automatic message context extraction -->
    <property name="messageConverter">
        <null/>
    </property>
</bean>

请注意,如果 messageListener 接收到 TextMessage 以外类型的 JMS 消息,则抛出 IllegalStateException (随后又消除了) . MessageListenerAdapter 类的另一个功能是,如果处理程序方法返回非 void 值,则能够自动发回响应消息. 考虑以下接口和类:

public interface ResponsiveTextMessageDelegate {

    // notice the return type...
    String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
    // implementation elided for clarity...
}

如果上述 DefaultResponsiveTextMessageDelegateMessageListenerAdapter 一起使用,则从执行 'receive(..)' 方法返回的任何非 null 值将(在默认配置中)转换为 TextMessage. 然后,生成的 TextMessage 将被发送到原始消息的 JMS 答复属性中定义的目标(如果存在), 或 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 Our transaction manager.

4.4. 用于支持 JCA 消息端点

从版本 2.5 开始, Spring 还提供对 JCA-based 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>

或者,您可以用给定的 ActivationSpec 对象设置一个 JmsMessageEndpointManager. 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 也可以指向特定于环境的线程池,通常是通过 SimpleTaskWorkManagerasyncTaskExecutor 属性. 如果碰巧使用多个适配器,请考虑为所有 ResourceAdapter 实例定义一个共享线程池.

在某些环境(如 WebLogic 9+)中,整个 ResourceAdapter 对象可以从JNDI获得,而不是使用 <jee:jndi-lookup>,然后, Spring 消息监听器可与服务器托管的 ResourceAdapter 进行交互,也可以使用服务器的内置 WorkManager.

有关更多详细信息,请参阅 javadoc 以获取 JmsMessageEndpointManager, JmsActivationSpecConfig, 和 ResourceAdapterFactoryBean

Spring 还提供了一个通用的 JCA 消息端点管理器,它与JMS无关: org.springframework.jca.endpoint.GenericMessageEndpointManager. 此组件允许使用任何消息监听器类型(如 JMS MessageListener)和任何提供程序特定的 ActivationSpec 对象,查看您的 JCA 提供者的文档,了解连接的实际功能,并查阅 GenericMessageEndpointManager 的 JavaDoc 以了解 Spring 特定的配置详细信息.

JCA-based 消息端点管理非常类似于 EJB2.1 消息驱动的 bean. 它使用相同的基础资源提供程序协定,与 EJB2.1 MDB 一样,您的 JCA 提供程序支持的任何消息监听器接口也可以在 Spring 上下文中使用. 不过, 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 方法可以设置成 3 个核心线程和 10 个最大线程的调用.

可以使用注解自定义监听器容器工厂,或者通过实现 JmsListenerConfigurer 接口来配置显式的默认值. 只有在没有特定容器工厂的情况下注册了至少一个端点时,才需要默认值. 有关完整的详细信息和示例,请参考 JmsListenerConfigurer.

如果你倾向于使用 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 . 请注意,此消息包含自定义和标准 header(由 JmsHeaders 定义).

  • @Header-annotated 方法参数, 用于提取特定的 header 值, 包括标准的 JMS 头.

  • @Headers-annotated 方法注解参数,它还必须可赋给 java.util.Map 以获得对所有标题的访问权限.

  • 非注解元素类型 (例如 MessageSession) 是支持的,可以被处理. 您可以通过用 @Payload 注解参数来显式使用它,您还可以通过添加额外的 @Valid 来启用验证.

注入 Spring 消息抽象的能力对于从存储在特定于传输的消息中的所有信息中受益, 而不依赖于特定于传输的 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 的返回类型. 在这种情况下,调用的结果将封装在原始消息的 JMSReplyTo 头中指定的目的地中, 或者在监听器上配置的默认目的地 javax.jms.Message 中发送. 现在可以使用消息抽象的 @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 提供了一个用于简化 JMS 配置的 XML 命名空间. 要使用 JMS 命名空间元素, 您需要引用 JMS schema,如以下示例所示:

<?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 Referencing the JMS schema.

命名空间包含三个顶级的元素: <annotation-driven/>, <listener-container/><jca-listener-container/>. <annotation-driven/> 允许使用注解驱动的监听器端点, <listener-container/><jca-listener-container/> 定义了共享的监听器容器配置,并且可能包含 <listener/> 子元素 . 下面是两个监听器的基本配置示例:

<jms:listener-container>

    <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>

    <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>

</jms:listener-container>

上面的示例等效于创建两个不同的监听器容器 bean 和两个不同的 MessageListenerAdapter bean定义. ,如使用 MessageListenerAdapter 中所示. 除了前面示例中显示的属性之外,listener 元素还可以包含几个可选的属性. 下表描述了所有可用属性:

Table 3. Attributes of the JMS <listener> element
属性 描述

id

监听器容器的 Bean 名称. 如果未指定,则自动生成 bean 名称.

destination (required)

此监听器的目的地,通过 DestinationResolver 策略解析.

ref (required)

处理程序对象的 bean 名称.

method

要调用的处理程序方法的名称. 如果 ref 属性指向 MessageListener 或 Spring SessionAwareMessageListener,则可以省略此属性.

response-destination

要将响应消息发送到的默认响应目标的名称. 这适用于请求消息未携带 JMSReplyTo 字段的情况. 此目标的类型由监听器容器的 response-destination-type 属性确定.

subscription

持久性订阅的名称 (如果有) .

selector

此监听器的可选消息选择器.

concurrency

为此监听器启动的 sessions 或 consumers 数量,可以是指示最大数量的一个简单的数字 (例如,5) ,也可以是指示下限和上限的范围 (例如, 3-5) . 请注意,指定的最小值只是一个提示,可能在运行时被忽略. 默认值是容器提供的值.

<listener-container/> 元素也接受几个可选属性. 这允许自定义各种策略(例如 taskExecutordestinationResolver),以及基本的 JMS 设置和资源引用. 使用这些属性,可以实现高度自定义的监听器容器,同时仍可从命名空间的方便性中获益.

通过指定要通过 factory-id 属性暴露的 bean id,可以自动将此类设置暴露为 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 及其具体子类的 javadocs. javadocs 还提供了有关事务选择和消息重整方案的讨论.

Table 4. JMS <listener-container> 元素属性
属性 描述

container-type

此监听器容器的类型. 可用选项为 default, simple, default102, 或 simple102 (默认选项为 default) .

container-class

自定义监听器容器实现类作为完全限定的类名. 根据 container-type 属性,默认值为 Spring 的标准 DefaultMessageListenerContainerSimpleMessageListenerContainer.

factory-id

将此元素定义的设置暴露为具有指定 idJmsListenerContainerFactory,以便它们可以与其他端点一起使用.

connection-factory

对 JMS ConnectionFactory bean 的引用 (默认 bean 名称为 connectionFactory) .

task-executor

对 JMS 监听器调用者的 Spring TaskExecutor 的引用.

destination-resolver

DestinationResolver 策略的引用,用于解析 JMS Destination 实例.

message-converter

对用于将 JMS 消息转换为监听器方法参数的 MessageConverter 策略的引用. 默认值为 SimpleMessageConverter.

error-handler

ErrorHandler 策略的引用,用于处理在执行 MessageListener 期间可能发生的任何未捕获的异常.

destination-type

此监听器的 JMS 目标类型: queue, topic, durableTopic, sharedTopicsharedDurableTopic. 这可能会启用容器的 pubSubDomain, subscriptionDurablesubscriptionShared 属性. 默认值为 queue (禁用这三个属性) .

response-destination-type

响应的 JMS 目标类型: queue or topic. 默认值是 destination-type 属性的值.

client-id

此监听器容器的 JMS 客户端 ID. 您必须在使用持久订阅时指定它.

cache

JMS 资源的缓存级别: none, connection, session, consumerauto. 默认情况下 (auto) ,缓存级别实际上是 consumer,除非指定了外部事务管理器 - 在这种情况下,有效默认值为 none (假设 Java EE 样式的事务管理,其中给定的 ConnectionFactory 是XA感知的池) .

acknowledge

本地 JMS 确认模式: auto, client, dups-oktransacted. transacted 激活本地 transacted 的会话. 作为替代方法,您可以指定 transaction-manager 属性,稍后将在表中进行说明. 默认为 auto.

transaction-manager

对外部 PlatformTransactionManager 的引用 (通常是基于XA的事务协调器,例如 Spring 的 JtaTransactionManager) . 如果未指定,则使用本地确认 (请参阅 acknowledge 属性) .

concurrency

为每个监听器启动的并发会话或使用者数. 它可以是指示最大数量的简单数字 (例如,5) ,也可以是指示下限和上限的范围 (例如,3-5) . 请注意,指定的最小值只是一个提示,可能在运行时被忽略. 默认值为 1.如果是主题监听器或队列排序很重要,则应将并发限制为 1. 考虑将其提升为一般队列.

prefetch

要加载到单个会话中的最大消息数. 请注意,提高此数字可能会导致并发消费者的饥饿.

receive-timeout

用于接收呼叫的超时 (以毫秒为单位) . 默认值为 1000 (一秒) . -1 表示没有超时.

back-off

指定用于计算恢复尝试之间间隔的 BackOff 实例. 如果 BackOffExecution 实现返回 BackOffExecution#STOP,则监听器容器不会进一步尝试恢复. 设置此属性时,将忽略 recovery-interval 值. 默认值为 FixedBackOff,间隔为5000毫秒 (即5秒) .

recovery-interval

指定恢复尝试之间的间隔 (以毫秒为单位) . 它提供了一种使用指定间隔创建 FixedBackOff 的便捷方法. 有关更多恢复选项,请考虑指定 BackOff 实例. 默认值为 5000 毫秒 (即5秒) .

phase

此容器应开始和停止的生命周期阶段. 值越低,此容器启动越早,后者停止. 默认值为 Integer.MAX_VALUE,表示容器尽可能晚启动并尽快停止.

使用 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 变量的可用配置选项:

Table 5. JMS <jca-listener-container/> 元素属性
属性 描述

factory-id

将此元素定义的设置暴露为具有指定 idJmsListenerContainerFactory,以便它们可以与其他端点一起使用.

resource-adapter

对JCA ResourceAdapter bean的引用 (默认 bean 名称为 resourceAdapter) .

activation-spec-factory

JmsActivationSpecFactory 的引用. 默认情况是自动检测JMS提供程序及其 ActivationSpec 类 (请参阅 DefaultJmsActivationSpecFactory) .

destination-resolver

DestinationResolver 策略的引用,用于解析 JMS Destinations.

message-converter

对用于将 JMS 消息转换为监听器方法参数的 MessageConverter 策略的引用. 默认值为 SimpleMessageConverter.

destination-type

此监听器的 JMS 目标类型: queue, topic, durableTopic, sharedTopicsharedDurableTopic. 这可能会启用容器的 pubSubDomain, subscriptionDurablesubscriptionShared 属性. 默认值为 queue (禁用这三个属性) .

response-destination-type

响应的 JMS 目标类型: queue or topic. 默认值是 destination-type 属性的值.

client-id

此监听器容器的 JMS 客户端 ID. 在使用持久订阅时需要指定它.

acknowledge

本地 JMS 确认模式: auto, client, dups-ok, 或 transacted. transacted 激活本地 transacted 的会话. 或者,您可以指定稍后描述的 transaction-manager 属性. 默认为 auto.

transaction-manager

对 Spring JtaTransactionManagerjavax.transaction.TransactionManager 的引用,用于为每个传入消息启动XA事务. 如果未指定,则使用本地确认 (请参阅 acknowledge 属性) .

concurrency

为每个监听器启动的并发会话或使用者数. 它可以是指示最大数量的简单数字 (例如 5) 或指示下限和上限的范围 (例如,3-5) . 请注意,指定的最小值只是一个提示,在运行时通常会在使用JCA监听器容器时被忽略. 默认值为 1.

prefetch

要加载到单个会话中的最大消息数. 请注意,提高此数字可能会导致并发消费者的饥饿.

5. JMX

Spring 中的 JMX (Java 管理扩展) 支持提供了一些功能, 使您可以轻松, 透明地将 Spring 应用程序集成到 JMX 基础结构中.

JMX?

本章不是 JMX 的介绍. 我们不会解释您为什么要使用 JMX. 如果您是 JMX 的新手,请参阅本章末尾的 更多资源 .

具体来说, Spring 的 JMX 支持提供了四个核心功能:

  • 将任何 Spring bean 自动注册为 JMX MBean.

  • 一种灵活的机制,用于控制 bean 的管理界面.

  • 声明远程暴露 MBeans、JSR-160 连接

  • 本地和远程 MBean 资源的简单代理

这些功能设计意图是不将您的应用程序和 Spring 或 JMX 接口和类耦合. 实际上, 在大多数情况下, 您的应用程序类不必知道 Spring 或 JMX,就可以利用 Spring JMX 功能.

5.1. 暴露你的 bean 给 JMX

Spring 的 JMX 框架中的核心类是 MBeanExporter. 这个类是负责收集您的 Spring bean 和注册他们到 JMX MBeanServer, 例如,请考虑以下类:

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 实例所述.

使用此配置,testBeanObjectName bean 下暴露为 bean:name=testBean1. 默认情况下,bean 的所有 public 属性都作为属性暴露,所有 public 方法 (从 Object 类继承的公共方法除外) 都作为操作暴露.

MBeanExporter 是一个有生命周期的 bean (查看 启动和关闭回调).默认情况下,在应用程序生命周期中尽可能晚地暴露 mbean. 通过设置 autoStartup 标志,可以配置暴露发生的阶段或禁用自动注册.

5.1.1. 创建一个 MBeanServer

上面的 配置假定应用程序在已运行的一个(且只有一个) MBeanServer 的环境中运行. 在这种情况下, Spring 将尝试定位运行的 MBeanServer,并将您的 bean 注册到该服务器(如果有的话). 当应用程序在诸如 Tomcat 或 IBMWebSphere 等具有自己的 MBeanServer 的容器中运行时,此行为非常有用.

但是,这种方法在独立的环境中是没有用的,或者在不提供 MBeanServer 的容器内运行. 为了解决这个问题,您可以通过将 org.springframework.jmx.support.MBeanServerFactoryBean 类的实例添加到您的配置中以声明方式创建一个 MBeanServer 实例. 还可以通过将 MBeanExporterserver 属性的值设置为 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 的平台或情况,应使用 factory-method,如以下示例所示:

<beans>
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="server">
            <!-- Custom MBeanServerLocator -->
            <bean class="platform.package.MBeanServerLocator" factory-method="locateMBeanServer"/>
        </property>
    </bean>

    <!-- other beans here -->

</beans>

5.1.3. 延迟初始化的 MBean

如果您配置的 bean 中还配置了用于延迟初始化的 MBeanExporter,则 MBeanExporter 将不会中断此关系,并将避免对 bean 进行实例处理. 相反,它将向 MBeanServer 注册一个代理,并将从容器中获取 bean,直到出现对代理的第一个调用.

5.1.4. 自动注册MBeans

任何通过 MBeanExporter 暴露的 bean 和已经有效的 MBeans 都是在 MBeanExporter 的情况下注册到 MBeanServer. 通过将 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 自动注册. 默认情况下,Spring bean 在 JMX 注册时自动使用他们的 name 作为 ObjectName. 您可以覆盖此行为,如控制 Bean 的 ObjectName 实例中所述.

5.1.5. 控制注册的行为

请考虑这样一个场景,即 Spring MBeanExporter 尝试使用对象 bean:name=testBean1 将 MBean 注册到 MBeanServer. 如果 MBean 实例已在同一 ObjectName 下注册,则默认行为是失败(并引发 InstanceAlreadyExistsException).

可以控制 MBeanMBeanServer 中注册时所发生的行为, Spring 的 JMX 支持允许三种不同的注册行为在注册过程发现 MBean 已在同一 ObjectName 下注册时控制注册行为. 下表汇总了这些注册行为:

Table 6. 注册行为
注册行为 说明

FAIL_ON_EXISTING

这是默认的注册行为. 如果 MBean 实例已在同一 ObjectName 下注册,则未注册正在注册的 MBean,并抛出 InstanceAlreadyExistsException. 现有 MBean 不受影响.

IGNORE_EXISTING

如果 MBean 实例已在同一 ObjectName 下注册,则未注册正在注册的 MBean. 现有 MBean 不受影响,并且不会抛出异常. 这在多个应用程序想要在共享 MBeanServer 中共享公共 MBean 的设置中很有用.

REPLACE_EXISTING

如果 MBean 实例已在同一 ObjectName 下注册,则先前注册的现有 MBean 将取消注册,并且新 MBean 将在其位置注册 (新 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属性和操作暴露. 为了精确控制导出的 bean 的哪些属性和方法实际上作为 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 可以使用源代码级元数据为 bean 定义管理接口. 元数据的读取由 org.springframework.jmx.export.metadata.JmxAttributeSource 接口封装. Spring JMX 提供了一个使用 Java 标注的默认实现, 即 org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource. MetadataMBeanInfoAssembler 必须配置为 JmxAttributeSource 接口的实现实例才能正常工作(没有默认值).

要将 bean 标记为要暴露的 JMX,应使用 ManagedResource 注解对bean类进行注解. 要作为操作暴露的每个方法都必须用 ManagedOperation 注解进行标记,并且要暴露的每个属性都必须用 ManagedAttribute 注解标记. 在标记属性时,可以忽略 getter 的注解或 setter 来分别创建只写或只读属性.

一个 ManagedResource 注解的 bean 必须是 public 的,因为方法需要暴露操作或属性.

以下示例显示了我们在 创建一个 MBeanServer 中使用的 JmxTestBean 类的注解形式:

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 的各个方面,稍后将在 源码级别的元数据类型 中进行更详细的说明.

你也将注意到 agename 属性被注解使用了 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 的 MBean 的元数据驱动管理接口所需的全部内容.

5.2.3. 源码级别的元数据类型

以下源码级别元数据类型可在 Spring JMX 中使用:

Table 7. 源码级别的元数据类型
目的 注解 注解类型

Class 的所有实例标记为JMX托管资源.

@ManagedResource

Class

将方法标记为JMX操作. .

@ManagedOperation

Method

将getter或setter标记为JMX属性的一半.

@ManagedAttribute

Method (only getters and setters)

定义操作参数的描述.

@ManagedOperationParameter and @ManagedOperationParameters

Method

下表描述了可在这些源级元数据类型上使用的配置参数:

Table 8. 源码级别的元数据参数
参数 描述 应用于

ObjectName

MetadataNamingStrategy 用于确定受管资源的 ObjectName.

ManagedResource

description

设置资源,属性或操作的描述.

ManagedResource, ManagedAttribute, ManagedOperation, or ManagedOperationParameter

currencyTimeLimit

设置 currencyTimeLimit 描述符字段的值.

ManagedResource or ManagedAttribute

defaultValue

设置 defaultValue 描述符字段的值.

ManagedAttribute

log

设置 log 描述符字段的值.

ManagedResource

logFile

设置 logFile 描述符字段的值.

ManagedResource

persistPolicy

设置 persistPolicy 描述符字段的值.

ManagedResource

persistPeriod

设置 persistPeriod 描述符字段的值.

ManagedResource

persistLocation

设置 persistLocation 描述符字段的值.

ManagedResource

persistName

设置 persistName 描述符字段的值.

ManagedResource

name

设置操作参数的显示名称.

ManagedOperationParameter

index

设置操作参数的索引.

ManagedOperationParameter

5.2.4. 使用 AutodetectCapableMBeanInfoAssembler 接口

为了进一步简化配置, Spring 引入了扩展 MBeanInfoAssembler 接口的 AutodetectCapableMBeanInfoAssembler 接口,以添加对 MBean 资源自动检测的支持. 如果您将 MBeanExporter 配置为 AutodetectCapableMBeanInfoAssembler 实例,则允许对包含在 JMX 中的 bean 进行 "vote".

AutodetectCapableMBeanInfo 接口唯一一个开箱即用的实现是 MetadataMBeanInfoAssembler,它将投票包括任何用 ManagedResource 属性标记的 bean. 在这种情况下,默认的方法是使用 bean 名称作为 ObjectName,从而产生如下的配置:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <!-- notice how no 'beans' are explicitly configured here -->
        <property name="autodetect" value="true"/>
        <property name="assembler" ref="assembler"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

    <bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
        <property name="attributeSource">
            <bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
        </property>
    </bean>

</beans>

请注意,在此配置中没有将 bean 传递给 MBeanExporter. 但是,JmxTestBean 仍将注册,因为它是用 ManagedResource 属性标记的,而 MetadataMBeanInfoAssembler 检测到此值并将其包括在内. 这种方法的唯一问题是,JmxTestBean 的名称现在具有业务含义. 您可以通过更改控制 bean 的 ObjectName 中定义的 为你的 bean 控制 ObjectName 实例 创建的默认行为来解决此问题.

5.2.5. 使用 Java 接口定义管理接口

除了 MetadataMBeanInfoAssembler, Spring 还包括有 InterfaceBasedMBeanInfoAssembler, 它允许您根据接口集合中定义的方法集来约束所暴露的方法和属性.

尽管用于暴露 mbean 的标准机制是使用接口和简单的命名方案,但 InterfaceBasedMBeanInfoAssembler 通过删除对命名约定的需求来扩展此功能,从而允许您使用多个接口并消除对 bean 的需要以实现 MBean 接口.

请考虑此接口, 用于为先前看到的 JmxTestBean 类定义管理接口:

public interface IJmxTestBean {

    public int add(int x, int y);

    public long myOperation();

    public int getAge();

    public void setAge(int age);

    public void setName(String name);

    public String getName();

}

此接口定义将在 JMXMBean 上作为操作和属性暴露的方法和属性. 下面的代码演示如何将 Spring JMX配置为使用此接口作为管理接口的定义:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean5" value-ref="testBean"/>
            </map>
        </property>
        <property name="assembler">
            <bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
                <property name="managedInterfaces">
                    <value>org.springframework.jmx.IJmxTestBean</value>
                </property>
            </bean>
        </property>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

在这里您可以看到,InterfaceBasedMBeanInfoAssembler 配置为在为任何 bean 构建管理接口时使用 IJmxTestBean 接口. 重要的是要了解, InterfaceBasedMBeanInfoAssembler 处理的 bean 不需要实现用于生成 JMX 管理界面的接口.

在上面的例子中,IJmxTestBean 接口用于构造所有 bean 的所有管理接口. 在许多情况下,这不是所需的行为,您可能希望对不同的 bean 使用不同的接口. 在这种情况下, 您可以通过 interfaceMappings 属性传递 InterfaceBasedMBeanInfoAssemblerProperties 实例用于该 bean,其中每个条目的键是 bean 名称,每个条目的值是一个逗号分隔的接口名称列表.

如果没有通过 managedInterfacesinterfaceMappings 属性指定管理接口,则 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>

在这里您可以看到,方法 addmyOperation 将暴露为 JMX 操作,getName(), setName(String), 和 getAge() 将暴露为 JMX 属性的一部分. 在上面的代码中, 方法映射适用于向 JMX 暴露的 bean. 要想逐个 bean 控制方法的暴露,请使用 MethodNameMBeanInfoAssemblermethodMappings 属性将 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 实例,并将其配置为从 Properties 实例读取 ObjectName, 而不是使用 bean key. KeyNamingStrategy 将会尝试定位 Properties 找到一个对应于 bean key 的项. 如果未找到任何项, 或者 Properties 实例为 null, 则使用 bean key 本身.

以下代码显示了 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, 因为这是 Properties 实例中具有对应于 bean key 的项.

如果无法找到 Properties 实例中的条目, 则使用 bean key 的名称作 为 ObjectName.

5.3.2. 使用 MetadataNamingStrategy

MetadataNamingStrategy 使用每个 bean 上的 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. Spring framework.jmx.export.naming.MetadataNamingStrategy">
        <property name="attributeSource" ref="attributeSource"/>
    </bean>

    <bean id="attributeSource"
            class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

</beans>

如果没有为 ManagedResource 提供 objectName 属性,则会使用下面的格式创建 objectName: [fully-qualified-package-name]:type=[short-classname],name=[bean-name]. 例如,下面的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 基于元数据(自动检测也总是启用). 实际上, @EnableMBeanExport @Configuration 注解支持更简单的语法,而不是定义 MBeanExporter bean. 如下例所示:

@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 注解自动检测结合使用. 基于代理 "hide" 目标类,它还隐藏 JMX 托管资源注解. 因此,在这种情况下使用目标类代理:通过在 <aop:config/> 上设置 'proxy-target-class' 标志, <tx:annotation-driven/> 等. 否则,您的 JMXbean 在启动时可能会被悄悄忽略.

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:9875JMXConnectorServer . 因此, serverConnector bean 通过 JMXMP 端口 9875 上的本地主机协议向客户端暴露 MBeanServer . 请注意,JMXMP 协议被 JSR160 规范标记为可选的: 目前,主要的开源 JMX 实现有: MX4J 和一个 JDK 不支持的 JMXMP.

要指定另一个 URL 并在 MBeanServer 中注册 JMXConnectorServer 本身,请分别使用 serviceUrlObjectName 属性. 如下:

<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 ,将自动在该 ObjectName 下的 MBeanServer 中注册您的连接器. 下面的示例显示了在创建 JMXConnector 时可以传递给 ConnectorServerFactoryBean 的完整参数集:

<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-based 连接器时,需要启动查找服务(tnameservrmiregistry)才能完成名称注册. 如果您使用 Spring 向您通过 RMI 导出远程服务,则 Spring 将已经构建了 RMI 注册表. 如果不是,您可以使用下面的配置片段轻松地启动注册表. 如下:

<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
    <property name="port" value="1099"/>
</bean>

5.4.2. 客户端连接器

要创建 MBeanServerConnection 到远程 JSR-160 启用的 MBeanServer, 请使用 MBeanServerConnectionFactoryBean,如下所示:

<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
    <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>

5.4.3. 基于 Hessian/SOAP 的 JMX

JSR-160 允许扩展到客户端和服务器之间进行通信的方式.前面部分中显示的示例使用 JSR-160 规范 (IIOP 和 JRMP) 和 (可选) JMXMP 所需的基于 RMI 的强制实现. 通过使用其他提供程序或 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 创建的 bean:name=testBean. 代理将实现的接口由 proxyInterfaces 属性控制,这些接口上的映射方法和属性的规则与 MBean 上的操作和属性是 InterfaceBasedMBeanInfoAssembler 使用的相同规则.

MBeanProxyFactoryBean 可以为任何可通过 MBeanServerConnection 访问的 MBean 创建代理. 默认情况下,本地 MBeanServer 位于和使用,但您可以重写此操作,并提供指向远程 MBeanServerMBeanServerConnection,以满足指向远程 mbean 的代理:

<bean id="clientConnector"
        class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
    <property name="serviceUrl" value="service:jmx:rmi://remotehost:9875"/>
</bean>

<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName" value="bean:name=testBean"/>
    <property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/>
    <property name="server" ref="clientConnector"/>
</bean>

在这里您可以看到,我们创建一个 MBeanServerConnection 指向远程机器使用 MBeanServerConnectionFactoryBean. 然后,通过 server 属性将此 MBeanServerConnection 传递到 MBeanProxyFactoryBean . 创建的代理将通过此 MBeanServerConnection 将所有调用转发到 MBeanServer .

5.6. 通知

Spring JMX 提供了 JMX 全面通知的支持

5.6.1. 注册通知的监听器

Spring 的 JMX 支持使您可以很容易地注册任何数量的 NotificationListeners 与任何数量的 MBean(这包括 Spring 的 MBeanExporter 暴露的 MBean 和通过其他机制注册的 MBean. 例如,考虑每当目标 MBean 的属性发生变化时,希望 (通过 Notification) 通知的场景. 以下示例将通知写入控制台:

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 通知时,都将通知通过 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>

如果要为封闭的 MBeanExporter 导出的所有 bean 注册单个 NotificationListener 实例,则可以使用特殊通配符 (*) 作为 notificationListenerMappings 属性映射中的条目的键,如以下示例所示:

<property name="notificationListenerMappings">
    <map>
        <entry key="*">
            <bean class="com.example.ConsoleLoggingNotificationListener"/>
        </entry>
    </map>
</property>

如果需要执行相反的流程(即,对一个 MBean 注册多个不同的监听器),则必须改用 notificationListeners 属性(并且优先于 notificationListenerMappings 属性). 这一次, 不是简单地为单个 MBean 配置 NotificationListener, 而是配置 NotificationListenerBean 实例. NotificationListenerBean 封装了 NotificationListenerObjectName(或 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 时都要给出一个 handback 对象,而且我们还希望通过提供一个 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 通知模型'的 JMX 规范 (1.2) 部分.

5.6.2. 发布通知

Spring 提供的支持不仅用于注册接收 Notifications , 而且还用于发布 Notifications . Spring 提供的支持不仅用于注册接收 Notifications , 而且还用于发布 Notifications .

请注意, 这一节讨论的是只与 Spring 管理的 bean, 是真正通过 MBeanExporter 暴露的 MBean; 任何现有的、用户定义的 MBean 都应使用标准 JMX API 进行通知发布.

Spring 的 JMX 通知发布支持中的关键接口是 NotificationPublisher 接口 (在 org.springframework.jmx.export.notification 包中定义) . 任何将通过 MBeanExporter 实例导出为 MBean 的 bean 都可以实现相关的 NotificationPublisherAware 接口以获取对 NotificationPublisher 实例的访问权限. NotificationPublisherAware 接口通过一个简单的 setter 方法向实现bean提供 NotificationPublisher 实例,然后 bean 可以使用它来发布 Notifications.

NotificationPublisher 类的 javadocs 中所述,通过 NotificationPublisher 机制发布事件的托管 bean 不负责任何通知监听器的状态管理和类似. Spring 的 JMX 支持将负责处理所有 JMX 基础问题, 应用程序开发人员需要做的事就是实现 NotificationPublisherAware 接口并使用提供的 NotificationPublisher 实例启动发布事件. 请注意, NotificationPublisher 将在托管bean注册到 MBeanServer 后设置.

使用 NotificationPublisher 实例非常简单,一个简单地创建一个 JMXNotification 实例(或一个适当的 Notification 子类的实例),是一个用与要发布的事件相关的数据填充通知,然后一个调用 NotificationPublisher 实例上的 sendNotification(Notification),传递 Notification.

在以下示例中, JmxTestBean 暴露的实例将在每次调用 add(int, int) 操作时发布 NotificationEvent :

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 支持的一个更好的特性. 然而它来与你的类的 price 标签耦合 Spring 和 JMX; 一如既往,,这里的建议是真诚的. 如果您需要 NotificationPublisher 提供的功能,并且您可以接受与 Spring 和 JMX 的耦合,那么就这样做.

5.7. 更多资源

本节包含有关 JMX 的更多资源的链接:

6. 电子邮件

本节介绍如何使用 Spring Framework 发送电子邮件.

依赖库

以下 JAR 需要位于应用程序的类路径中才能使用 Spring Framework 的电子邮件库:

该库可在 Web 上免费获取 - 例如,在 Maven Central 中以 com.sun.mail:jakarta.mail 的形式提供. 请确保使用最新的 1.6.x 版本 而不是 Jakarta Mail 2.0 (带有不同的程序包名称) .

Spring 提供了一个发送电子邮件的高级抽象层,它向用户屏蔽了底层邮件系统的一些细节,同时代表客户端负责底层的资源处理.

org.springframework.mail 包是 Spring Framework 电子邮件支持的主要包,它包括了发送电子邮件的主要接口 MailSender,和值对象 SimpleMailMessage,它封装了简单邮件的属性如 fromto (以及许多其他邮件) . 此程序包还包含已检查异常的层次结构,这些异常提供较低级别邮件系统异常的更高抽象级别,根异常为 MailException. 有关富邮件异常层次结构的详细信息,请参阅 javadoc.

为了使用 JavaMail 中的一些特色,比如 MIME 类型的信件,Spring 提供了 MailSender 的一个子接口 (内嵌了的),即 org.springframework.mail.javamail.JavaMailSender. JavaMailSender 还提供了一个回调接口 org.springframework.mail.javamail.MimeMessagePreparator,用于准备 MimeMessage.

6.1. 使用

假设我们有一个名为 OrderManager 的业务接口,如下例所示:

public interface OrderManager {

    void placeOrder(Order order);

}

进一步假设我们要求说明需要生成带有订单号的电子邮件消息并将其发送给下达相关订单的客户.

6.1.1. MailSenderSimpleMailMessage 的基本用法

以下示例显示了当有人下订单时如何使用 MailSenderSimpleMailMessage 发送电子邮件:

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="customerservice@mycompany.example"/>
    <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. 使用 JavaMailSenderMimeMessagePreparator

本节描述了使用 MimeMessagePreparator 回调接口的 OrderManager 的另一个实现. 在以下示例中,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("mail@mycompany.example"));
                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 的一些合适的连接点 (joinpoint)中被执行了.

Spring Framework 的邮件支持附带标准的 JavaMail 实现. 有关更多信息,请参阅相关的 javadoc.

6.2. 使用 JavaMail MimeMessageHelper

org.springframework.mail.javamail.MimeMessageHelper 是处理 JavaMail 邮件时比较顺手组件之一. 它可以让你摆脱繁复的 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("test@host.com");
helper.setText("Thank you for ordering!");

sender.send(message);

6.2.1. 发送附件和内部资源

Multipart email 允许添加附件和内嵌资源(inline resources). 内嵌资源可能是你在信件中希望使用的图片或样式表,但是又不想把它们作为附件.

附件

以下示例显示如何使用 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("test@host.com");

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("test@host.com");

// 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);
通过使用指定的 Content-ID (上例中的 identifier1234) 将内联资源添加到 MimeMessage. 添加文本和资源的顺序非常重要. 请务必先添加文本,然后再添加资源. 如果你反过来这样做,它就行不通.

6.2.2. 使用模板库创建电子邮件内容

在之前的代码示例中,所有邮件的内容都是显式定义的,并通过调用 message.setText(..) 来设置邮件内容. 这种做法针对简单的情况或在上述的例子中没什么问题. 因为在这里只是为了向你展示基础 API.

但是,在典型的企业应用程序中,开发人员通常不会使用之前显示的方法创建电子邮件内容,原因如下:

  • 使用 Java 代码创建基于 HTML 的电子邮件内容非常繁琐且容易出错.

  • 显示逻辑和业务逻辑之间没有明确的区别.

  • 更改电子邮件内容的显示结构需要编写 Java 代码,重新编译,重新部署等.

通常,解决这些问题的方法是使用模板库 (例如 FreeMarker) 来定义电子邮件内容的显示结构. 这使您的代码仅负责创建要在电子邮件模板中呈现的数据并发送电子邮件. 当您的电子邮件内容变得相当复杂时,这绝对是一种最佳实践,而且,使用 Spring Framework 的 FreeMarker 支持类,它变得非常容易.

7. 执行任务和任务计划

Spring 框架分别为异步执行、TaskExecutor 的任务调度和 TaskScheduler 接口提供了抽象. Spring 还具有支持线程池或委派到应用程序服务器环境 CommonJ 的接口实现. 最终, 在 Java SE 5、Java SE 6 和Java EE 等具有差异的环境实现了一套公共的抽象接口.

Spring 还支持使用 Timer (JDK自1.3以来的一部分) 和 Quartz Scheduler (www.quartz-scheduler.org/) 进行任务调度. 您可以使用 FactoryBean 同时分别对 TimerTrigger 实例进行可选引用来设置这两个调度程序. 此外,还提供了 Quartz Scheduler 和 Timer 的便捷类,它允许您调用现有目标对象的方法 (类似于正常的 MethodInvokingFactoryBean 操作) .

7.1. Spring TaskExecutor 抽象

Executors 是 JDK 中使用的线程池的名字,executor 意思是无法保证底层的实现是一个池,一个 executor 可以是单线程的或者是同步的. 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: 此实现使用 CommonJ WorkManager 作为其后备服务提供程序,并且是在 Spring 应用程序上下文中在 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的中心便利类.

  • 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 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 的方法,它只接受 RunnableDate. 这会导致任务在指定时间后运行一次. 所有其他方法都能够重复运行. 通过这些方法,可以实现一些简单的场景,比如需要以固定频率和固定时间间隔方法执行任务,但接受 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 表达式调度任务. 例如,以下任务在工作日的早上 9 点到下午 5 点 "营业时间" ,每小时15分钟内运行:

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

另一个现成的实现是 PeriodicTrigger,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值来表示该期间是否为固定速率或固定延迟. 由于 TaskScheduler 接口已经定义了以固定速率或固定延迟来调度任务的方法,因此应该优先直接使用这些方法. PeriodicTrigger 实现的价值在于,它可以在依赖于 Trigger 抽象的组件中使用. 例如,允许周期性触发器、cron-based 触发器、甚至是可互换使用的自定义触发器实现,可能会很方便. 此类组件可以利用依赖注入,这样可以在外部配置此类 Triggers, 因此容易修改或扩展.

7.2.3. TaskScheduler 实现

与 Spring 的 TaskExecutor 抽象一样, TaskScheduler 的主要好处是,依赖于调度行为的代码不必与特定的调度程序实现耦合. 当在应用程序服务器环境中运行时,不应由应用程序本身直接创建线程,因此提供的灵活性尤其重要. 对于这种情况, Spring 提供了一个 TimerManagerTaskScheduler,它委托给 WebLogic 或 WebSphere 上的 CommonJ TimerManager,以及一个委托给 Java EE 7+ 环境中的 JSR-236 ManagedScheduledExecutorService 的更新的 DefaultManagedTaskScheduler. 两者通常都配置有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. 对于更多的 fine-grained 控制,您可以另外实现 SchedulingConfigurerAsyncConfigurer 接口. 有关详细信息,请参阅对应的 javadocs.

如果您喜欢XML配置, 请使用 <task:annotation-driven> 元素. 如下:

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>

请注意,在上面的 XML 中,提供了一个 executor 引用来处理与 @Async 注解的方法对应的那些任务,并提供了调度程序引用来管理那些用 @Scheduled 注解的方法.

处理 @Async 注解的通知模式默认是 proxy,它允许仅通过代理拦截调用. 同一类中的本地调用不能以这种方式截获. 对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到 aspectj 模式.

7.3.2. @Scheduled 注解

可以将 @Scheduled 注解与触发器元数据一起添加到方法中. 例如, 下面的方法每隔 5 秒调用一次固定的延迟, 也就是从每次调用的完成时间开始计算:

@Scheduled(fixedDelay=5000)
public void doSomething() {
    // something that should run periodically
}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。 如果您想使用不同的时间单位,例如秒或分钟,您可以通过 @Scheduled 中的 timeUnit 属性进行配置。

例如,前面的例子也可以写成如下。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}

如果需要按照固定的速度来执行,只需简单的改变注解中的 fixedRate 属性名即可执行. 下面可以每 5 秒执行速度,也就是从每次调用的开始时间开始计算:

@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 4.3 框架中, 任何作用域的 bean 都支持 @Scheduled 方法.

请确保在运行时不会在同一个 @Scheduled 注解的类上初始化多个实例,除非您确实希望为每个此类实例都计划回调. 与此相关,不要在带有 @Scheduled 注解并注册为容器中的常规 Spring bean 类上使用 @Configurable 注解. 否则,您将获得两次初始化(一次通过容器,一次通过 @Configurable), 结果是每个 @Scheduled 方法都将被调用两次.

7.3.3. @Async 注解

可以在方法上提供 @Async 注解,使此方法进行异步调用. 换言之,调用方将在调用时立即返回,并且该方法的实际执行将发生在已提交到 Spring TaskExecutor 的任务中. 在最简单的情况下,注解可以应用于 void 返回方法. 如下:

@Async
void doSomething() {
    // this will be run asynchronously
}

与使用 @Scheduled 注解的注解方法不同,这些方法可以有入参的,因为调用方将在运行时以 "normal" 方式调用它们,而不是由容器管理的计划任务. 例如,以下是 @Async 注解的合法应用:

@Async
void doSomething(String s) {
    // this will be run asynchronously
}

即使返回值的方法也可以异步调用,但是,此类方法需要具有 Future 类型的返回值. 这仍然提供了异步执行的好处,以便调用者可以在调用 Future 上的 get() 之前执行其他任务. 以下示例显示如何在返回值的方法上使用 @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 版本后,存在于 JDK8 的 java.util.concurrent.CompletableFuture. 用于与异步任务进行更丰富的交互,以及通过进一步的处理步骤进行组合操作.

@Async 不能与生命周期回调(如 @PostConstruct)一起使用. 若要异步初始化 Spring bean,则当前必须使用单独的初始化 Spring 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 的 Executor 的条件

默认情况下,在方法上指定 @Async 时,使用的执行程序是 启用异步支持时配置的执行程序,例如,如果使用XML或 AsyncConfigurer 实现 (如果有) ,则为 “annotation-driven” 元素. 但是,当需要指示执行给定方法时,应使用非默认的执行器时, 可以使用 @Async 注解的 value 属性. 以下示例显示了如何执行此操作:

@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 类型的返回值时,可以很容易地管理在方法执行期间引发的异常,因为当调用对 Future 结果的 get 时将引发此异常. 但是,对于 void 返回类型,异常是无法捕获的,无法传输的. 对于这些情况,可以提供 AsyncUncaughtExceptionHandler 来处理此类异常. 以下示例显示了如何执行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // handle exception
    }
}

默认情况下,仅记录异常. 您可以使用 AsyncConfigurer<task:annotation-driven/> XML 元素定义自定义 AsyncUncaughtExceptionHandler.

7.4. task 命名空间

从 Spring 3.0 开始,有一个用于配置 TaskExecutorTaskScheduler 实例的 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 实现. 对于可以在高负载下跳过某些任务的应用程序,您可以改为配置 DiscardPolicyDiscardOldestPolicy. 另一个适用于需要在高负载下限制提交任务的应用程序的选项是 CallerRunsPolicy. 该策略不是抛出异常或丢弃任务,而是强制调用 submit 方法的线程自己运行任务. 这个想法是这样的调用者在运行该任务时很忙,并且不能立即提交其他任务. 因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制. 通常,这允许执行程序 "赶上" 它正在处理的任务,从而释放队列,池中或两者中的一些容量. 您可以从 executor 元素上的 rejection-policy 属性的可用值枚举中选择任何这些选项.

以下示例显示了一个 executor 元素,其中包含许多属性以指定各种行为:

<task:executor
        id="executorWithCallerRunsPolicy"
        pool-size="5-25"
        queue-capacity="100"
        rejection-policy="CALLER_RUNS"/>

最后, keep-alive 设置确定在终止之前线程可能保持空闲的时间限制(以秒为单位). 如果池中当前有多个线程的核心数目,则在等待此时间量后不处理任务,多余的线程将被终止. 时间值为零将导致多余的线程在执行任务后立即终止,而不需要在任务队列中保留后续工作. 以下示例将 keep-alive 值设置为两分钟:

<task:executor
        id="executorWithKeepAlive"
        pool-size="5-25"
        keep-alive="120"/>

7.4.3. 'scheduled-tasks' 元素

Spring 的任务命名空间的最强大功能是支持配置在 Spring 应用程序上下文中安排的任务. 这遵循了类似于 Spring 中其他 “method-invokers” 的方法,例如由 JMS 命名空间提供的用于配置消息驱动的 pojo. 基本上, ref 属性可以指向任何 Spring 管理的对象, method 属性提供要在该对象上调用的方法的名称. 以下清单显示了一个简单示例:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

scheduler 由外部元素引用,每个单独的任务都包括其触发器元数据的配置. 在前面的示例中,该元数据定义了一个定期触发器,它具有固定的延迟,表示每个任务执行完成后等待的毫秒数. 另一个选项是 "fixed-rate",表示无论以前执行多长时间,方法的执行频率. 此外, ,对于 fixed-delayfixed-rate 任务,可以指定 'initial-delay' 参数,指示在第一次执行该方法之前等待的毫秒数. 为了获得更多的控制,可以改为提供一个 cron 表达式. 下面是一个演示其他选项的示例:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
    <task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
    <task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

7.5. 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)
 │ │ │ │ │ │
 * * * * * *

有一些适用的规则:

  • 字段可以是星号 (*), 代表 “first-last”. 在 day-of-the-month 或 day-of-the-week 字段中, 可以使用问号 (?) 代替星号.

  • 逗号 (,) 用于分隔一个列表值.

  • 用 (-) 来指定一个范围.

  • 在一个范围 (或 *) 后面加上 / 指定该范围内的数字值的间隔.

  • 英文名称也可以用于 day-of-month 和 day-of-week 字段.使用特定日期或月份的前三个字母(大小写无关紧要).

  • day-of-month 和 day-of-week 字段可以包含一个 L 字符, L 字符具有不同的含义

    • 在 day-of-month 字段中, L 代表 一个月的最后一天. 如果后面跟着一个负的偏移量 (即, L-n), 它意味着从第 n 个月的最后一天.

    • 在 day-of-week 字段中, L 代表 一周的最后一天. 如果前缀是数字或三个字母的名字 (dLDDDL), 则表示 该月一周的最后一天 (dDDD).

  • day-of-month 字段可以是 nW, 表示 n 月的某一天最近的工作日. 如果 n 落在星期六, 则结果为星期五. 如果 n 落在星期天, 则结果为星期一, 如果 n1 并落在星期六, 则结果为星期一 (即: 1W 表示 该月的第一个工作日).

  • 如果 day-of-month 字段是 LW, 则表示 该月的最后一个工作日.

  • day-of-week 字段可以为 d#n (或 DDD#n), 表示 _一个月内 d (或 DDD) 星期的第 n 天. _.

这里有一些示例:

Cron Expression 意思

0 0 * * * *

top of every hour of every day

*/10 * * * * *

每 10 秒

0 0 8-10 * * *

每天的 8, 9 和 10 点

0 0 6,19 * * *

每天的 上午 6:00 AM 和 7:00 PM

0 0/30 8-10 * * *

每天的 8:00, 8:30, 9:00, 9:30, 10:00 和 10:30

0 0 9-17 * * MON-FRI

on the hour nine-to-five weekdays

0 0 0 25 DEC ?

every Christmas Day at midnight

0 0 0 L * *

last day of the month at midnight

0 0 0 L-3 * *

third-to-last day of the month at midnight

0 0 0 * * 5L

last Friday of the month at midnight

0 0 0 * * THUL

last Thursday of the month at midnight

0 0 0 1W * *

first weekday of the month at midnight

0 0 0 LW * *

last weekday of the month at midnight

0 0 0 ? * 5#2

the second Friday in the month at midnight

0 0 0 ? * MON#1

the first Monday in the month at midnight

7.5.1. Macros

诸如 0 0 * * * * 之类的表达式对于人类来说很难解析, 因此很难在发生错误的情况下进行修复. 为了提高可读性, Spring 支持以下宏, 它们代表常用的序列. 您可以使用这些宏而不是六位数字的值, 例如: @Scheduled(cron = "@hourly").

Macro Meaning

@yearly (或 @annually)

一年一次 (0 0 0 1 1 *)

@monthly

一月一次 (0 0 0 1 * *)

@weekly

一周一次 (0 0 0 * * 0)

@daily (或 @midnight)

一天一次 (0 0 0 * * *), 或

@hourly

一小时一次, (0 0 * * * *)

7.6. 使用 Quartz 的 Scheduler

Quartz使用 Trigger, Job, 和 JobDetail 等对象来进行各种类型的任务调度. 关于 Quartz 的基本概念,请参阅 www.quartz-scheduler.org/. 为了让基于 Spring 的应用程序方便使用,Spring 提供了一些类来简化 quartz 的用法.

7.6.1. 使用 JobDetailFactoryBean

Quartz JobDetail 对象保存运行一个任务所需的全部信息. Spring 提供一个叫作 JobDetailFactoryBean 的类让 JobDetail 能对一些有意义的初始值(从XML配置)进行初始化,让我们来看个例子:

<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>

job detail 配置拥有所有运行 job(ExampleJob)的必要信息. 可以通过 job 的data map 来指定 timeout. Job 的 data map可以通过 JobExecutionContext (在运行时传递给你)来得到,但是 JobDetail 同时把从job的 data map 中得到的属性映射到实际 job 中的属性中去. 所以,如果 ExampleJob 中包含一个名为 timeout 的属性,JobDetail 将自动为它赋值:

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
    }

}

data map 中的所有附加属性当然也可以使用的

使用 namegroup 属性,你可以分别修改 job 在哪一个组下运行和使用什么名称. 默认情况下,job 的名称等于 JobDetailFactoryBean 的名称 (在上面的例子中为 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>

上面例子将调用 exampleBusinessObject 中的 doIt 方法如下:

public class ExampleBusinessObject {

    // properties and collaborators

    public void doIt() {
        // do the actual work
    }
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

使用 MethodInvokingJobDetailFactoryBean 你不需要创建只有一行代码且只调用一个方法的 job, 你只需要创建真实的业务对象来包装具体的细节的对象.

默认情况下,Quartz Jobs 是无状态的,可能导致 jobs 之间互相的影响. 如果你为相同的 JobDetail 指定两个 Trigger, 很可能当第一个job完成之前,第二个 job 就开始了. 如果 JobDetail 对象实现了 Stateful 接口,就不会发生这样的事情. 第二个 job 将不会在第一个 job 完成之前开始. 为了使得 jobs 不并发运行,设置 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>
默认情况下,jobs 在并发的方式下运行.

7.6.3. 使用triggers和 SchedulerFactoryBean 来织入任务

我们已经创建了 job details,jobs. 我们同时回顾了允许你调用特定对象上某一个方法的便捷的 bean. 当然我们仍需要调度这些 jobs. 这需要使用 triggers 和 SchedulerFactoryBean 来完成. Quartz中提供了几个 triggers,Spring 提供了两个带有方便默认值的 Quartz FactoryBean 实现: CronTriggerFactoryBeanSimpleTriggerFactoryBean.

Triggers 也需要被调度. Spring 提供 SchedulerFactoryBean 来暴露一些属性来设置 triggers. SchedulerFactoryBean 负责调度那些实际的 triggers

以下清单使用 SimpleTriggerFactoryBeanCronTriggerFactoryBean:

<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>

现在我们创建了两个 triggers,其中一个开始延迟 10 秒以后每 50 秒运行一次,另一个每天早上 6 点钟运行. 我们需要创建一个 SchedulerFactoryBean 来最终实现上述的一切:

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="cronTrigger"/>
            <ref bean="simpleTrigger"/>
        </list>
    </property>
</bean>

更多的属性你可以通过 SchedulerFactoryBean 来设置,例如 job details 使用的日期, 用来订制 Quartz 的一些属性以及 Spring 提供的 JDBC DataSource. 你可以查阅 SchedulerFactoryBean 的 Javadoc.

SchedulerFactoryBean 还可以识别类路径中的 quartz.properties 文件,基于 Quartz 属性键,与常规 Quartz 配置一样。 请注意,许多 SchedulerFactoryBean 设置与属性文件中的常见 Quartz 设置重叠; 因此,不建议同时在两个级别上指定值。 例如,如果您要依赖 Spring 提供的数据源,请不要设置 "org.quartz.jobStore.class" 属性,或者指定一个的 org.springframework.scheduling.quartz.LocalDataSourceJobStore 变体 替换标准的 org.quartz.impl.jdbcjobstore.JobStoreTX

8. 缓存抽象

从 3.1 版本开始,Spring 框架为在现有的 Spring 应用程序透明地添加缓存提供了支持. 与 事务支持类似,缓存抽象允许一致地使用各种缓存解决方案,而对代码的影响最小.

在 Spring 4.1 中,通过 JSR-107 注解 和更多自定义选项的支持,缓存抽象得到了显着改进.

8.1. 了解缓存抽象

Cache vs Buffer

术语 “buffer” 和 “cache,” 倾向于可互换使用. 但请注意,它们代表不同的东西. 缓冲区通常用作快速和慢速实体之间的数据的中间临时存储区. 由于一方必须等待另一个影响性能的因素, 因此缓冲区会通过允许整个数据块同时移动, 而不是一小块来缓解这一问题. 数据只在缓冲区中写入和读取一次. 此外, 缓冲区至少对知道它的一方是可见的.

另一方面,理论上的缓存是隐藏的,任何一方都不知道缓存是否发生. 它还可以提高性能,而且相同的数据可以快速读取多次.

对两者更多的解释是在 here.

缓存抽象的核心是将缓存应用于Java方法,从而根据缓存中可用的信息减少执行次数. 也就是说,每次调用目标方法时,抽象都会应用一种缓存行为,该行为检查该方法是否已针对给定参数执行. 如果已执行,则返回缓存的结果,而不必执行实际方法. 如果该方法尚未执行,则执行该方法,并将结果缓存并返回给用户,以便下次调用该方法时,返回缓存的结果. 这样,对于给定的一组参数,昂贵的方法 (无论是 CPU 还是 IO) 只能执行一次,并且重用结果而不必再次实际执行该方法. 缓存逻辑是透明应用的,不会对调用者造成任何干扰.

此方法仅适用于保证为给定输入 (或参数) 返回相同输出 (结果) 的方法,无论它执行多少次.

其他与缓存相关的操作由抽象提供,如更新缓存内容的能力或删除所有条目中的一个. 如果缓存处理在应用程序过程中可以更改的数据,这些都是很有用的.

就像Spring框架中的其他服务一样,缓存服务是抽象(不是缓存实现),需要使用实际存储来存储缓存数据,也就是说,抽象使开发人员不必编写缓存逻辑,但不提供实际的存储. 此抽象由 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口具体化.

Spring 提供了一些抽象实现: 基于 JDK java.util.concurrent.ConcurrentMap 的缓存, Ehcache 2.x,Gemfire 缓存, Caffeine 和符合 JSR-107 的缓存 (例如 Ehcache 3.x) . 有关插入其他缓存存储和提供程序的详细信息,请参阅 各种各样的后端缓存插件.

缓存抽象没有针对多线程和多进程环境的特殊处理,因为这些功能由缓存实现处理.

如果您具有多进程环境 (即,部署在多个节点上的应用程序) ,则需要相应地配置缓存提供程序. 根据您的使用情况,几个节点上的相同数据的副本就足够了. 但是,如果在应用程序过程中更改数据,则可能需要启用其他传播机制.

在代码中直接添加缓存的经典流程有 get-if-not-found-then-proceed-and-put-eventually 这里没有用到锁. 同时允许多线程同步时加载同一个缓存. 同样对于回收缓存也是相似. 但如果有多个线程试图更新或者回收数据的话,可能会用到旧数据. 某些缓存为程序为该区域的操作提供了高级功能,请参阅您正在使用的缓存提供程序的文档以获取详细信息.

要使用缓存抽象,您需要注意两个方面:

  • 缓存声明: 确定需要缓存的方法及其策略.

  • 缓存配置: 存储数据并从中读取数据的后端缓存.

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 版本使用的键生成策略,对于多个键参数,只考虑参数的 hashCode() 而没有考虑到 equals() 方法,这将导致一定的碰撞 (见 SPR-10237) . 新的 SimpleKeyGenerator 为这种情况使用复合键.

如果您希望继续使用前面的 key strategy, 则可以配置已弃用的 org.springframework.cache.interceptor.DefaultKeyGenerator 类或创建自定义的基于 KeyGenerator 的实现.

自定义键生成器

由于缓存是具有普遍性的,因此目标方法很可能具有不同的签名,不能简单地映射到缓存结构的顶部. 当目标方法有多个参数时,问题显得更突出,其中只有一部分是适合于缓存的(其余仅由方法逻辑使用). 例如:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

乍一看,虽然两个 boolean 参数影响了 book 的发现方式,但它们对缓存没有用处. 如果两个中只有一个重要而另一个不重要怎么办?

对于这种情况,@Cacheable 注解允许用户指定 key 的生成属性. 开发人员可以使用 SpEL )来选择感兴趣的参数(或它们的嵌套属性)执行操作,甚至调用任意方法,而不必编写任何代码或实现任何接口. 这是在默认生成器.

以下示例是各种 SpEL 声明 (如果您不熟悉 SpEL,请阅读 Spring Expression Language) :

@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)
keykeyGenerator 参数是互斥的, 指定两者的操作将导致异常.
默认的缓存解析

缓存抽象使用一个简单的 CacheResolver (可即用) 它使用 CacheManager 配置检索在操作级别定义的缓存.

为了提供不同默认缓存解析,需要实现 org.springframework.cache.interceptor.CacheResolver 接口.

自定义缓存解析

默认的缓存解析适合于应用对于使用单个 CacheManager,并且不需要复杂的解析要求.

对于应用使用不同的缓存管理,可以设置 cacheManager 使用每个操作. 如以下示例所示:

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
1 指定 anotherCacheManager.

也可以完全以类似于 key generation的方式替换 CacheResolver. 为每个缓存操作请求该解决方案, 使实现能够根据运行时参数实际解析要使用的缓存. 以下示例显示如何指定 CacheResolver:

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
1 指定 CacheResolver.

Spring 4.1 版本以后, 缓存注解的 value 属性不再是强制性的了,因为无论注解的内容如何,CacheResolver 都可以提供此特定信息.

keykeyGenerator 似,cacheManagercacheResolver 参数是互斥的,同时指定两者的操作将导致异常,因为 CacheResolver 实现忽略了自定义 CacheManager. 这可能不是你所期望的.

同步缓存

在多线程环境中,对于同一参数(通常在启动时),可能会同时调用某些操. 默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而无法达到缓存的目的.

对于这些特定情况,可以使用 sync 属性指示基础缓存提供程序在计算值时锁定缓存项. 因此,只有一个线程将忙于计算该值,而其余的则被阻塞,直到在缓存中更新该项. 以下示例显示了如何使用 sync 属性:

@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
1 使用 sync 属性.
这是一个可选的功能,您喜欢的缓存库可能不支持它. 核心框架提供的所有 CacheManager 实现都支持它. 有关详细信息,请参阅缓存提供程序的文档,
条件缓存

有时,一个方法做缓存可能不是万能的(例如,它可能依赖于给定的参数). 缓存注解通过 condition 参数支持此类功能,它采用 SpEL 表达式,并将其计算为 truefalse. 如果为 true,则方法执行缓存. 如果不是,则它的行为就像不缓存的方法一样. 每次不管缓存中的值是什么或使用了什么参数,都将执行它. 例如,仅当参数名称的长度小于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 Using the unless attribute to block hardbacks.

缓存抽象支持 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. 由于它可能为 null,我们应该使用 安全导航操作符.

可用的缓存 SpEL 表达式内容

每个 SpEL 表达式都可以用于再次指定的 context 值. 除了在参数中生成外,框架还提供专用的与缓存相关的元数据(如参数名称). 下表列出了可用于context的项,以便可以使用这些项进行键和条件计算.

Table 9. 缓存SpEL可用的元数据
名字 位置 描述 例子

methodName

Root object

正在调用的方法的名称

#root.methodName

method

Root object

正在调用的方法

#root.method.name

target

Root object

正在调用的目标对象

#root.target

targetClass

Root object

被调用的目标的类

#root.targetClass

args

Root object

用于调用目标的参数 (作为数组)

#root.args[0]

caches

Root object

执行当前方法的高速缓存的集合

#root.caches[0].name

Argument name

Evaluation context

任何方法参数的名称. 如果名称不可用 (可能由于没有调试信息) ,参数名称也可以在 #a<#arg> 下获得,其中 #arg 代表参数索引 (从 0 开始) .

#iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias).

result

Evaluation context

方法调用的结果 (要缓存的值) . 仅在 unless 表达式,缓存 cache put 表达式 (计算key) 或缓存 cache evict 表达式 (当 beforeInvocationfalse 时) 中可用. 对于受支持的包装器 (例如 Optional) , #result 引用实际的对象,而不是包装器.

#result

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,它指示是否需要进行缓存范围的回收,而不是只执行一项(基于键):

@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.
Table 10. Cache 注解设置
XML 属性 注解属性 默认值 描述

cache-manager

N/A (see the CachingConfigurer javadoc)

cacheManager

要使用的缓存管理器的名称. 使用此缓存管理器 (或未设置的 cacheManager) 在后台初始化默认的 CacheResolver. 要获得更精细的缓存 fine-grained 管理,请考虑设置 'cache-resolver' 属性.

cache-resolver

N/A (see the CachingConfigurer javadoc)

A SimpleCacheResolver using the configured cacheManager.

用于解析后端缓存的 CacheResolver 的 bean 名称. 此属性不是必需的,只需要指定为 'cache-manager' 属性的替代.

key-generator

N/A (see the CachingConfigurer javadoc)

SimpleKeyGenerator

要使用的自定义键生成器的名称.

error-handler

N/A (see the CachingConfigurer javadoc)

SimpleCacheErrorHandler

要使用的自定义缓存错误处理程序的名称. 默认情况下,在缓存相关操作期间抛出的任何异常都会在客户端返回.

mode

mode

proxy

默认模式 (proxy) 使用 Spring 的 AOP 框架处理要注解的注解 bean (遵循代理语义,如前所述,仅适用于通过代理进入的方法调用) . 替代模式 (aspectj) 用 Spring 的 AspectJ 缓存方面编织受影响的类,修改目标类字节代码以应用于任何类型的方法调用. AspectJ 编织需要在类路径中使用 spring-aspects.jar 以及启用加载时织入 (或编译时织入) . (有关如何设置加载时织入的详细信息,请参阅Spring configuration . )

proxy-target-class

proxyTargetClass

false

仅适用于代理模式. 控制为使用 @Cacheable@CacheEvict 注解的类创建哪种类型的高速缓存代理. 如果 proxy-target-class 属性设置为 true,则创建基于类的代理. 如果 proxy-target-classfalse 或者省略了该属性,则会创建基于标准JDK接口的代理. (有关不同代理类型的详细检查,请参阅 代理机制

order

order

Ordered.LOWEST_PRECEDENCE

定义应用于使用 @Cacheable@CacheEvict 注解的 bean 的缓存通知的顺序. (有关与 AOP 排序 advice 相关的规则的更多信息,请参阅Advice Ordering. ) 没有指定的排序意味着 AOP 子系统确定 advice 的顺序.

<cache:annotation-driven/> 在 bean 中定义的应用程序上下文中只查找 @Cacheable/@CachePut/@CacheEvict/@Caching. 这意味着,如果你在 WebApplicationContext 中为 DispatcherServlet 放置 <cache:annotation-driven/>,他只会检查控制器中的 bean,而不是你的服务. 有关更多信息,请参阅MVC 部分.
方法可见和缓存注解

使用代理时,应仅将缓存注解应用于具有公共可见性的方法. 如果使用这些注解对受保护的、私有的或包可见的方法进行注解,虽然不会引发任何错误,但注解的方法不会显示已配置的缓存设置. 如果需要在更改字节码本身时对非公共方法进行注解,请考虑使用 AspectJ (请参阅本节的其余部分)

TIP

Spring 建议您只用 @Cache* 注解来注解具体类(还有具体类的方法),而不是注解接口. 当然,您可以将 @Cache* 注解放在接口(或接口方法)上,但这只适用于您在使用基于代理 (mode="proxy") 时所期望的效果. 如果您使用的是基于 weaving-based aspect(mode="aspectj"),则编织基础结构在接口级声明中无法识别缓存设置。,

在代理模式(默认情况下), 仅截获通过代理进入的外部方法调用. 这意味着,实际上,self-invocation 在目标对象中调用目标对象的另一种方法时,在运行时不会导致实际的缓存,即使调用的方法标记为 @Cacheable,考虑使用 aspectj 模式也是这种情况,此外,代理必须完全初始化以提供预期的行为,因此您不应依赖于初始化代码中的此功能,即 @PostConstruct.

8.2.7. 使用自定义的注解

自定义的注解和 AspectJ

此功能仅在基于方法的情况下工作,但可以通过使用AspectJ的额外工作来启用.

spring-aspects 模块定义了标准注解的切面. 如果你定义自己的注解,则还需要为这些注解定义一个切面. 请检查 AnnotationCacheAspect 以查看示例:

缓存抽象允许您使用自己的注解来识别触发缓存储存或回收的方法. 这在模板机制中非常方便,因为它消除了重复缓存注解声明的需要(在指定键或条件时尤其有用),或者在您的代码库中不允许使用外部导入(org.springframework). 与 stereotype 注解的其余部分类似, @Cacheable, @CachePut@CacheEvict, 和 @CacheConfig 可以用作 meta-annotations. 这是可以注解其他注解的注解 (就是元注解) . 在下面的示例中,我们使用自己的自定义注解替换常见的 @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) 注解

自 Spring 框架 4.1 以来,缓存抽象完全支持 JCache 标准 ((JSR-107) )注解: @CacheResult, @CachePut, @CacheRemove, 和 @CacheRemoveAll 以及 @CacheDefaults, @CacheKey, 和 @CacheValue. 这些注解可以正确地使用,而无需将缓存存储迁移到 JSR-107. 内部实现使用 Spring 的缓存抽象, 并提供默认的 CacheResolverKeyGenerator 实现,它们符合规范. 换言之,如果您已经在使用 Spring 的缓存抽象,则可以切换到这些标准注解,而无需更改缓存存储(或配置).

8.3.1. 特性总结

对于熟悉 Spring 缓存注解的用户,下表描述了 Spring 注解和 JSR-107 对应项之间的主要区别:

Table 11. Spring vs. JSR-107 caching annotations
Spring JSR-107 Remark

@Cacheable

@CacheResult

非常相似. @CacheResult 可以缓存特定的异常并强制执行该方法,而不管缓存的内容如何.

@CachePut

@CachePut

当 Spring 使用方法调用的结果更新缓存时,JCache 要求将其作为使用 @CacheValue 注解的参数传递. 由于这种差异,JCache 允许在实际方法调用之前或之后更新缓存.

@CacheEvict

@CacheRemove

非常相似. 当方法调用导致异常时,@CacheRemove 支持条件回收.

@CacheEvict(allEntries=true)

@CacheRemoveAll

@CacheRemove.

@CacheConfig

@CacheDefaults

允许您以类似的方式配置相同的概念.

JCache 具有与 Spring 的 CacheResolver 接口相同的 javax.cache.annotation.CacheResolver,但 JCache 只支持单个缓存. 默认情况下,一个简单的实现是根据注解上声明的名称检索要使用的缓存. 应该注意的是,如果在注解中没有指定缓存名称,则会自动生成默认值,参看 @CacheResult#cacheName() .

CacheResolver 实例由 CacheResolverFactory 检索. 可以为每个缓存操作自定义工厂,如以下示例所示:

@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1)
public Book findBook(ISBN isbn)
1 为此操作自定义工厂.
对于所有引用的类,Spring 尝试查找具有给定类型的 bean. 如果存在多个匹配项,则会创建一个新实例,并且可以使用常规 bean 生命周期回调(如依赖注入).

key 由 javax.cache.annotation.CacheKeyGenerator 方法生成,其作用与 Spring 的 KeyGenerator 一样. 默认情况下,所有方法参数都将被考虑,除非至少有一个参数是用 @CacheKey 注解. 这类似于 Spring 的自定义 key 生成器. 例如,同样的操作,一个使用 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. 这是一个永久性的失败,没有 book 可以用这样的参数检索. 下面抛出缓存异常,以便使用相同的无效 ISBN 进行进一步调用,直接抛出缓存的异常,而不是再次调用该方法:

@CacheResult(cacheName="books", exceptionCacheName="failures"
            cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)

8.3.2. 启用 JSR-107 支持

除了 Spring 的声明性注解支持之外,不需要做任何具体的工作来启用 JSR-107 支持. 如果 spring-context-support 模块已经在类加载路径中,那么使用 @EnableCaching 或者 cache:annotation-driven 元素都将自动启用 JCache 支持.

根据您的使用情况,选择使用与否由你选择. 您甚至可以使用 JSR-107 API 和其他使用 Spring 自己的注解来混合使用服务. 但是,请注意,如果这些服务影响到相同的缓存,则应使用一致的和相同的键生成实现.

8.4. 基于 XML 声明的缓存

如果注解不是可选的(不能访问源代码或没有外部码),则可以使用 XML 进行声明性缓存. 因此,您不必对缓存方法进行注解,而是在外部指定目标方法和缓存指令(类似于声明性事务管理 advice). 上一节中的示例可以转换为以下示例:

<!-- 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 中的所有方法,并将缓存 advice 应用于它们.

声明性 XML 缓存支持所有基于注解的模型,因此在两者之间转换应该相当简单. 在同一个应用程序中可以进一步使用它们. 基于 XML 的方法不会设计到目标代码,但是编写它非常冗长无聊. 在处理具有针对缓存的重载方法的类时,确定正确的方法确实需要额外工作,因为该方法参数不能很好的被辨别. 在这些情况下, 在这些情况下,您可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能. 然而,通过 XML,更容易应用在包/组/接口范围上的缓存(再次因为 Aspect 切点)和创建类似模板的定义(如我们在上面的例子中通过 cache 属性 目标的 cache:definitions 属性.

8.5. 配置缓存的存储

缓存抽象集成了多个存储功能,可以开箱即用. 为了使用他们,您需要声明一个适当的 CacheManager (一个控制和管理 Cache 实例的实体,可用于检索这些实例以进行存储) .

8.5.1. 基于JDKConcurrentMap 缓存

基于 JDK 的 Cache 实现位于 org.springframework.cache.concurrent 包下. 它允许您使用 ConcurrentHashMap 作为支持 Cache 存储. 以下示例显示如何配置两个缓存:

<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <property name="caches">
        <set>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
        </set>
    </property>
</bean>

上面的代码段使用 SimpleCacheManager 为名为 defaultbooks 的两个嵌套的 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.xml 读取整个特定于 ehcache 的配置.

8.5.3. Caffeine Cache

Caffeine 是 Java 8 重写了 Guava 的缓存,他的实现在 org.springframework.cache.caffeine 包中,并且提供了访问 Caffeine 特性的方法.

以下示例配置一个按需创建缓存的 CacheManager:

<bean id="cacheManager"
        class="org.springframework.cache.caffeine.CaffeineCacheManager"/>

还可以提供缓存以显式使用,在这种情况下,只有 manager 才能提供这些内容. 以下示例显示了如何执行此操作:

<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
    <property name="cacheNames">
        <set>
            <value>default</value>
            <value>books</value>
        </set>
    </property>
</bean>

Caffeine CacheManager 也支持自定义的 CaffeineCacheLoader. 查阅 Caffeine 文档 了解更多信息.

8.5.4. 基于 GemFire 的缓存

GemFire 是一个面向内存/磁盘存储的全局的备份数据库,它具有可伸缩的、可持续的、可扩展的、具有内置模式订阅通知功能等等特性. 全局复制的数据库. 并提供全功能的边缘缓存. 有关如何将 GemFire 用作 CacheManager (以及更多) 的更多信息,请参阅 Spring Data GemFire 参考文档.

8.5.5. JSR-107 缓存

JSR-107 兼容的缓存也可用于 Spring 的缓存抽象. 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 标志,添加了一个没有 op 的缓存,为所有的定义没有被配置的缓存管理器处理. 也就是说,在 jdkCachegemfireCache(上面配置)中找不到的每个缓存定义都将由 no-op 缓存来处理,并且不会存储目标方法的信息而方法每次都会被执行 (就是多配置成可执行的无缓存操作) .

8.6. 各种各样的后端缓存插件

显然,有大量的缓存产品可以用作后端存储. 要将它们集成,需要提供 CacheManagerCache 实现, 这听起来可能比使用它更难,因为在实践中,类往往是简单的 adapters,它将缓存抽象框架映射到存储 API 的之上, 就像 ehcache 类可以显示的那样. 大多数 CacheManager 类可以使用 org.springframework.cache.support 包中的类,如 AbstractCacheManager,它负责处理样板代码,只留下实际的映射即可结束工作. 我们希望,提供与 Spring 集成的库能够及时填补这一小的配置缺口.

8.7. 我可以如何设置 TTL/TTI/Eviction policy/XXX 特性?

直接通过缓存提供程序. 缓存抽象是抽象,而不是缓存实现. 您正在使用的解决方案可能支持不同的数据策略和不同的拓扑结构,而其他解决方案不会这样做(例如,JDK ConcurrentHashMap - 暴露在缓存抽象中将是无用的,因为没有后端支持). 应该通过后端缓存 (配置时) 或通过其本地API直接控制此类功能.

9. Appendix

9.1. XML Schemas

附录的这一部分列出了与集成技术相关的 XML Schemas.

9.1.1. jee Schema

jee 元素处理与 Java EE(Java Enterprise Edition) 配置相关的问题,例如查找 JNDI 对象和定义 EJB 引用.

要使用 jee schema 中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导码. 以下代码段中的文本引用了正确的 schema,以便 jee 命名空间中的元素可供您使用:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd">

    <!-- bean definitions here -->

</beans>
<jee:jndi-lookup/> (simple)

以下示例显示如何使用 JNDI 在没有 jee schema 的情况下查找数据源:

<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 schema查找数据源:

<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 Stateless SessionBean 的引用.

以下示例显示如何在没有 jee 的情况下配置对本地 EJB Stateless SessionBean 的引用:

<bean id="simple"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/RentalServiceBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>

以下示例显示如何使用 jee 配置对本地 EJB Stateless SessionBean 的引用:

<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"/>
<jee:local-slsb/> (复杂)

<jee:local-slsb/> 元素配置对本地 EJB Stateless SessionBean 的引用.

以下示例显示如何配置对本地 EJB Stateless SessionBean 的引用以及许多不带 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>

以下示例显示如何使用 jee 配置对本地 EJB Stateless SessionBean 和许多属性的引用:

<jee:local-slsb id="complexLocalEjb"
        jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true">
<jee:remote-slsb/>

<jee:remote-slsb/> 元素配置对 remoteEJB Stateless SessionBean 的引用.

以下示例显示如何在不使用 jee 的情况下配置对远程 EJB Stateless SessionBean 的引用

<bean id="complexRemoteEjb"
        class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/MyRemoteBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
    <property name="cacheHome" value="true"/>
    <property name="lookupHomeOnStartup" value="true"/>
    <property name="resourceRef" value="true"/>
    <property name="homeInterface" value="com.foo.service.RentalService"/>
    <property name="refreshHomeOnConnectFailure" value="true"/>
</bean>

以下示例显示如何使用 jee 配置对远程 EJB Stateless SessionBean 的引用:

<jee:remote-slsb id="complexRemoteEjb"
        jndi-name="ejb/MyRemoteBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true"
        home-interface="com.foo.service.RentalService"
        refresh-home-on-connect-failure="true">

9.1.2. jms Schema

jms 元素处理配置与JMS相关的 bean,例如 Spring 的 Message Listener Containers. 这些元素在 JMS 命名空间支持 中详细介绍. 有关此支持和 jms 元素本身的完整详细信息,请参阅该章节.

为了完整性,要使用 jms schema 中的元素,您需要在 Spring XML 配置文件的顶部包含以下前导码. 以下代码段中的文本引用了正确的 schema,以便您可以使用 jms 命名空间中的元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>

9.1.3. 使用 <context:mbean-export/>

配置基于注解的 MBean 导出中详细介绍了此元素.

9.1.4. cache Schema

您可以使用 cache 元素来启用对 Spring 的 @CacheEvict, @CachePut,和 @Caching 注解的支持. 它还支持基于声明的基于 XML 的缓存. 有关详细信息,请参阅启用缓存注解基于XML的声明性缓存 .

要使用 cache schema 中的元素,需要在 Spring XML 配置文件的顶部包含以下前导码. 以下代码段中的文本引用了正确的 schema,以便您可以使用 cache 命名空间中的元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">

    <!-- bean definitions here -->

</beans>