一、简单例子
1.去官方网站下载:ActiveMQ
2.运行ActiveMQ:解压缩apache-activemq-5.9.1-bin.zip,然后双击apache-activemq-5.9.1\bin\activemq.bat运行ActiveMQ程序。启动ActiveMQ以后,登陆:http://localhost:8161/admin/,创建一个Queue,命名为FirstQueue。
注:部署需要jdk1.5及以上,编译需要jdk1.5(java5)及以上,Java的环境变量(JAVA_HOME)必须设置,即jdk安装的目录,比如C:\Program Files\jdk1.6
控制台监控用户名和密码设置:conf/jetty.xml <bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint"> 的属性authenticate修改为true,用户名和密码在conf/jetty-realm.properties设置。
Linux和Aix系统下的安装:解压:tar zxvf activemq-x.x.x.tar.gz,进入bin文件夹,运行:./activemq start &,也可以只运行:./activemq console。
3.创建Eclipse项目,所需jar包在apache-activemq-5.9.1\lib
4.新建发送类:
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class Sender {
public static void main(String[] args) {
// ConnectionFactory :连接工厂,JMS 用它创建连接
ConnectionFactory connectionFactory;
// Connection :JMS 客户端到JMS Provider 的连接
Connection connection = null;
// Session: 一个发送或接收消息的线程
Session session;
// Destination :消息的目的地;消息发送给谁.
Destination destination;
// MessageProducer:消息发送者
MessageProducer producer;
// 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar
connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
try {
// 构造从工厂得到连接对象
connection = connectionFactory.createConnection();
// 启动
connection.start();
// 第一个参数是否使用事务:当消息发送者向消息提供者(即消息代理)发送消息时,消息发送者等待消息代理的确认,没有回应则抛出异常,消息发送程序负责处理这个错误。
// 第二个参数消息的确认模式:AUTO_ACKNOWLEDGE :指定消息提供者在每次收到消息时自动发送确认。消息只向目标发送一次,但传输过程中可能因为错误而丢失消息。
// CLIENT_ACKNOWLEDGE :由消息接收者确认收到消息,通过调用消息的acknowledge()方法(会通知消息提供者收到了消息)
// DUPS_OK_ACKNOWLEDGE :指定消息提供者在消息接收者没有确认发送时重新发送消息(这种确认模式不在乎接收者收到重复的消息)。
session = connection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
// 获取session注意参数值FirstQueue是一个服务器的queue,须在在ActiveMq的console配置
destination = session.createQueue("FirstQueue");
// 得到消息生成者【发送者】
producer = session.createProducer(destination);
// 设置不持久化,即MQ服务器重启消息就没了,实际根据项目决定(默认为DeliveryMode.PERSISTENT)
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
// 构造消息内容
TextMessage message = session.createTextMessage("ActiveMq 发送的消息");
// 发送消息
producer.send(message);
session.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != connection)
connection.close();
} catch (Throwable ignore) {
}
}
}
}
注:运行后MQ控制台查看FirstQueue是否接收到了一条消息
5.新建接收类(主要代码,其它与发送端相同):
// ConnectionFactory :连接工厂,JMS 用它创建连接
ConnectionFactory connectionFactory=new ActiveMQConnectionFactory("tcp://localhost:61616");
// 构造从工厂得到连接对象
connection = connectionFactory.createConnection();
// 启动
connection.start();
// 获取操作连接
Session session = connection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
// 获取session注意参数值FirstQueue是一个服务器的queue,须在在ActiveMq的console配置
Destination destination = session.createQueue("FirstQueue");
// 消费者,消息接收者
MessageConsumer consumer = session.createConsumer(destination);
//设置接收者接收消息的等待时间
TextMessage message = (TextMessage) consumer.receive(5000);
if (null != message) {
System.out.println("收到消息" + message.getText());
}
6.优化接收类,使用监听器
consumer.setMessageListener(new MessageListener() {
public void onMessage(Message arg0){
TextMessage message = (TextMessage) arg0;
if (null != message) {
try {
System.out.println("收到消息" + message.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
System.out.println();
}
});
注:监听器中当次消息处理完成才会处理下一个消息。当有多个监听器监听同一队列时,消息平均分配给各个监听器,即使被分配的监听器正在处理上一个消息,本次消息也会等待此监听器处理完成,而不会分配给其它空闲的监听器,在等待过程中,如此监听器connection关闭,正在等待的消息会重新分配给其它监听器执行。
注:相对于Queue,Topic为一对多发送,发送端发送的消息,每个接收端都能收到,不过接收端启动监听前发送端发出的消息就收不到了。使用Topic,在MQ控制台新建一个Topic,发送端和接收端只修改一行代码:Destination destination = session.createTopic("topic名称");
二、使用SSL协议连接
1.在MQ目录/conf/activemq.xml文件中修改以下配置
<sslContext>
<sslContext keyStore="file:${activemq.base}/conf/mybroker.ks" keyStorePassword="pwd123"/>
</sslContext>
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ssl" uri="ssl://0.0.0.0:61617?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
注:broker.ks是MQ的默认证书,默认密码为password。其它不使用的协议可以删除。
2.将broker.ks broker.ts client.ks client.ts 复制到项目src下
3.发送端修改代码
ActiveMQSslConnectionFactory connectionFactory = new ActiveMQSslConnectionFactory(
"ssl://localhost:61617");
connectionFactory.setKeyAndTrustManagers(getKeyManagers(
"broker.ks", "password"), getTrustManagers("broker.ts"),
new SecureRandom());
public static KeyManager[] getKeyManagers(String keyStore,String keyStorePassword) throws Exception {
System.out.println("Initiating KeyManagers");
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(ClassLoader.getSystemResourceAsStream(keyStore),keyStorePassword.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyStorePassword.toCharArray());
System.out.println("Initiated KeyManagers");
return kmf.getKeyManagers();
}
public static TrustManager[] getTrustManagers(String trustStore)throws Exception {
System.out.println("Initiating TrustManagers");
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(ClassLoader.getSystemResourceAsStream(trustStore), null);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
System.out.println("Initiated TrustManagers");
return tmf.getTrustManagers();
}
注:接收端类似,传入client.ks client.ts即可
4.上面用到的证书是MQ自带的,正式环境中不可能用默认证书,否则SSL将形同虚设。因此,我们必须生成自己的证书,以保证其安全性。打开cmd 切换到项目src目录,执行以下命令:
keytool -genkey -alias broker -keyalg RSA -keystore mybroker.ks
keytool -export -alias broker -keystore mybroker.ks -file mybroker.cert
keytool -import -alias broker -keystore mybroker.ts -file mybroker.cert
keytool -genkey -alias client -keyalg RSA -keystore myclient.ks
keytool -import -alias broker -keystore myclient.ts -file mybroker.cert
说明:根据提示执行完上面的命令,src下会产生mybroker.cert mybroker.ks mybroker.ts myclient.ks myclient.ts 五个文件,将mybroker.ks复制到MQ/conf目录,修改sslContext的配置,修改代码使用自己生成的证书。
三、消息持久化
在broker中设置属性persistent=”true”(默认是true),同时发送的消息也应该是persitent类型的。ActiveMQ消息持久化有三种方式:AMQ、KahaDB、JDBC。
1.AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储在一个个文件中,文件的默认大小为32兆,如果一条消息的大小超过了32 兆,那么这个值必须设置大点。当一个存储文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。默认配置如下(conf/activemq.xml中):
<persistenceAdapter>
<amqPersistenceAdapterdirectory="activemq-data" maxFileLength="32mb"/>
</persistenceAdapter>
AMQ的属性:
属性名称 | 默认值 | 描述 |
directory | activemq-data | 消息文件和日志的存储目录 |
useNIO | true | 使用NIO协议存储消息 |
syncOnWrite | false | 同步写到磁盘,这个选项对性能影响非常大 |
maxFileLength | 32mb | 一个消息文件的大小 |
persistentIndex | true | 消息索引的持久化,如果为false,那么索引保存在内存中 |
maxCheckpointMessageAddSize | 4kb | 一个事务允许的最大消息量 |
cleanupInterval | 30000 | 清除操作周期,单位ms |
indexBinSize | 1024 | 索引文件缓存页面数,缺省为1024,当amq扩充或者缩减存储时,会锁定整个broker,导致一定时间的阻塞,所以这个值应该调整到比较大,但是代码中实现会动态伸缩,调整效果并不理想。 |
indexKeySize | 96 | 索引key的大小,key是消息ID |
indexPageSize | 16kb | 索引的页大小 |
directoryArchive | archive | 存储被归档的消息文件目录 |
archiveDataLogs | false | 当为true时,归档的消息文件被移到directoryArchive,而不是直接删除 |
2.KahaDB是基于文件的本地数据库储存形式,虽然没有AMQ的速度快,但是它具有强扩展性,恢复的时间比AMQ短,从5.4版本之后KahaDB做为默认的持久化方式。默认配置如下:
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
KahaDB的属性:
property name | default value | Comments |
directory | activemq-data | 消息文件和日志的存储目录 |
indexWriteBatchSize | 1000 | 一批索引的大小,当要更新的索引量到达这个值时,更新到消息文件中 |
indexCacheSize | 10000 | 内存中,索引的页大小 |
enableIndexWriteAsync | false | 索引是否异步写到消息文件中 |
journalMaxFileLength | 32mb | 一个消息文件的大小 |
enableJournalDiskSyncs | true | 是否讲非事务的消息同步写入到磁盘 |
cleanupInterval | 30000 | 清除操作周期,单位ms |
checkpointInterval | 5000 | 索引写入到消息文件的周期,单位ms |
ignoreMissingJournalfiles | false | 忽略丢失的消息文件,false,当丢失了消息文件,启动异常 |
checkForCorruptJournalFiles | false | 检查消息文件是否损坏,true,检查发现损坏会尝试修复 |
checksumJournalFiles | false | 产生一个checksum,以便能够检测journal文件是否损坏。 |
5.4版本之后有效的属性: | ||
archiveDataLogs | false | 当为true时,归档的消息文件被移到directoryArchive,而不是直接删除 |
directoryArchive | null | 存储被归档的消息文件目录 |
databaseLockedWaitDelay | 10000 | 在使用负载时,等待获得文件锁的延迟时间,单位ms |
maxAsyncJobs | 10000 | 同个生产者产生等待写入的异步消息最大量 |
concurrentStoreAndDispatchTopics | false | 当写入消息的时候,是否转发主题消息 |
concurrentStoreAndDispatchQueues | true | 当写入消息的时候,是否转发队列消息 |
5.6版本之后有效的属性: | ||
archiveCorruptedIndex | false | 是否归档错误的索引 |
从5.6版本之后,有可能发布通过多个kahadb持久适配器来实现分布式目标队列存储。什么时候用呢?如果有一个快速的生产者和消费者,当某一个 时刻生产者发生了不规范的消费,那么有可能产生一条消息被存储在两个消息文件中,同时,有些目标队列是危险的并且要求访问磁盘。在这种情况下,你应该用通 配符来使用mKahaDB。如果目标队列是分布的,事务是可以跨越多个消息文件的。
每个KahaDB的实例都可以配置单独的适配器,如果没有目标队列提交给filteredKahaDB,那么意味着对所有的队列有效。如果一个队列没有对应的适配器,那么将会抛出一个异常。配置如下:
<persistenceAdapter>
<mKahaDB directory="$/data/kahadb">
<filteredPersistenceAdapters>
<!-- match all queues -->
<filteredKahaDB queue=">">
<persistenceAdapter>
<kahaDB journalMaxFileLength="32mb" />
</persistenceAdapter>
</filteredKahaDB>
<!-- match all destinations -->
<filteredKahaDB>
<persistenceAdapter>
<kahaDB enableJournalDiskSyncs="false" />
</persistenceAdapter>
</filteredKahaDB>
</filteredPersistenceAdapters>
</mKahaDB>
</persistenceAdapter>
如果filteredKahaDB的perDestination属性设置为true,那么匹配的目标队列将会得到自己对应的KahaDB实例。配置如下:
<persistenceAdapter>
<mKahaDB directory="$/data/kahadb">
<filteredPersistenceAdapters>
<!-- kahaDB per destinations -->
<filteredKahaDB perDestination="true">
<persistenceAdapter>
<kahaDB journalMaxFileLength="32mb" />
</persistenceAdapter>
</filteredKahaDB>
</filteredPersistenceAdapters>
</mKahaDB>
</persistenceAdapter>
3.JDBC 配置JDBC适配器:
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#oracle-ds"
createTablesOnStartup="false" />
</persistenceAdapter>
<bean id="oracle-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@10.53.132.47:1521:activemq" />
<property name="username" value="activemq" />
<property name="password" value="activemq" />
<property name="maxActive" value="200" />
<property name="poolPreparedStatements" value="true" />
</bean>
四、配置安全性
ActiveMQ也可以对各个主题和队列设置用户名和密码,在conf/activemq.xml中的broker内添加:
<plugins>
<!-- Configure authentication; Username, passwords and groups -->
<simpleAuthenticationPlugin>
<users>
<authenticationUser username="system" password="manager"
groups="users,admins" />
<authenticationUser username="user" password="password"
groups="users" />
<authenticationUser username="guest" password="password"
groups="guests" />
<authenticationUser username="testUser" password="123456"
groups="testGroup" />
</users>
</simpleAuthenticationPlugin>
<!-- Lets configure a destination based authorization mechanism -->
<authorizationPlugin>
<map>
<authorizationMap>
<authorizationEntries>
<authorizationEntry queue="FirstQueue" read="testGroup" write="testGroup" />
<authorizationEntry queue=">" read="admins" write="admins" admin="admins" />
<authorizationEntry queue="USERS.>" read="users" write="users" admin="users" />
<authorizationEntry queue="GUEST.>" read="guests" write="guests,users" admin="guests,users" />
<authorizationEntry topic=">" read="admins" write="admins" admin="admins" />
<authorizationEntry topic="USERS.>" read="users" write="users" admin="users" />
<authorizationEntry topic="GUEST.>" read="guests" write="guests,users" admin="guests,users" />
<authorizationEntry topic="ActiveMQ.Advisory.>" read="guests,users ,testGroup"
write="guests,users,testGroup"
admin="guests,users,testGroup" />
</authorizationEntries>
</authorizationMap>
</map>
</authorizationPlugin>
</plugins>
注:simpleAuthenticationPlugin中设置用户名、密码和群组,authorizationPlugin设置主题和队列的访问群 组,“>”表示所有的主题或者队列。上面的配置中添加了一个testUser,属于群组testGroup,同时设置FirstQueue这个队列的访问读写权 限为testGroup,当然admins也可以访问的,因为admins是对所有的队列都有访问权限。将简单例子代码中的connectionFactory设置用户名和密码为用户testUser,如果密码不正确,将会抛出User name or password is invalid.异常,如果testUser所属的群组不能访问FirstQueue队列,那么会抛出User guest is not authorized to write to: queue://FirstQueue异常。需要注意的是所有的群组都需要对以ActiveMQ.Advisory为前缀的主题具有访问权限
五、与spring3整合
spring-aop-3.2.8.RELEASE.jar
spring-beans-3.2.8.RELEASE.jar
spring-context-3.2.8.RELEASE.jar
spring-core-3.2.8.RELEASE.jar
spring-expression-3.2.8.RELEASE.jar
spring-jms-3.2.8.RELEASE.jar
spring-tx-3.2.8.RELEASE.jar
<beans xmlns="Index of /schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="Index of /schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<property name="brokerURL">
<value>tcp://localhost:61616</value>
</property>
</bean>
<property name="connectionFactory">
<ref bean="connectionFactory" />
</property>
</bean>
<constructor-arg index="0">
<value>FirstQueue</value>
</constructor-arg>
</bean>
</beans>
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JmsTemplate jmsTemplate = (JmsTemplate) context.getBean("jmsTemplate");
Destination destination = (Destination) context.getBean("destination");
for (int i = 1; i < 10; i++) {
MyMessageCreator mc = new MyMessageCreator();// 生成消息
mc.n = i;
jmsTemplate.send(destination, mc);
Thread.sleep(2000);// 1秒后发送下一条消息
}
}
}
public int n = 0;
public Message createMessage(Session paramSession) throws JMSException {
return paramSession.createTextMessage("这个是第 " + n + " 个测试消息!");
}
}
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JmsTemplate jmsTemplate = (JmsTemplate) context.getBean("jmsTemplate");
Destination destination = (Destination) context.getBean("destination");
boolean isContinue = true;// 是否继续接收消息
while (isContinue) {
TextMessage msg = (TextMessage) jmsTemplate.receive(destination);
if (msg!=null) {
System.out.println("收到消息 :" + msg.getText());
}else{
isContinue = false;
}
}
System.out.println("程序退出了!");
}
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory -->
<property name="targetConnectionFactory" ref="targetConnectionFactory" />
</bean>
<property name="connectionFactory" ref="targetConnectionFactory" />
<property name="maxConnections" value="10" />
</bean>
<!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory -->
<property name="targetConnectionFactory" ref="pooledConnectionFactory" />
</bean>
SimpleMessageListenerContainer会在一开始的时候就创建一个会话session和消费者Consumer,并且会使用标准的JMS MessageConsumer.setMessageListener()方法注册监听器让JMS提供者调用监听器的回调函数。它不会动态的适应运行时需要和参与外部的事务管理。兼容性方面,它非常接近于独立的JMS规范,但一般不兼容Java EE的JMS限制。
大多数情况下我们还是使用的DefaultMessageListenerContainer,跟SimpleMessageListenerContainer相比,DefaultMessageListenerContainer会动态的适应运行时需要,并且能够参与外部的事务管理。它很好的平衡了对JMS提供者要求低、先进功能如事务参与和兼容Java EE环境。
<bean id="consumerMessageListener" class="com.hanjun.ConsumerMessageListener"/>
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="consumerMessageListener" />
</bean>
<!-- 消息监听适配器 -->
<bean id="messageListenerAdapter" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<property name="delegate" ref="textMessageListener"></property>
</bean>
public void handleMessage(String message) {
System.out.println("接收到一个纯文本消息。");
System.out.println("消息内容是:" + message);
}
}
<property name="delegate" ref="textMessageListener"></property>
<property name="defaultResponseDestination" ref="destination"/>
</bean>