JMS - ActiveMQ 实战
JMS(JAVA Message Service,java 消息服务)API 是一个消息服务的标准或者说是规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。
基本概念
JMS 是 java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。
消息模型分为:
点对点:Point-to-Point(P2P)
发布订阅:Publish/Subscribe(Pub/Sub)
JMS 编程模型
1.ConnectionFactory
针对两种不同的jms消息模型,分别有 QueueConnectionFactory 和 TopicConnectionFactory 两种。
2.Destination
Destination 是消息生产者的消息发送目标或者说消息消费者的消息来源。它是某个队列(Queue)或某个主题(Topic)。所以,Destination 就是两种类型的对象:Queue、Topic。
3.Connection
Connection 表示在客户端和 JMS 系统之间建立的链接(对TCP/IP socket的包装)。Connection 可以产生一个或多个 Session。跟 ConnectionFactory 一样,Connection也有两种类型:QueueConnection 和 TopicConnection。
4.Session
Session 是我们操作消息的接口。可以通过session创建生产者、消费者、消息等。Session提供了事务的功能。当我们需要使用 session 发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分为 QueueSession 和 TopicSession。
5.消息生产者
消息生产者由Session创建,并用于将消息发送到Destination。同样,消息生产者分两种类型:QueueSender 和 TopicPublisher。可以调用消息生产者的方法(send 或 publish 方法)发送消息。
6.消息消费者
消息消费者由 Session 创建,用于接收被发送到Destination的消息。两种类型:QueueReceiver 和 TopicSubscriber。可分别通过 session 的 createReceiver(Queue) 或 createSubscriber(Topic) 来创建。当然,也可以 session 的 creatDurableSubscriber 方法来创建持久化的订阅者。
7.MessageListener
消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的onMessage方法。
ActiveMQ
Apache ActiveMQ 是一款最流行的,能力强劲的开源消息中间件。ActiveMQ是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider 实现。ActiveMQ 默认使用的 TCP 连接端口是61616。提供一个用于监控ActiveMQ 的应用,默认地址:http://127.0.0.1:8161/admin/。
点对点
每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。如果希望发送的每个消息都应该被成功处理的话,那么需要 P2P 模式。特点如下:
1.每个消息只有一个消费者,一旦被消费,消息就不再在消息队列中。
2.发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列。
3.接收者在成功接收消息之后需向队列应答成功。
生产者
package com.mq.p2p;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.text.MessageFormat;
/**
* 类 名:JMSProducer
* 类描述:点对点 - 生产者
*/
public class JMSProducer {
private static final String USERNAME = "hq";
private static final String PASSWORD = "123";
private static final String BROKEURL = "tcp://localhost:61616";
private static final int SENDNUM = 5; //发送消息的次数
public static void createMQ() {
ConnectionFactory connectionFactory; //连接工厂
Connection connection = null; //连接
Session session; //会话,接收或发送消息的线程
Destination destination; //消息目的地
MessageProducer messageProducer; //消息生产者
//实例化连接工厂
connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKEURL);
try {
connection = connectionFactory.createConnection(); //通过连接工厂获取连接
connection.start(); //启动连接
session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); //创建session 参数:是否增加事物,消息确定方式
destination = session.createQueue("HQ"); //创建消息队列
messageProducer = session.createProducer(destination); //创建消息生产者
for (int i = 1; i <= SENDNUM; i++) {
String msg = MessageFormat.format("MQ消息:{0}", i);
System.out.println("发送消息:" + msg);
TextMessage message = session.createTextMessage(msg);
messageProducer.send(message);
}
session.commit();
} catch (JMSException e) {
e.printStackTrace();
}finally {
//释放资源
if(connection != null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
消费者
package com.mq.p2p;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 类 名:JMSConsumer
* 类描述:点对点 - 消费者
*/
public class JMSConsumer {
private static final String USERNAME = "hq";
private static final String PASSWORD = "123";
private static final String BROKEURL = "tcp://localhost:61616";
public static void consumeMQ(){
ConnectionFactory connectionFactory; //连接工厂
Connection connection = null; //连接
Session session; //会话,接收或发送消息的线程
Destination destination; //消息目的地
MessageConsumer messageConsumer; //消息消费者
//实例化连接工厂
connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKEURL);
try {
connection=connectionFactory.createConnection(); //通过连接工厂获取连接
connection.start(); //启动连接
session=connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE); //创建Session
destination=session.createQueue("HQ"); //创建连接的消息队列
messageConsumer=session.createConsumer(destination); //创建消息消费者
messageConsumer.setMessageListener(new JMSListener()); // 注册消息监听
} catch (JMSException e) {
e.printStackTrace();
}
}
}
消息监听
package com.mq.p2p;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
/**
* 类 名:JMSListener
* 类描述:消息监听
*/
public class JMSListener implements MessageListener {
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println("接收到MQ消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
发布订阅(Pub/Sub)
客户端将消息发送到主题。多个发布者将消息发送到 Topic,系统将这些消息传递给多个订阅者。如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。特点如下:
1.每个消息可以有多个消费者。
2.发布者和订阅者之间有时间上的依赖性。必须先订阅,才能消费,而且为了消费消息,订阅者必须保持运行的状态。
3.为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有运行,它也能接收到发布者的消息。
发布者
package com.mq.topic;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.text.MessageFormat;
/**
* 发布订阅 - 发布者
*/
public class JMSProducer {
private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;
private static final int SENDNUM = 5; //发送消息的次数
public static void createMQ() {
ConnectionFactory connectionFactory; //连接工厂
Connection connection = null; //连接
Session session; //会话,接收或发送消息的线程
Destination destination; //消息目的地
MessageProducer messageProducer; //消息生产者
//实例化连接工厂
connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKEURL);
try {
connection = connectionFactory.createConnection(); //通过连接工厂获取连接
connection.start(); //启动连接
session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); //创建session 参数:是否增加事物,消息确定方式
destination = session.createTopic("HQ_topic"); //创建消息队列
messageProducer = session.createProducer(destination); //创建消息生产者
for (int i = 1; i <= SENDNUM; i++) {
String msg = MessageFormat.format("MQ消息:{0}", i);
System.out.println("Topic发送消息:" + msg);
TextMessage message = session.createTextMessage(msg);
messageProducer.send(message);
}
session.commit();
} catch (JMSException e) {
e.printStackTrace();
}finally {
//释放资源
if(connection != null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
订阅者
package com.mq.topic;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 发布订阅 - 订阅者
*/
public class JMSConsumer {
private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;
public static void consumeMQ(){
ConnectionFactory connectionFactory; //连接工厂
Connection connection = null; //连接
Session session; //会话,接收或发送消息的线程
Destination destination; //消息目的地
MessageConsumer messageConsumer; //消息消费者
//实例化连接工厂
connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKEURL);
try {
connection=connectionFactory.createConnection(); //通过连接工厂获取连接
connection.start(); //启动连接
session=connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE); //创建Session
destination=session.createTopic("HQ_topic"); //创建连接的消息队列
messageConsumer=session.createConsumer(destination); //创建消息消费者
messageConsumer.setMessageListener(new JMSListener()); // 注册消息监听
} catch (JMSException e) {
e.printStackTrace();
}
}
}
消息监听
package com.mq.topic;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
/**
* 发布订阅 - 消息监听
*/
public class JMSListener implements MessageListener {
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println("订阅者一接收到MQ消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
ActiveMQ 与 Spring4.0 整合
pom.xml 文件配置
<!-- ActiveMQ -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-broker</artifactId>
<version>5.15.2</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>5.15.2</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-spring</artifactId>
<version>5.15.2</version>
</dependency>
applicationContext.xml 文件配置
<!-- connectionFactory 是 Spring 用于创建到 JMS 服务器链接的 -->
<bean id="jmsFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616"/>
<property name="userName" value="hq"/>
<property name="password" value="123"/>
</bean>
<!-- 配置生产者。利用 Spring 提供的 JmsTemplate 类可以配置消息发送 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<!-- value="false":队列模式;value="true":发布订阅模式 -->
<property name="pubSubDomain" value="false" />
<property name="receiveTimeout" value="30000" />
</bean>
<!-- 目的地,即队列名称 -->
<bean id="hqQueue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg index="0" value="hq"/>
</bean>
<!-- 监听器 -->
<bean id="hqListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<property name="delegate" ref="HqReceiver"/>
</bean>
<!-- 消息监听容器。每个消费者对应每个目的地都需要有对应的 MessageListenerContainer -->
<bean id="hqListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="messageListener" ref="hqListener"/>
<property name="destination" ref="hqQueue"/>
<!-- ack 消息确认机制。4:单条消息确认 -->
<property name="sessionAcknowledgeMode" value="4"/>
</bean>
ack 消息确定机制
JMS 规范了四种 ack 消息确认机制。
1. AUTO_ACKNOWLEDGE = 1 自动确认
2. CLIENT_ACKNOWLEDGE = 2 客户端手动确认
3. DUPS_OK_ACKNOWLEDGE = 3 自动批量确认
4. SESSION_TRANSACTED = 0 事务提交并确认
ActiveMQ 补充了一个自定义的 ack 模式:
INDIVIDUAL_ACKNOWLEDGE = 4 单条消息确认
ActiveMQ 与 Spring 整合时,如果想设置 ack 消息确认机制为客户端手动确认,在配置消费者监听器的时候,设置sessionAcknowledgeMode 的值为 2。但遗憾的是这种设置并不起作用,通过一段源码可以发现,当配置2时,会被 Spring 自动确认消费。所以,如果想设置 ack 消息确认机制为客户端手动确认,通常是将 sessionAcknowledgeMode 设置为4。在消费者的 onMessage 中,看可以通过 textMessage.acknowledge() 方法手动确认消息的消费。
//org.springframework.jms.listener.AbstractMessageListenerContainer 的一段源码
protected void commitIfNecessary(Session session, Message message) throws JMSException {
if (session.getTransacted()) {
if (isSessionLocallyTransacted(session)) {
JmsUtils.commitIfNecessary(session);
}
}
else if (message != null && isClientAcknowledge(session)) {
message.acknowledge();
}
}
protected boolean isClientAcknowledge(Session session) throws JMSException {
//Spring 自动确认 CLIENT_ACKNOWLEDGE 的情况
return (session.getAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE);
}
生产者
import javax.annotation.Resource;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Service;
/**
* 生产者
*/
@Service
public class ProducerService {
@Resource(name="jmsTemplate")
private JmsTemplate jmsTemplate;
/**
* 向指定队列发送消息
*/
public void sendMessage(Destination destination, final String msg) {
System.out.println("向队列" + destination.toString() + "发送了消息------------" + msg);
jmsTemplate.send(destination, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(msg);
}
});
}
}
消费者
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.listener.SessionAwareMessageListener;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.TextMessage;
/**
* 消费者
*/
@Component
public class HqReceiver implements SessionAwareMessageListener<TextMessage> {
@Override
public void onMessage(TextMessage textMessage, Session session) throws JMSException {
String text = textMessage.getText();
//todo sth.
//手动消费
textMessage.acknowledge();
}
}
用户名密码
ActiveMQ 中包含两套用户系统,分别是应用服务系统、管理系统。
应用服务系统账户
1. ActiveMQ 默认不设置用户名密码,实例化连接工厂时直接使用系统默认值即可。
2. 设置 ActiveMQ 的访问用户名和密码,可以提高系统的安全性。账号信息在 conf/activemq.xml 文件的 <broker> 节点中添加。如此,实例化连接工厂时如果用户名密码不正确便会报运行时异常。
<!-- 账号 & 密码 -->
<plugins>
<simpleAuthenticationPlugin>
<users>
<authenticationUser username="hq" password="123" groups="users,admins"/>
</users>
</simpleAuthenticationPlugin>
</plugins>
管理系统
管理系统的账号默认是 admin/admin,但可以根据需要重新编辑账户信息。
1. 设置免登陆:ActiveMQ 管理系统是基于 jetty 服务器运行的,将 conf/jetty.xml 文件中的 id="securityConstraint" 的节点的 name="authenticate" 属性的值设置为 false 即可。
2. 设置用户名密码:这是用户名密码管理系统的用户名密码保存在 conf/jetty-realm.properties 文件中。格式:用户名 : 密码,角色名。