Spring предусматривает интеграционную структуру JMS, которая упрощает использование API-интерфейса JMS точно так же, как средства интеграции Spring для API-интерфейса JDBC.

JMS можно условно разделить на две области функциональности, а именно производство и потребление сообщений. Класс JmsTemplate используется для производства сообщений и синхронного приема сообщений. Для асинхронного приема, похожего на стиль бинов, управляемых сообщениями Java EE, Spring предусматривает ряд контейнеров со слушателями сообщений, которые можно использовать для создания POJO, управляемых сообщениями (Message-Driven POJO/MDP). Spring также предусматривает декларативный способ создания слушателей сообщений.

Пакет org.springframework.jms.core содержит основную функциональность для использования JMS. Он содержит классы JMS-шаблонов, которые упрощают использование JMS, обрабатывая создание и освобождение ресурсов, подобно тому, как это делает JdbcTemplate для JDBC. Принцип проектирования, общий для шаблонных классов Spring, заключается в предоставлении вспомогательных методов для выполнения общих операций и, в случае более сложного использования, делегировании сути задачи обработки реализованным пользователем интерфейсам обратного вызова. JMS-шаблон соблюдает ту же схему. Классы предусматривают различные вспомогательные методы для отправки сообщений, синхронного потребления сообщений и открытия сессии JMS и производителя сообщений пользователю.

Пакет org.springframework.jms.support содержит функциональность преобразования JMSException. Функция преобразования превращает иерархию проверяемых исключений JMSException в зеркальную иерархию непроверяемых исключений. Если существуют какие-либо специфичные для провайдера подклассы проверяемого исключения javax.jms.JMSException, это исключение оборачивается в непроверяемое UncategorizedJmsException.

Пакет org.springframework.jms.support.converter содержит абстракцию MessageConverter для преобразования между объектами Java и сообщениями JMS.

Пакет org.springframework.jms.support.destination содержит различные стратегии для управления адресами назначения JMS, такие как передача локатора служб для адресов назначения, хранящихся в JNDI.

Пакет org.springframework.jms.annotation содержит необходимую инфраструктуру для поддержки конечных точек слушателей, управляемых аннотациями, с помощью @JmsListener.

Пакет org.springframework.jms.config содержит реализацию парсера для пространства имен jms, а также средства поддержки java-конфигурации для конфигурирования контейнеров слушателей и создания конечных точек слушателей.

Наконец, пакет org.springframework.jms.connection содержит реализацию ConnectionFactory, подходящую для использования в автономных приложениях. Он также содержит реализацию PlatformTransactionManager от Spring для JMS (с хитрым названием JmsTransactionManager). Это позволяет с легкостью интегрировать JMS как транзакционный ресурс в механизмы управления транзакциями Spring.

Начиная с версии Spring Framework 5, пакет для JMS из Spring полностью поддерживает JMS 2.0 и требует наличия API-интерфейса JMS 2.0 во время выполнения. Рекомендуется использовать поставщика, совместимого с JMS 2.0.

Если в системе используется более старая версия брокера сообщений, можете попробовать перейти на совместимый с JMS 2.0 драйвер для существующего поколения брокеров. Кроме того, также можно попробовать запустить драйвер на базе JMS 1.1, просто поместив jar-файл API-интерфейса JMS 2.0 в classpath, но используя только совместимый с JMS 1.1 API в драйвере. Средства поддержки JMS в Spring по умолчанию соблюдают соглашения JMS 1.1, поэтому при соответствующей конфигурации они будут поддерживать такой сценарий. Однако, учитывайте это только для переходных сценариев.

Использование JmsTemplate

Класс JmsTemplate является центральным классом в основном пакете JMS. Он упрощает использование JMS, поскольку обрабатывает создание и освобождение ресурсов при отправке или синхронном получении сообщений.

Коду, использующему JmsTemplate, необходимо лишь реализовать интерфейсы обратного вызова, который передает им четко определенный высокоуровневый контракт. Интерфейс обратного вызова MessageCreator создает сообщение, когда ему передается Session, указанная вызывающим кодом в JmsTemplate. В случае более комплексного использования API-интерфейса JMS SessionCallback передает JMS-сессию, а ProducerCallback открывает пару Session и MessageProducer.

API-интерфейс JMS предусматривает два типа методов отправки, один из которых принимает режим доставки, приоритет и время жизни в качестве параметров качества обслуживания (Quality of Service/QOS), а другой не принимает никаких QOS-параметров и использует значения по умолчанию. Поскольку JmsTemplate содержит множество методов отправки, установка QOS-параметров была открыта как свойства бинов, чтобы избежать дублирования относительно количества методов отправки. Аналогично, значение времени ожидания для синхронных вызовов приема устанавливается с помощью свойства setReceiveTimeout.

Некоторые JMS-поставщики позволяют устанавливать значения QOS по умолчанию административно через конфигурацию ConnectionFactory. Это приводит к тому, что вызов метода send экземпляра MessageProducer(send(Destination destination, Message message)) использует значения QOS по умолчанию, отличные от тех, что указаны в JMS-спецификации. Чтобы обеспечить последовательное управление значениями QOS, шаблону JmsTemplate нужно специально разрешить использовать свои собственные значения QOS путем установки булева свойства isExplicitQosEnabled в true.

Для удобства JmsTemplate также открывает базовую операцию запрос-ответ, которая позволяет отправить сообщение и ожидать ответа во временной очереди, создаваемой в рамках операции.

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

Начиная со Spring Framework 4.1, JmsMessagingTemplate надстраивается поверх JmsTemplate и обеспечивает интеграцию с абстракцией обмена сообщениями – то есть org.springframework.messaging.Message. Это позволяет создавать сообщение для отправки общим образом.

Соединения

JmsTemplate требуется ссылка на ConnectionFactory. Фабрика ConnectionFactory является частью JMS-спецификации и служит точкой входа для работы с JMS. Она используется клиентским приложением в качестве фабрики для создания соединений с JMS-поставщиком и инкапсулирует различные параметры конфигурации, многие из которых зависят от производителя, например, параметры SSL-конфигурации.

При использовании JMS внутри EJB производитель предусматривает реализации JMS-интерфейсов, чтобы их можно было задействовать в управлении декларативными транзакциями и объединять соединения и сессии в пул. Чтобы использовать эту реализацию, контейнерам Java EE обычно требуется, чтобы фабрику JMS-соединений была объявлена в качестве resource-ref в дескрипторах развертывания EJB или сервлета. Чтобы обеспечить использование этих функций с JmsTemplate внутри EJB, клиентское приложение должно обязательно ссылаться на управляемую реализацию ConnectionFactory.

Кэширование ресурсов обмена сообщениями

Стандартный API предполагает создание множества промежуточных объектов. Чтобы отправить сообщение, в API происходит следующая цепочка действий:

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

Между ConnectionFactory и операцией Send создаются и уничтожаются три промежуточных объекта. Чтобы оптимизировать использование ресурсов и повысить производительность, Spring предусматривает две реализации ConnectionFactory.

Использование SingleConnectionFactory

Spring предусматривает реализацию интерфейса ConnectionFactory, SingleConnectionFactory, которая возвращает одно и то же Connection при всех вызовах createConnection() и игнорирует вызовы close(). Это полезно для тестирования и автономных окружений, чтобы одно и то же соединение можно было использовать для нескольких вызовов JmsTemplate, которые могут охватывать любое количество транзакций. SingleConnectionFactory принимает ссылку на стандартную ConnectionFactory, которая обычно берется из JNDI.

Использование CachingConnectionFactory

CachingConnectionFactory расширяет функциональность SingleConnectionFactory и добавляет кэширование экземпляров Session, MessageProducer и MessageConsumer. Начальный размер кэша установлен на 1. Можно использовать свойство sessionCacheSize для увеличения количества кэшируемых сессий. Обратите внимание, что количество реальных кэшированных сессий превышает это число, поскольку сессии кэшируются на основе их режима подтверждения, поэтому может доходить до четырех кэшированных экземпляров сессий (по одному для каждого режима подтверждения), когда свойство sessionCacheSize установлено на 1. Экземпляры MessageProducer и MessageConsumer кэшируются в рамках их собственной сессии, а также учитывают уникальные свойства производителей и потребителей при кэшировании. Генераторы сообщений кэшируются на основе их назначения. MessageConsumers кэшируются на основе ключа, состоящего из адреса назначения, селектора, флага доставки noLocal и имени долговременной подписки (если создаются долговременные потребители).

MessageProducers и MessageConsumers для временных очередей и тем (TemporaryQueue/TemporaryTopic) никогда не кэшируются. К сожалению, WebLogic для JMS иногда может реализовать интерфейсы временной очереди/темы для своей обычной реализации адресов назначения, ошибочно указав, что ни один из его адресов назначения нельзя кэшировать. Используйте другой пул/кэш соединений для WebLogic или настройте CachingConnectionFactory для целей, ориентированных на использование WebLogic.

Управление адресами назначения

Адреса назначения, как экземпляры ConnectionFactory, являются управляемыми JMS-объектами, которые можно хранить и извлекать в JNDI. При конфигурировании контекста приложения Spring можно использовать класс-фабрику JndiObjectFactoryBean из JNDI или <jee:jndi-lookup>, чтобы осуществить внедрение зависимостей для ссылок вашего объекта на адреса назначения JMS. Однако данная стратегия зачастую оказывается чересчур громоздкой, если в приложении имеется большое количество адресов назначения или если имеются расширенные функции управления адресами назначения, уникальные для JMS-поставщика. Примеры такого расширенного управления адресами назначения включают создание динамических адресов назначения или поддержку иерархического пространства имен адресов назначения. JmsTemplate делегирует разрешение имени адреса назначения объекту адреса назначения JMS, который реализует интерфейс DestinationResolver. DynamicDestinationResolver – это реализация по умолчанию, используемая JmsTemplate и позволяющая разрешать динамические адреса назначения. Также предусмотрен JndiDestinationResolver для работы в качестве локатора служб для адресов назначения, содержащихся в JNDI, и по желанию его можно откатить к логике работы, содержащейся в DynamicDestinationResolver.

Довольно часто адреса назначения, используемые в JMS-приложении, становятся известны только во время выполнения программы и, следовательно, их нельзя создать административно при развертывании приложения. Чаще всего это происходит потому, что между взаимодействующими компонентами системы существует общая прикладная логика, которая создает адреса назначения во время выполнения программы в соответствии с известным соглашением об именовании. Несмотря на то, что создание динамических адресов назначения не является частью спецификации JMS, большинство производителей предусмотрели такую возможность. Динамические адреса назначения создаются с именем, определяемым пользователем, что отличает их от временных адресов назначения, и зачастую они не регистрируются в JNDI. API-интерфейс, используемый для создания динамических адресов назначения, у разных производителей отличается, поскольку свойства, связанные с адресом назначения, зависят от производителя. Однако, простой вариант реализации, который иногда используется производителями, заключается в том, чтобы игнорировать предупреждения в спецификации JMS и использовать метод TopicSession createTopic(String topicName) или метод QueueSession createQueue(String queueName), чтобы создать новый адрес назначения со свойствами адреса назначения по умолчанию. В зависимости от реализации производителя, DynamicDestinationResolver может также создавать физический адрес назначения вместо того, чтобы разрешать лишь один.

Булево свойство pubSubDomain используется для конфигурирования JmsTemplate, если известно, какой домен JMS используется. По умолчанию значение этого свойства установлено в false, что указывает на использование домена "точка-точка", Queues. Это свойство (используемое JmsTemplate) определяет логику работы разрешения динамических адресов назначения через реализацию интерфейса DestinationResolver.

Также можно сконфигурировать JmsTemplate с адресом назначения по умолчанию при помощи свойства defaultDestination. Адрес назначения по умолчанию используется в операциях отправки и получения, которые не ссылаются на конкретный адрес назначения.

Контейнеры слушателей сообщений

Одно из самых распространенных применений JMS-сообщений в контексте EJB – это управление бинами, управляемыми сообщениями (MDB). Spring предлагает решение для создания управляемых сообщениями POJO (MDP) таким образом, чтобы не привязывать пользователя к контейнеру EJB. (См. "Асинхронный прием: управляемые сообщениями POJO", где подробного описаны средства поддержки MDP в Spring). Начиная с версии Spring Framework 4.1, методы конечных точек можно пометить аннотацией @JmsListener – более подробную информацию см. в разделе "Конечные точки слушателей, управляемые аннотациями".

Контейнер слушателя сообщений используется для получения сообщений из очереди сообщений JMS и управления MessageListener, который внедрен в него. Контейнер слушателя отвечает за все потоки приема и отправки сообщений в слушатель для обработки. Контейнер слушателя сообщений является посредником между MDP и поставщиком обмена сообщениями и отвечает за регистрацию для получения сообщений, участие в транзакциях, получение и освобождение ресурсов, преобразование исключений и так далее. Это позволяет написать (возможно, комплексную) бизнес-логику, связанную с получением сообщения (и, возможно, ответом на него), и делегировать фреймворку стереотипную сквозную функциональность кодовой инфраструктуры JMS.

Существует два стандартных контейнера слушателей сообщений JMS, поставляемых вместе с Spring, каждый из которых имеет свой специализированный набор функций.

Использование SimpleMessageListenerContainer

Этот контейнер слушателя сообщений является более простым из двух стандартных вариантов. Он создает фиксированное количество JMS-сессий и потребителей при запуске, регистрирует слушателя с помощью стандартного метода JMS MessageConsumer.setMessageListener() и оставляет на усмотрение JMS-поставщика выполнение обратных вызовов слушателя. Этот вариант не допускает динамической адаптации к требованиям среды выполнения или участия в управляемых извне транзакциях. С точки зрения совместимости, он крайне близок по духу к автономной спецификации JMS, но в целом не совместим с ограничениями JMS в Java EE.

Хотя SimpleMessageListenerContainer не допускает участия в управляемых извне транзакциях, он поддерживает собственные транзакции JMS. Чтобы активировать эту функцию, можно переключить флаг sessionTransacted на true или установить атрибут acknowledge в значение transacted в пространстве имен XML. Исключения, генерируемые слушателем, затем приводят к откату, при этом сообщение доставляется повторно. Кроме того, обратите внимание на возможность использования режима CLIENT_ACKNOWLEDGE, который также обеспечивает повторную доставку в случае генерации исключения, но не использует транзакционные экземпляры Session и, следовательно, не включает в протокол транзакции другие операции Session (такие как отправка сообщений-ответов).
Режим AUTO_ACKNOWLEDGE по умолчанию не обеспечивает надлежащих гарантий надежности. Сообщения могут быть утрачены при неудачном выполнении слушателя (поскольку поставщик автоматически подтверждает каждое без исключений сообщение после вызова слушателя, которые должны быть переданы поставщику) или когда контейнер слушателя выключается (вы можете сконфигурировать это, установив флаг acceptMessagesWhileStopping). Обязательно используйте транзакционные сессии в случае, если необходимо обеспечить надежность (например, для надежной обработки очередей и долговечных подписок на темы).

Использование DefaultMessageListenerContainer

Этот контейнер слушателя сообщений используется в большинстве случаев. В отличие от SimpleMessageListenerContainer, данный вариант контейнера допускает динамическую адаптацию к требованиям среды выполнения и участие в управляемых извне транзакциях. Каждое полученное сообщение регистрируется в XA-транзакции, если конфигурация включает JtaTransactionManager. В результате при обработке может использоваться семантика XA-транзакций. Этот контейнер слушателя обеспечивает отличный баланс между низкими требованиями к JMS-поставщику, расширенной функциональностью (например, участие в управляемых извне транзакциях) и совместимостью со средами Java EE.

Можно настраивать уровень кэша контейнера. Обратите внимание, что если кэширование не активировано, то для каждого приема сообщения создается новое соединение и новая сессия. Данный факт в сочетании с недолговременной подпиской при высокой нагрузке может привести к утрате сообщений. В этом случае обязательно используйте соответствующий уровень кэша.

Этот контейнер также способен выполнять восстановление в случае выхода из строя брокера. По умолчанию простая реализация BackOff повторяет попытку каждые пять секунд. Можно задать кастомную реализацию BackOff для указания более точных параметров восстановления. В качестве примера см. ExponentialBackOff.

Как и его собрат (SimpleMessageListenerContainer), DefaultMessageListenerContainer поддерживает собственные JMS-транзакции и позволяет настраивать режим подтверждения. Если это возможно в вашем случае, настоятельно рекомендуется использовать транзакции с внешним управлением – то есть, если у вас получится мириться с периодическими дублирующими сообщениями в случае отключения JVM. Кастомные фазы обнаружения дублирующих сообщений в бизнес-логике могут охватывать такие ситуации – например, в форме проверки существования бизнес-сущности или проверки таблицы протокола. Любые такие меры значительно эффективнее альтернативы: обернуть всю процедуру обработки XA-транзакцией (через конфигурирование DefaultMessageListenerContainer с JtaTransactionManager), чтобы охватить прием JMS-сообщения, а также выполнение бизнес-логики в слушателе сообщений (включая операции с базой данных и т.д.).
Режим AUTO_ACKNOWLEDGE по умолчанию не обеспечивает надлежащих гарантий надежности. Сообщения могут быть утрачены при неудачном выполнении слушателя (поскольку поставщик автоматически подтверждает каждое без исключений сообщение после вызова слушателя, которые должны быть переданы поставщику) или когда контейнер слушателя выключается (вы можете сконфигурировать это, установив флаг acceptMessagesWhileStopping). Обязательно используйте транзакционные сессии в случае, если необходимо обеспечить надежность (например, для надежной обработки очередей и долговечных подписок на темы).

Управление транзакциями

Spring предусматривает JmsTransactionManager, который управляет транзакциями для одной ConnectionFactory из JMS. Это позволяет JMS-приложениям использовать возможности управляемых транзакций Spring, как описано в разделе "Управление транзакциями" главы "Доступ к данным". JmsTransactionManager выполняет локальные транзакции ресурсов, привязывая пару "соединение-сессия" JMS из указанной ConnectionFactory к потоку. JmsTemplate автоматически определяет такие транзакционные ресурсы и работает с ними соответствующим образом.

В окружении Java EE фабрика ConnectionFactory объединяет экземпляры соединения и сессии в пул, что позволяет эффективно использовать эти ресурсы в различных транзакциях. В автономном окружении использование SingleConnectionFactory из Spring приводит к общему Connection по стандарту JMS, при этом каждая транзакция имеет свою собственную независимую Session. Кроме того, можно использовать адаптер пулинга, специфичный для поставщика, например, класс PooledConnectionFactory из ActiveMQ.

Также можно использовать JmsTemplate с JtaTransactionManager и ConnectionFactory из JMS с поддержкой XA для выполнения распределенных транзакций. Обратите внимание, что это требует использования менеджера транзакций JTA, а также надлежащим образом сконфигурированной XA-фабрики ConnectionFactory. (Обратитесь к документации вашего сервера Java EE или поставщика JMS).

Повторное использование кода в управляемом и неуправляемом транзакционном окружении может вносить неясность при использовании API-интерфейса JMS для создания Session из Connection. Это связано с тем, что JMS API имеет лишь один метод-фабрику для создания Session, и ему требуются значения для режимов транзакции и подтверждения. В управляемом окружении установка этих значений лежит на транзакционной инфраструктуры окружения, поэтому данные значения игнорируются функцией-обёрткой поставщика для JMS-подключения. При использовании JmsTemplate в неуправляемом окружении можно задать эти значения с помощью свойств sessionTransacted и sessionAcknowledgeMode. Если используется PlatformTransactionManager с JmsTemplate, шаблону всегда предоставляется транзакционная Session по стандарту JMS.