Spring提供了一个JmsTransactionManager用于对JMS ConnectionFactory做事务管理。这将允许JMS应用利用Spring的事务管理特性。JmsTransactionManager在执行本地资源事务管理时将从指定的ConnectionFactory绑定一个ConnectionFactory/Session这样的配对到线程中。JmsTemplate会自动检测这样的事务资源,并对它们进行相应操作。
在Java EE环境中,ConnectionFactory会池化Connection和Session,这样这些资源将会在整个事务中被有效地重复利用。在一个独立的环境中,使用Spring的SingleConnectionFactory时所有的事务将公用一个Connection,但是每个事务将保留自己独立的Session。
JmsTemplate可以利用JtaTransactionManager和能够进行分布式的 JMS ConnectionFactory处理分布式事务。
在Spring整合JMS的应用中,如果我们要进行本地的事务管理的话非常简单,只需要在定义对应的消息监听容器时指定其sessionTransacted属性为true,如:
- <bean id="jmsContainer"
- class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="connectionFactory" />
- <property name="destination" ref="queueDestination" />
- <property name="messageListener" ref="consumerMessageListener" />
- <property name="sessionTransacted" value="true"/>
- </bean>
该属性值默认为false,这样JMS在进行消息监听的时候就会进行事务控制,当在接收消息时监听器执行失败时JMS就会对接收到的消息进行回滚,对于SessionAwareMessageListener在接收到消息后发送一个返回消息时也处于同一事务下,但是对于其他操作如数据库访问等将不属于该事务控制。
这里我们可以来做一个这样的测试:我们如上配置监听在queueDestination的消息监听容器的sessionTransacted属性为true,然后把我们前面提到的消息监听器ConsumerMessageListener改成这样:
- public class ConsumerMessageListener implements MessageListener {
- public void onMessage(Message message) {
- //这里我们知道生产者发送的就是一个纯文本消息,所以这里可以直接进行强制转换,或者直接把onMessage方法的参数改成Message的子类TextMessage
- TextMessage textMsg = (TextMessage) message;
- System.out.println("接收到一个纯文本消息。");
- try {
- System.out.println("消息内容是:" + textMsg.getText());
- if (1 == 1) {
- throw new RuntimeException("Error");
- }
- } catch (JMSException e) {
- e.printStackTrace();
- }
- }
- }
我们可以看到在上述代码中我们的ConsumerMessageListener在进行消息接收的时候抛出了一个RuntimeException,根据我们上面说的,因为我们已经在对应的监听容器上定义了其sessionTransacted属性为true,所以当这里抛出异常的时候JMS将对接收到的消息进行回滚,即下次进行消息接收的时候该消息仍然能够被接收到。为了验证这一点,我们先执行一遍测试代码,往queueDestination发送一个文本消息,这个时候ConsumerMessageListener在进行接收的时候将会抛出一个RuntimeException,已经接收到的纯文本消息将进行回滚;接着我们去掉上面代码中抛出异常的语句,即ConsumerMessageListener能够正常的进行消息接收,这个时候我们再运行一次测试代码,往ConsumerMessageListener监听的queueDestination发送一条消息。如果之前在接手时抛出了异常的那条消息已经回滚了的话,那么这个时候将能够接收到两条消息,控制台将输出接收到的两条消息的内容。具体结果有兴趣的朋友可以自己验证一下。
如果想接收消息和数据库访问处于同一事务中,那么我们就可以配置一个外部的事务管理同时配置一个支持外部事务管理的消息监听容器(如DefaultMessageListenerContainer)。要配置这样一个参与分布式事务管理的消息监听容器,我们可以配置一个JtaTransactionManager,当然底层的JMS ConnectionFactory需要能够支持分布式事务管理,并正确地注册我们的JtaTransactionManager。这样消息监听器进行消息接收和对应的数据库访问就会处于同一数据库控制下,当消息接收失败或数据库访问失败都会进行事务回滚操作。
- <bean id="jmsContainer"
- class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="connectionFactory" />
- <property name="destination" ref="queueDestination" />
- <property name="messageListener" ref="consumerMessageListener" />
- <property name="transactionManager" ref="jtaTransactionManager"/>
- </bean>
- <bean id="jtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
当给消息监听容器指定了transactionManager时,消息监听容器将忽略sessionTransacted的值。
关于使用JtaTransactionManager来管理上述分布式事务,我们这里也可以来做一个试验。
首先:往Spring配置文件applicationContext.xml中添加如下配置:
- <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
- <property name="dataSource" ref="dataSource"/>
- </bean>
- <jee:jndi-lookup jndi-name="jdbc/mysql" id="dataSource"/>
- <bean id="jtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
- <tx:annotation-driven transaction-manager="jtaTransactionManager"/>
我们可以看到,在这里我们引入了一个jndi数据源,定义了一个JtaTransactionManager,定义了Spring基于注解的声明式事务管理,定义了一个Spring提供的进行Jdbc操作的工具类jdbcTemplate。
接下来把我们的ConsumerMessageListener改为如下形式:
- public class ConsumerMessageListener implements MessageListener {
- @Autowired
- private TestDao testDao;
- private int count = 0;
- public void onMessage(Message message) {
- //这里我们知道生产者发送的就是一个纯文本消息,所以这里可以直接进行强制转换,或者直接把onMessage方法的参数改成Message的子类TextMessage
- TextMessage textMsg = (TextMessage) message;
- System.out.println(new Date().toLocaleString() + "接收到一个纯文本消息。");
- try {
- String text = textMsg.getText();
- System.out.println("消息内容是:" + text);
- System.out.println("当前count的值是:" + count);
- testDao.insert(text + count);
- if (count == 0) {
- count ++;
- throw new RuntimeException("Error! 出错啦!");
- }
- } catch (JMSException e) {
- e.printStackTrace();
- }
- }
- }
我们可以看到,在ConsumerMessageListener中我们定义了一个实例变量count,其初始值为0;在onMessage里面,我们可以看到我们把接收到的消息内容作为参数调用了testDao的insert方法;当count值为0,也就是进行第一次消息接收的时候会将count的值加1,同时抛出一个运行时异常。那么我们这里要测试的就是进行第一次接收的时候testDao已经把相关内容插入数据库了,接着在onMessage里面抛出了一个异常同时count加1,我们预期的结果应该是此时数据库进行回滚,同时JMS也回滚,这样JMS将继续尝试接收该消息,此时同样会调用testDao的insert方法将内容插入数据库,再接着count已经不为0了,所以此时将不再抛出异常,JMS成功进行消息的接收,testDao也成功的将消息内容插入到了数据库。要证明这个预期我们除了看数据库中插入的数据外,还可以看控制台的输出,正常情况控制台将输出两次消息接收的内容,且第一次时count为0,第二次count为1。
TestDao是一个接口,其TestDaoImpl对insert的方法实现如下:
这里我们使用支持JtaTransactionManager的Weblogic来进行测试,因为是Web容器,所以我们这里定义了一个Controller来进行消息的发送,具体代码如下:
- @Controller
- @RequestMapping("test")
- public class TestController {
- @Autowired
- @Qualifier("queueDestination")
- private Destination destination;
- @Autowired
- private ProducerService producerService;
- @RequestMapping("first")
- public String first() {
- producerService.sendMessage(destination, "你好,现在是:" + new Date().toLocaleString());
- return "/test/first";
- }
- }
接下来就是启用Weblogic服务器,进入其控制台,定义一个名叫“jdbc/mysql”的JNDI数据源,然后把该项目部署到Weblogic服务器上并进行启动。接下来我们就可以访问/test/first.do访问到上述first方法。之后控制台会输出如下信息:
我们可以看到当count为0时接收了一次,并随后抛出了异常,之后count为1又接收了一次,这说明在count为0时抛出异常后我们的JMS进行回滚了,那么我们的数据库是否有进行回滚呢?接着我们来看数据库中的内容:
我们可以看到数据库表中只有一条记录,而且最后一位表示count的值的为1,这说明在JMS进行消息接收抛出异常时我们的数据库也回滚了。关于使用JtaTransactionManager进行分布式事务管理的问题就说到这里了,有兴趣的朋友可以自己试验一下。
- SpringInteJMS.zip (109.4 KB)
- 下载次数: 854
顶
踩
参考知识库
- 深度学习知识库 1176 关注 | 198 收录
- React Native知识库 951 关注 | 485 收录
- iOS知识库 854 关注 | 1144 收录
- 直播技术知识库 2151 关注 | 894 收录
评论
有个问题是DefaultMessageListenerContainer设置了sessionTransacted='true'后。如果抛异常是可以rollback重新放回队列,重新处理的。但是这里有个“限制”。
就是重新放回队列 7次后,第8次就没法重新放回队列中去了。。与此同时listener就处理后续队列中的message。最终导致的结果是,抛异常的那条message丢失了。
看源码也找不到具体出问题的原因。不知道博主注意到这个问题没有。
附相关代码
1. 接收message的listener:
- public class EmailReceiveListener {
- private AtomicInteger count = new AtomicInteger(1);
- public void receive(Email email){
- if (email==null || email.getTitle().contains("3")){
- System.out.println("[ERROR][Email]"+email + ";count"+count.getAndIncrement());
- throw new RuntimeException("3点必须不能开会!!!");
- }else{
- System.out.println("[EmailReceiveListener][Email]"+email);
- }
- }
- }
2. 然后是container的配置
- <bean id="emailMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="connectionFactory"/>
- <property name="destination" ref="converterQueue"/>
- <property name="messageListener" ref="emailMessageListenerAdapter"/>
- <property name="sessionTransacted" value="true"/>
- </bean>
3. 最后是发送的简单例子
- public static void main(String[] args) {
- ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"applicationContext-jms-spring.xml"});
- context.start();
- EmailSender sender3 = (EmailSender)context.getBean("emailSender");
- Destination converterQueue = (Destination)context.getBean("converterQueue");
- Email email = new Email();
- email.setContent("系统服务化");
- email.setFrom("小黑手");
- email.setSendTo("D team");
- for (int i=0;i<10;i++) {
- try {
- Thread.sleep(Long.valueOf(new Random().nextInt(1000)));
- email.setTitle("下午 "+i+" 点开会");
- sender3.send(converterQueue,email);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
其他就不贴了。都是正常的。
console打印如下:
- [EmailReceiveListener][Email]Email{title='下午 0 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 1 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 2 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count1
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count2
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count3
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count4
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count5
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count6
- [ERROR][Email]Email{title='下午 3 点开会', content='系统服务化', sendTo='D team', from='小黑手'};count7
- [EmailReceiveListener][Email]Email{title='下午 4 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 5 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 6 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 7 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 8 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
- [EmailReceiveListener][Email]Email{title='下午 9 点开会', content='系统服务化', sendTo='D team', from='小黑手'}
不知道博主能否指点下。我目前想到的解决办法是,如果抛异常,不采用自动rollback,而是重新send到队列中。
已解决,自己偷懒catch了所有exception,造成异常没抛出来
博文里面已经展示了对应的测试情况。是不是哪里配置有误啥的?
我应该是直接拿它的代码跑的 要说配置 除了改了下jtaTransactionManager 例子中的跑步起来 我就改成这样 <bean id="jtaTransactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref bean="atomikosTransactionManager" />
</property>
<property name="userTransaction">
<ref bean="atomikosUserTransaction" />
</property>
</bean>
还有dataSource jndi的配置也改成c3p0的了 我觉得应该没有影响
你能跑起来么 可以发我邮箱谢谢 zzsszz0106@163.com
dataSource就需要是jndi配置的。
博文里面已经展示了对应的测试情况。是不是哪里配置有误啥的?
我应该是直接拿它的代码跑的 要说配置 除了改了下jtaTransactionManager 例子中的跑步起来 我就改成这样 <bean id="jtaTransactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref bean="atomikosTransactionManager" />
</property>
<property name="userTransaction">
<ref bean="atomikosUserTransaction" />
</property>
</bean>
还有dataSource jndi的配置也改成c3p0的了 我觉得应该没有影响
你能跑起来么 可以发我邮箱谢谢 zzsszz0106@163.com
博文里面已经展示了对应的测试情况。是不是哪里配置有误啥的?
直接指定sessionTransacted="true",在从JMS容器接收消息的时候就会有对消息接收的事务管理了,不需要自己再去配置JmsTransactionManager。如果要自己配置的话配置方式跟文中介绍的JtaTransactionManager的配置方式类似,只是其需要多注入一个ConnectionFactory。至于HibernateTransactionManager应该是不支持对JMS进行事务管理的。
消息接收的事务管理使用sessionTransacted="true"管理就可以了,同意lz的观点。如果接收消息的同时支持数据库事务管理貌似必须使用JtaTransactionManager事务管理了,且需要应用服务器支持JtaTransactionManager的配置方式。如Weblogic、JBoos等。如果使用tomcat需要使用第三方支持XA协议的JTOM或者atomikos。网址: http://www.jakubkorab.net/2011/08/configuring-activemq-transactions-in-spring.html比较清晰。多谢lz的回答!
文中有提到分布式事务管理需要使用JtaTransactionManager的,即可以让JMS事务与数据库事务处于同一事务中。
直接指定sessionTransacted="true",在从JMS容器接收消息的时候就会有对消息接收的事务管理了,不需要自己再去配置JmsTransactionManager。如果要自己配置的话配置方式跟文中介绍的JtaTransactionManager的配置方式类似,只是其需要多注入一个ConnectionFactory。至于HibernateTransactionManager应该是不支持对JMS进行事务管理的。
消息接收的事务管理使用sessionTransacted="true"管理就可以了,同意lz的观点。如果接收消息的同时支持数据库事务管理貌似必须使用JtaTransactionManager事务管理了,且需要应用服务器支持JtaTransactionManager的配置方式。如Weblogic、JBoos等。如果使用tomcat需要使用第三方支持XA协议的JTOM或者atomikos。网址: http://www.jakubkorab.net/2011/08/configuring-activemq-transactions-in-spring.html比较清晰。多谢lz的回答!
直接指定sessionTransacted="true",在从JMS容器接收消息的时候就会有对消息接收的事务管理了,不需要自己再去配置JmsTransactionManager。如果要自己配置的话配置方式跟文中介绍的JtaTransactionManager的配置方式类似,只是其需要多注入一个ConnectionFactory。至于HibernateTransactionManager应该是不支持对JMS进行事务管理的。