Статьи

JMS и весна: мелочи иногда имеют значение

JmsTemplate и DefaultMessageListenerContainer являются помощниками Spring для доступа к JMS-совместимой MOM. Их главная цель — сформировать слой над JMS API и работать с такой инфраструктурой, как управление транзакциями / подтверждение сообщений и сокрытие некоторых повторяющихся и неуклюжих частей JMS API (повесить там: JMS 2.0 уже в пути!). Чтобы использовать любого из этих помощников, вы должны предоставить им (как минимум) JMS ConnectionFactory и действительное назначение JMS .

При запуске приложения на сервере приложений, ConnectionFactory, скорее всего, будет определяться с использованием архитектуры JEE. Это сводится к добавлению ConnectionFactory и его параметров конфигурации, позволяющих публиковать их в службе каталогов под заданным псевдонимом (например, jms / myConnectionFactory). Внутри вашего

Например, вы можете использовать «jndi-lookup» из пространства имен JEE или компонентов JndiTemplate / JndiObjectFactoryBean, если для поиска ConnectionFactory требуется дополнительная конфигурация и передать ее в JmsTemplate и / или DefaultMessageListenerContainer.

Последний, пункт назначения JMS, определяет очередь или тему JMS, для которых вы хотите создавать сообщения или получать сообщения. Однако оба JmsTemplate как DefaultMessageListenerContainer имеют два разных свойства для внедрения целевого объекта. Есть метод, принимающий назначение как String, а другой — тип назначения JMS . Эта функциональность не изобретена Spring, в спецификации JMS упоминаются оба подхода:

1
2
3
4
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.

Если вы передадите пункт назначения в виде строки, то помощники скроют дополнительные шаги, необходимые для сопоставления их с действительным пунктом назначения JMS. В конце концов createConsumer в сеансе JMS ожидает, что вы передадите объект Destination, чтобы указать, откуда получать сообщения перед возвратом MessageConsumer . Когда адресаты настроены как String, Spring ищет адресат с помощью самого API JMS. По умолчанию JmsTemplate и DefaultMessageListenerContainer имеют ссылку на DestinationResolver, который по умолчанию является DynamicDestinationResolver (подробнее об этом позже). Приведенный ниже код является выдержкой из DynamicDestinationResolver, выделенные строки указывают на использование JMS API для преобразования строки в пункт назначения (в данном примере это очередь):

01
02
03
04
05
06
07
08
09
10
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.2
  return ((QueueSession) session).createQueue(queueName);
 }
 else {
  // Fall back to generic JMS Session: will only work on JMS 1.1
  return session.createQueue(queueName);
 }
}

Другой способ, упомянутый в спецификации (подход JNDI), состоит в том, чтобы сконфигурировать Назначения как администрируемые объекты на вашем сервере приложений. Это следует принципу как с ConnectionFactory; место назначения публикуется в каталоге серверов приложений и может быть найдено по его имени JNDI (например, jms / myQueue). Опять же, вы можете найти JMS-адресат в своем приложении и передать его JmsTemplate и / или DefaultMessageListenerContainer, используя свойство, принимающее JMS-адресат в качестве параметра.

Теперь, почему у нас есть эти два варианта?

Я всегда предполагал, что это был вопрос выбора между удобством (динамический подход) и прозрачностью / конфигурируемостью среды (подход JNDI). Например: в некоторых ситуациях имя физического места назначения может отличаться в зависимости от среды, в которой выполняется ваше приложение. Если вы сконфигурируете свои физические имена получателей внутри приложения, вы, очевидно, потеряете это преимущество, поскольку их нельзя изменить без перестройки приложения. Если вы настроили их как администрируемый объект с другой стороны, это просто простое изменение в конфигурации сервера приложений, чтобы изменить физическое имя назначения.

Помнить; может иметь смысл настраивать физические имена адресатов. Помимо типа Destination, приложения, связанные с обменом сообщениями, не зависят от его деталей. Назначение обмена сообщениями не имеет функционального контракта, и ни одно из его свойств (физическое назначение, постоянство и т. Д.) Не имеет значения для кода, который вы пишете. Фактический контракт находится внутри самих сообщений (заголовки и тело). С другой стороны, таблица базы данных является примером чего-то, что само по себе представляет контракт и тесно связано с вашим кодом. В большинстве случаев переименование таблицы базы данных влияет на ваш код, поэтому создание чего-то подобного настраиваемому обычно не имеет никакой дополнительной ценности по сравнению с назначением для обмена сообщениями.

Недавно я обнаружил, что мое понимание этого не вся правда. Спецификация (из «4.4.4 Создание объектов назначения», как вставлено в некоторых параграфах выше) уже дает подсказку: «Большинство клиентов будут использовать назначения, которые являются объектами, управляемыми JMS, которые они искали через JNDI. Это самый переносимый подход ». По сути, это говорит нам о том, что другой подход (динамический подход, в котором мы работаем с местом назначения как String) является «наименее переносимым» способом. Для меня это никогда не было ясным, поскольку каждый поставщик должен реализовывать оба метода, однако «переносимый» должен рассматриваться в более широком контексте.

При настройке адресатов в качестве строки, Spring по умолчанию преобразует их в описания JMS при каждом создании нового сеанса JMS. При использовании DefaultMessageListenerContainer для потребления сообщений каждое обрабатываемое сообщение происходит в транзакции, и по умолчанию сеанс и потребитель JMS не объединяются в пул, поэтому они воссоздаются для каждой операции приема. Это приводит к преобразованию String в JMS-адресат каждый раз, когда контейнер проверяет наличие новых сообщений и / или получает новое сообщение. В игру вступает «непереносимый» аспект, поскольку он также означает, что детали и стоимость этого преобразования полностью зависят от драйвера / реализации вашей MOM. В нашем случае это произошло с Oracle AQ в качестве поставщика MOM. Каждый раз, когда происходит преобразование назначения, драйвер выполняет определенный запрос:

1
2
3
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:

1
2
3
4
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.

Btw; это также верно для MessageConsumers / Producers. Оба они связаны с сеансом, поэтому, если сеанс является легким для открытия, то эти объекты также будут.

Однако существует третье решение; пользовательский DestinationResolver. DestinationResolver — это абстракция, которая заботится о переходе от строки к месту назначения. Значение по умолчанию ( DynamicDestinationResolver ) использует createConsumer (javax.jms.Destination) в сеансе JMS для преобразования, но оно, однако, не кэширует результирующее назначение. Однако, если ваши Назначения настроены на сервере приложений в качестве ресурсов, вы можете (помимо использования поддержки JNDI Spring и непосредственного внедрения Назначения) также использовать JndiDestinationResolver . Этот распознаватель будет обрабатывать предоставленную строку как местоположение JNDI (вместо физического имени назначения) и выполнит поиск для вас. По умолчанию он кэширует результирующее назначение, избегая любых последующих поисков JNDI. Теперь можно также настроить JndiDestinationResolver в качестве декоратора кэширования для DynamicDestinationResolver. Если вы установите для отката значение true, он сначала попытается использовать строку в качестве местоположения для поиска из JNDI, а если это не удастся, он передаст нашу строку в DynamicDestinationResolver, используя API-интерфейс JMS для преобразования нашей строки в пункт назначения. Полученный в результате пункт назначения в обоих случаях кэшируется, и поэтому следующий запрос для того же пункта назначения будет обслуживаться из кэша. С этим решателем есть решение из коробки без необходимости писать код:

1
2
3
4
5
6
7
8
9
<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>

JndiDestinationResolver является потокобезопасным благодаря внутреннему использованию ConcurrentHasmap для хранения привязок. Назначение JMS само по себе является потокобезопасным в соответствии со спецификацией JMS 1.1 (2.8 Многопоточность) и может безопасно кэшироваться:

jmsObjects

Это снова хороший пример того, как простые вещи могут иногда иметь важное влияние. На этот раз решение было простым благодаря Spring. Однако было бы лучше сделать поведение кэширования по умолчанию, так как это отделило бы его от причуд любого конкретного провайдера при поиске места назначения. Причина, по которой это не значение по умолчанию, вероятно, заключается в том, что DefaultMessageListenerContainer поддерживает изменение места назначения на лету (например, с использованием JMX):

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