版本:5.3.2
官方文檔地址:http://activemq.apache.org/components/classic/documentation
模式:主題模式(發佈訂閱)
官方給出的topic模式的訂閱者有兩種方式:
1. 註解方式:
指定destination 以及ListenerContainerFactory,示例如下:
@Service
public class TopicConsumer {
@JmsListener(destination = "demo.topic", containerFactory = "jmsListenerContainerFactory")
public void receiver1(User text) {
System.out.println("TopicConsumer : receiver1 : " + text);
}
2. 實現JmsListenerConfigurer
public class ActiveConfig implements JmsListenerConfigurer {
@Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
endpoint.setDestination("demo.topic");
endpoint.setMessageListener(new TopicListener());
endpoint.setId("myJmsEndpoint");
registrar.setContainerFactory(jmsListenerContainerTopic);
registrar.registerEndpoint(endpoint);
}
}
同樣需要設置監聽容器和監聽者,請注意,發佈訂閱模式需要自己初始化jmsListenerContainerFactory
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerFactory(ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setPubSubDomain(true);
bean.setConnectionFactory(connectionFactory);
bean.setMessageConverter(jacksonJmsMessageConverter());
return bean;
}
問題:以上兩種方案的消費者都是需要事先約定好監聽的destination ,那可不可以在程序啓動後根據條件動態加載消費者呢?
原理分析:
我們先來看下第二種實現JmsListenerConfigurer接口是怎麼加載消費者的。
@FunctionalInterface
public interface JmsListenerConfigurer {
void configureJmsListeners(JmsListenerEndpointRegistrar registrar);
}
傳給我們一個JmsListenerEndpointRegistrar 對象,而它的作用就是一個幫助類,用於JmsListenerEndpointRegistry 註冊 JmsListenerEndpoint,JmsListenerEndpoint就是消費者。
而通過斷點我們可以知道configureJmsListeners這個方法是由JmsListenerAnnotationBeanPostProcessor 的afterSingletonsInstantiated 方法觸發的。
看下這個方法主要做了什麼:已經刪除相關性不大的代碼
public void afterSingletonsInstantiated() {
// 1.找到所有JmsListenerConfigurer實現類,調用configureJmsListeners方法
Map<String, JmsListenerConfigurer> beans =
((ListableBeanFactory) this.beanFactory).getBeansOfType(JmsListenerConfigurer.class);
List<JmsListenerConfigurer> configurers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(configurers);
for (JmsListenerConfigurer configurer : configurers) {
// 2. 現在registrar 裏有所有的消費者了。
configurer.configureJmsListeners(this.registrar);
}
if (this.registrar.getEndpointRegistry() == null) {
// 3. 從ioc容器中找到 JmsListenerEndpointRegistry bean ,並且設置給幫助類:registrar
if (this.endpointRegistry == null) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to find endpoint registry by bean name");
this.endpointRegistry = this.beanFactory.getBean(
JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME, JmsListenerEndpointRegistry.class);
}
this.registrar.setEndpointRegistry(this.endpointRegistry);
}
// 4. 註冊所有的監聽者
this.registrar.afterPropertiesSet();
上面的第4步尤其關鍵:註冊所有的監聽者,它最後會調用 JmsListenerEndpointRegistry 的registerListenerContainer 方法。
public void registerListenerContainer(JmsListenerEndpoint endpoint, JmsListenerContainerFactory<?> factory,
boolean startImmediately) {
synchronized (this.listenerContainers) {
if (this.listenerContainers.containsKey(id)) {
throw new IllegalStateException("Another endpoint is already registered with id '" + id + "'");
}
// 根據之前設置的監聽工廠和消費之,創建一個消息容器
MessageListenerContainer container = createListenerContainer(endpoint, factory);
this.listenerContainers.put(id, container);
// 啓動容器
if (startImmediately) {
startIfNecessary(container);
}
}
}
也就是上面的這個方法是最爲核心的,即 創建容器,啓動容器。
如果需要動態的添加消費者,那麼就可以調用這個方法便能夠達到目的了。
實現
核心原理在於:通過拿到 JmsListenerEndpointRegistry 對象,調用registerListenerContainer方法。
點進這個註解 @EnableJms
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({JmsBootstrapConfiguration.class})
public @interface EnableJms {
}
發現這個註解會加載JmsBootstrapConfiguration 配置類
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class JmsBootstrapConfiguration {
@Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public JmsListenerAnnotationBeanPostProcessor jmsListenerAnnotationProcessor() {
return new JmsListenerAnnotationBeanPostProcessor();
}
@Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
public JmsListenerEndpointRegistry defaultJmsListenerEndpointRegistry() {
return new JmsListenerEndpointRegistry();
}
結果發現,JmsListenerEndpointRegistry 被ioc 管理起來了,也就是說,可以自動裝配了。
因此,下面的代碼示例展示如何動態添加消費者的能力。
發起請求 http://ip:port/regist 請求就可以動態初始化消費者了
@RestController
public class Controller {
@Autowired
private JmsListenerEndpointRegistry endpointRegistry;
@Autowired
private JmsListenerContainerFactory containerFactory;
@GetMapping("/regist")
public void run() throws NoSuchFieldException, IllegalAccessException {
SimpleJmsListenerEndpoint endpoint2 = new SimpleJmsListenerEndpoint();
endpoint2.setId("myJmsEndpoint3");
endpoint2.setDestination("demo.topic");
endpoint2.setMessageListener(new TopicListener());
// 3. 啓動容器
registry.registerListenerContainer(endpoint2, containerFactory, true);
registry.registerListenerContainer 這個方法可以多次調用,它底層是個list 把所有的消費者存起來的。
再貼下消息消費者代碼:
public class TopicListener implements MessageListener {
@Override
public void onMessage(Message message) {
if (message instanceof ActiveMQTextMessage) {
try {
ActiveMQTextMessage txtMsg = (ActiveMQTextMessage) message;
String messageStr = txtMsg.getText();
System.out.println("主題監聽器 接收到文本消息:" + messageStr);
} catch (JMSException | IOException | ClassNotFoundException e) {
e.printStackTrace();
}
} else {
throw new IllegalArgumentException("只支持 ActiveMQTextMessage 類型消息!");
}
}
}