JmsTemplate和DefaultMessageListenerContainer是用于访问JMS兼容MOM的Spring帮助器。 他们的主要目标是在JMS API之上形成一层,并处理诸如事务管理/消息确认之类的基础结构,并隐藏JMS API的某些重复和笨拙的部分(保留在那里: JMS 2.0即将来临!)。 要使用这些帮助程序中的任何一个,都必须为其提供(至少) JMS ConnectionFactory和有效的JMS Destination 。
在应用程序服务器上运行应用程序时,很有可能使用JEE架构定义ConnectionFactory。 这简化了添加ConnectionFactory及其配置参数的过程,从而允许它们以给定的别名(例如jms / myConnectionFactory)在目录服务中发布。 在你内
应用程序,例如,如果需要更多配置来查找ConnectionFactory并将其传递给JmsTemplate和/或DefaultMessageListenerContainer,则可以使用JEE命名空间或JndiTemplate / JndiObjectFactoryBean bean中的“ jndi-lookup”。
后者是JMS目的地,标识要向其生成消息或从中使用消息的JMS队列或主题。 但是,这两个JmsTemplate作为DefaultMessageListenerContainer都有两个不同的属性用于注入目标。 有一种方法将目的地作为String ,将目的地作为JMS Destination类型。 Spring并没有发明这种功能, JMS规范提到了两种方法:
4.4.4 Creating Destination Objects
Most clients will use Destinations that are JMS administered objects that they have looked up via JNDI. This is the most portable approach.
Some specialized clients may need to create Destinations by dynamically manufacturing one using a provider-specific destination name.
Sessions provide a JMS provider-specific method for doing this.
如果将目标作为String传递,则助手将隐藏将它们映射到有效JMS目标所需的额外步骤。 最后,JMS会话上的createConsumer希望您在返回MessageConsumer之前传递Destination对象,以指示从何处使用消息。 当将目的地配置为String时,Spring会使用JMS API本身来查找目的地。 默认情况下,JmsTemplate和DefaultMessageListenerContainer具有对DestinationResolver的引用,默认情况下为DynamicDestinationResolver (稍后会对此进行详细介绍)。 下面的代码是DynamicDestinationResolver的摘录,突出显示的行指示使用JMS API将String转换为Destination(在此示例中为Queue):
protected Queue resolveQueue(Session session, String queueName) throws JMSException {if (session instanceof QueueSession) {// Cast to QueueSession: will work on both JMS 1.1 and 1.0.2return ((QueueSession) session).createQueue(queueName);}else {// Fall back to generic JMS Session: will only work on JMS 1.1return session.createQueue(queueName);}}
规范提到的另一种方法(JNDI方法)是将Destinations配置为应用程序服务器上的可管理对象。 这遵循ConnectionFactory的原理。 该目的地发布在应用程序服务器目录中,并且可以通过其JNDI名称(例如jms / myQueue)进行查找。 再次,您可以在应用程序中查找JMS目标,并使用以JMS目标为参数的属性将其传递给JmsTemplate和/或DefaultMessageListenerContainer。
现在,为什么我们有这两种选择?
我一直认为这是在方便性(动态方法)和环境透明性/可配置性(JNDI方法)之间选择的问题。 例如:在某些情况下,物理目标的名称可能会有所不同,具体取决于应用程序运行的环境。 如果在应用程序内部配置物理目标名称,则显然会失去此好处,因为如果不重建应用程序就无法更改它们。 另一方面,如果将它们配置为受管理对象,则只需更改应用程序服务器配置中的物理目标名称即可。
记得; 可以配置物理目标名称很有意义。 除了目标类型之外,处理消息传递的应用程序也不了解其详细信息。 消息传递目标没有功能协定,并且其任何属性(物理目标,持久性等)对于您编写的代码都不重要。 实际合同位于消息本身(标题和正文)内部。 另一方面,数据库表只是一个示例,它确实暴露了契约并与您的代码紧密结合。 在大多数情况下,重命名数据库表确实会影响您的代码,因此,与消息传递目标相比,使这种可配置项通常没有附加值。
最近,我发现我对这的理解还不是全部。 该规范(摘自上面某些段落的“ 4.4.4创建目标对象”)已经给出了提示:“大多数客户端将使用目标,这些目标是他们通过JNDI查找的JMS管理的对象。 这是最便携的方法。” 基本上,这告诉我们另一种方法(将目标作为String的动态方法)是“最少可移植”的方法。 对我来说,这从来都不是很清楚,因为每个提供程序都必须实现这两种方法,但是必须在更广泛的范围内考虑“便携式”。
当将Destination配置为String时,Spring在创建新的JMS Session时默认会将其转换为JMS Desintations。 当使用DefaultMessageListenerContainer消费消息时,您处理的每条消息都在事务中发生,并且默认情况下,JMS会话和使用者没有被合并,因此将为每个接收操作重新创建它们。 每次容器检查新消息和/或接收新消息时,这都会导致将String转换为JMS Destination。 “非便携式”方面发挥了作用,因为这还意味着此转换的细节和成本完全取决于MOM的驱动程序/实现。 在我们的案例中,我们在Oracle AQ作为MOM提供商方面经历了这一情况。 每当目标转换发生时,驱动程序都会执行一个特定的查询:
select /*+ FIRST_ROWS */ t1.owner, t1.name, t1.queue_table, t1.queue_type, t1.max_retries, t1.retry_delay, t1.retention, t1.user_comment, t2. type , t2.object_type, t2.secure
from all_queues t1, all_queue_tables t2
where t1.owner=:1 and t1.name=:2 and t2.owner=:3 and t1.queue_table=t2.queue_table
论坛条目可以在这里找到 。
尽管此查询在最新的驱动程序中得到了改进(如错误报告所提及),但它仍然导致数据库的大量开销。 解决此问题的两个选项:
- 执行规范建议的操作:将目标配置为应用程序服务器上的资源。 每次应用程序服务器都会分发相同的实例,因此它们已经被缓存在那里。 即使您每次查找都会收到相同的实例,但是在使用JndiTemplate(或JndiDestinationResolver,请参见下文)时,它也会在应用程序一侧被阻塞,因此即使查找本身也只会发生一次。
- 在DefaultMessageListenerContainer上启用会话/消费者缓存。 将缓存设置为使用方时,由于使用方持有对目标的引用,因此它还会间接重用目标。 这个池是Spring添加的功能, JavaDoc说它在使用资源本地事务时是安全的,而在使用XA事务时(在JBoss 4上运行除外)“应该”是安全的。
首先可能是最好的。 但是,在我们的情况下,所有目标均已在应用程序内定义(并且有很多目标),因此无需对其进行配置。 仅出于此技术原因而对它们进行重构将产生大量开销,而没有其他优势。 第二种解决方案是最不受欢迎的解决方案,因为这将意味着需要进行额外的测试和研究以确保没有任何问题。 同样,这似乎还需要做更多,因为在我们的案例中,没有迹象表明创建会话或使用者对性能有可衡量的影响。 根据JMS规范:
4.4 Session
A JMS Session is a single-threaded context* for producing and consuming
messages. Although it may allocate provider resources outside the Java virtual
machine, it is considered a lightweight JMS object.
顺便说一句; 这对于MessageConsumers / Producers也有效。 它们都绑定到一个会话,因此,如果一个会话轻量级可以打开,那么这些对象也将打开。
但是,还有第三种解决方案。 自定义的DestinationResolver。 DestinationResolver是负责从String到Destination的抽象。 缺省( DynamicDestinationResolver )在JMS会话上使用createConsumer(javax.jms.Destination)进行转换,但不会缓存生成的Destination。 但是,如果将Destinations在应用程序服务器上配置为资源,则可以(除了使用Spring的JNDI支持并直接注入Destination之外)还可以使用JndiDestinationResolver 。 该解析器会将提供的String视为JNDI位置(而不是物理目标名称),并为您执行查找。 默认情况下,它将缓存生成的目标,避免任何后续的JNDI查找。 现在,还可以将JndiDestinationResolver配置为DynamicDestinationResolver的缓存装饰器。 如果将fallback设置为true,它将首先尝试使用String作为从JNDI查找的位置,如果失败,它将使用JMS API将我们的String传递给DynamicDestinationResolver,以将我们的String转换为Destination。 在这两种情况下,都将生成的目标存储在缓存中,因此将从缓存中为对同一目标的下一个请求提供服务。 使用此解析器,可以直接使用一个解决方案,而无需编写任何代码:
<bean id="cachingDestinationResolver" class="org.springframework.jms.support.destination.JndiDestinationResolver"><property name="cache" value="true"/><property name="fallbackToDynamicDestination" value="true"/> </bean><bean id="infra.abstractMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer" abstract="true"><property name="destinationResolver" ref="cachingDestinationResolver"/>...</bean>
通过内部使用ConcurrentHasmap存储绑定,JndiDestinationResolver是线程安全的。 根据JMS 1.1规范(2.8多线程),JMS目标本身就具有线程安全性,并且可以安全地进行缓存:
这再次是一个很好的例子,说明简单的事情有时会产生重要的影响。 这次,借助Spring,解决方案非常简单。 但是,最好将缓存行为设置为默认值,因为这会使它与查找目的地的任何提供程序特定的怪癖脱钩。 这不是默认值的原因可能是因为DefaultMessageListenerContainer支持动态更改目的地 (例如,使用JMX):
Note: The destination may be replaced at runtime, with the listener container picking up the new destination immediately (works e.g. with DefaultMessageListenerContainer, as long as the cache level is less than CACHE_CONSUMER). However, this is considered advanced usage; use it with care!
翻译自: https://www.javacodegeeks.com/2013/04/jms-and-spring-small-things-sometimes-matter.html