1、分布式事务出现场景
场景描述:支付宝转账余额宝
分布式事务必须满足的条件:
1、远程RPC调用,支付宝和余额宝存在接口调用
2、支付宝和余额宝使用不同的数据库
如图:
2、分布式事务解决方案
1、基于数据库XA协议的两段提交
XA协议是数据库支持的一种协议,其核心是一个事务管理器用来统一管理两个分布式数据库,如图
事务管理器负责跟支付宝数据库和余额宝数据库打交道,一旦有一个数据库连接失败,另一个数据库的操作就不会进行,一个数据库操作失败就会导致另一个数据库回滚,只有他们全部成功两个数据库的事务才会提交。
基于XA协议的两段和三段提交是一种严格的安全确认机制,其安全性是非常高的,但是保证安全性的前提是牺牲了性能,这个就是分布式系统里面的CAP理论,做任何架构的前提需要有取舍。所以基于XA协议的分布式事务并发性不高,不适合高并发场景。
2、基于activemq的解决方案
如图:
1、支付宝扣款成功时往message表插入消息
2、message表有message_id(流水id,标识夸系统的一次转账操作),status(confirm,unconfirm)
3、timer扫描message表的unconfirm状态记录往activemq插入消息
4、余额宝收到消息消费消息时先查询message表如果有记录就不处理如果没记录就进行数据库增款操作
5、如果余额宝数据库操作成功往余额宝message表插入消息,表字段跟支付宝message一致
6、如果5操作成功,回调支付宝接口修改message表状态,把unconfirm状态转换成confirm状态
问题描述:
1、支付宝设计message表的目的
如果支付宝往activemq插入消息而余额宝消费消息异常,有可能是消费消息成功而事务操作异常,有可能是网络异常等等不确定因素。如果出现异常而activemq收到了确认消息的信号,这时候activemq中的消息是删除了的,消息丢失了。设置message表就是有一个消息存根,activemq中消息丢失了message表中的消息还在。解决了activemq消息丢失问题
2、余额宝设计message表的目的
当余额宝消费成功并且数据库操作成功时,回调支付宝的消息确认接口,如果回调接口时出现异常导致支付宝状态修改失败还是unconfirm状态,这时候还会被timer扫描到,又会往activemq插入消息,又会被余额宝消费一边,但是这条消息已经消费成功了的只是回调失败而已,所以就需要有一个这样的message表,当余额宝消费时先插入message表,如果message根据message_id能查询到记录就说明之前这条消息被消费过就不再消费只需要回调成功即可,如果查询不到消息就消费这条消息继续数据库操作,数据库操作成功就往message表插入消息。 这样就解决了消息重复消费问题,这也是消费端的幂等操作。
基于消息中间件的分布式事务是最理想的分布式事务解决方案,兼顾了安全性和并发性!
接下来贴代码:
支付宝代码:
@Controller
@RequestMapping("/order")
public class OrderController {
/**
* @Description TODO
* @param @return 参数
* @return String 返回类型
* @throws
*
* userID:转账的用户ID
* amount:转多少钱
*/
@Autowired
@Qualifier("activemq")
OrderService orderService;
@RequestMapping("/transfer")
public @ResponseBody String transferAmount(String userId,String messageId, int amount) {
try {
orderService.updateAmount(amount,messageId, userId);
}
catch (Exception e) {
e.printStackTrace();
return "===============================transferAmount failed===================";
}
return "===============================transferAmount successfull===================";
}
@RequestMapping("/callback")
public String callback(String param) {
JSONObject parse = JSONObject.parseObject(param);
String respCode = parse.getString("respCode");
if(!"OK".equalsIgnoreCase(respCode)) {
return null;
}
try {
orderService.updateMessage(param);
}catch (Exception e) {
e.printStackTrace();
return "fail";
}
return "ok";
}
}
public interface OrderService {
public void updateAmount(int amount, String userId,String messageId);
public void updateMessage(String param);
}
@Service("activemq")
@Transactional(rollbackFor = Exception.class)
public class OrderServiceActivemqImpl implements OrderService {
Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
JmsTemplate jmsTemplate;
@Override
public void updateAmount(final int amount, final String messageId, final String userId) {
String sql = "update account set amount = amount - ?,update_time=now() where user_id = ?";
int count = jdbcTemplate.update(sql, new Object[]{amount, userId});
if (count == 1) {
//插入到消息记录表
sql = "insert into message(user_id,message_id,amount,status) values (?,?,?,?)";
int row = jdbcTemplate.update(sql,new Object[]{userId,messageId,amount,"unconfirm"});
if(row == 1) {
//往activemq中插入消息
jmsTemplate.send("zg.jack.queue", new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
com.zhuguang.jack.bean.Message message = new com.zhuguang.jack.bean.Message();
message.setAmount(Integer.valueOf(amount));
message.setStatus("unconfirm");
message.setUserId(userId);
message.setMessageId(messageId);
return session.createObjectMessage(message);
}
});
}
}
}
@Override
public void updateMessage(String param) {
JSONObject parse = JSONObject.parseObject(param);
String messageId = parse.getString("messageId");
String sql = "update message set status = ? where message_id = ?";
int count = jdbcTemplate.update(sql,new Object[]{"confirm",messageId});
if(count == 1) {
logger.info(messageId + " callback successfull");
}
}
}
activemq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:amq="http://activemq.apache.org/schema/core"
xmlns:jms="http://www.springframework.org/schema/jms"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms-4.1.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"
>
<context:component-scan base-package="com.zhuguang.jack" />
<mvc:annotation-driven />
<amq:connectionFactory id="amqConnectionFactory"
brokerURL="tcp://192.168.88.131:61616"
userName="system"
password="manager" />
<!-- 配置JMS连接工长 -->
<bean id="connectionFactory"
class="org.springframework.jms.connection.CachingConnectionFactory">
<constructor-arg ref="amqConnectionFactory" />
<property name="sessionCacheSize" value="100" />
</bean>
<!-- 定义消息队列(Queue) -->
<bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue">
<!-- 设置消息队列的名字 -->
<constructor-arg>
<value>zg.jack.queue</value>
</constructor-arg>
</bean>
<!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestination" ref="demoQueueDestination" />
<property name="receiveTimeout" value="10000" />
<!-- true是topic,false是queue,默认是false,此处显示写出false -->
<property name="pubSubDomain" value="false" />
</bean>
</beans>
spring-dispatcher.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util" xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
">
<!-- 引入同文件夹下的redis属性配置文件 -->
<!-- 解决springMVC响应数据乱码 text/plain就是响应的时候原样返回数据-->
<import resource="../activemq/activemq.xml"/>
<!--<context:property-placeholder ignore-unresolvable="true" location="classpath:config/core/core.properties,classpath:config/redis/redis-config.properties" />-->
<bean id="propertyConfigurerForProject1" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="location">
<value>classpath:config/core/core.properties</value>
</property>
</bean>
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 避免IE执行AJAX时,返回JSON出现下载文件 -->
<bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
<!-- 开启controller注解支持 -->
<!-- 注:如果base-package=com.avicit 则注解事务不起作用 TODO 读源码 -->
<context:component-scan base-package="com.zhuguang">
</context:component-scan>
<mvc:view-controller path="/" view-name="redirect:/index" />
<bean
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<bean id="handlerAdapter"
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
</bean>
<bean
class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
<entry key="html" value="text/html" />
</map>
</property>
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
</list>
</property>
</bean>
<!-- 支持上传文件 -->
<!-- 控制器异常处理 -->
<bean id="exceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">
error
</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass">
<value>${jdbc.driverClassName}</value>
</property>
<property name="jdbcUrl">
<value>${jdbc.url}</value>
</property>
<property name="user">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
<property name="minPoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="maxIdleTime" value="1800" />
<property name="acquireIncrement" value="3" />
<property name="maxStatements" value="1000" />
<property name="initialPoolSize" value="10" />
<property name="idleConnectionTestPeriod" value="60" />
<property name="acquireRetryAttempts" value="30" />
<property name="breakAfterAcquireFailure" value="false" />
<property name="testConnectionOnCheckout" value="false" />
<property name="acquireRetryDelay">
<value>100</value>
</property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />
<aop:aspectj-autoproxy expose-proxy="true"/>
</beans>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<!-- 定义日志的根目录 -->
<!-- <property name="LOG_HOME" value="/app/log" /> -->
<!-- 定义日志文件名称 -->
<property name="appName" value="netty"></property>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<Encoding>UTF-8</Encoding>
<!--
日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Encoding>UTF-8</Encoding>
<!-- 指定日志文件的名称 -->
<file>${appName}.log</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>
<!--
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!--
日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</encoder>
</appender>
<!--
logger主要用于存放日志对象,也可以定义日志类型、级别
name:表示匹配的logger类型前缀,也就是包的前半部分
level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,false:表示只用当前logger的appender-ref,true:表示当前logger的appender-ref和rootLogger的appender-ref都有效
-->
<!-- <logger name="edu.hyh" level="info" additivity="true">
<appender-ref ref="appLogAppender" />
</logger> -->
<!--
root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="debug">
<appender-ref ref="stdout" />
<appender-ref ref="appLogAppender" />
</root>
</configuration>
2、余额宝代码
package com.zhuguang.jack.controller;
import com.alibaba.fastjson.JSONObject;
import com.zhuguang.jack.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/order")
public class OrderController {
/**
* @Description TODO
* @param @return 参数
* @return String 返回类型
* @throws
*
* 模拟银行转账
* userID:转账的用户ID
* amount:转多少钱
*/
@Autowired
OrderService orderService;
@RequestMapping("/transfer")
public @ResponseBody String transferAmount(String userId, String amount) {
try {
orderService.updateAmount(Integer.valueOf(amount), userId);
}
catch (Exception e) {
e.printStackTrace();
return "===============================transferAmount failed===================";
}
return "===============================transferAmount successfull===================";
}
}
消息监听器
package com.zhuguang.jack.listener;
import com.alibaba.fastjson.JSONObject;
import com.zhuguang.jack.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;
@Service("queueMessageListener")
public class QueueMessageListener implements MessageListener {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
OrderService orderService;
@Transactional(rollbackFor = Exception.class)
@Override
public void onMessage(Message message) {
if (message instanceof ObjectMessage) {
ObjectMessage objectMessage = (ObjectMessage) message;
try {
com.zhuguang.jack.bean.Message message1 = (com.zhuguang.jack.bean.Message) objectMessage.getObject();
String userId = message1.getUserId();
int count = orderService.queryMessageCountByUserId(userId);
if (count == 0) {
orderService.updateAmount(message1.getAmount(), message1.getUserId());
orderService.insertMessage(message1.getUserId(), message1.getMessageId(), message1.getAmount(), "ok");
} else {
logger.info("异常转账");
}
RestTemplate restTemplate = createRestTemplate();
JSONObject jo = new JSONObject();
jo.put("messageId", message1.getMessageId());
jo.put("respCode", "OK");
String url = "http://jack.bank_a.com:8080/alipay/order/callback?param="
+ jo.toJSONString();
restTemplate.getForObject(url,null);
} catch (JMSException e) {
e.printStackTrace();
throw new RuntimeException("异常");
}
}
}
public RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
simpleClientHttpRequestFactory.setConnectTimeout(3000);
simpleClientHttpRequestFactory.setReadTimeout(2000);
return new RestTemplate(simpleClientHttpRequestFactory);
}
}
package com.zhuguang.jack.service;
public interface OrderService {
public void updateAmount(int amount, String userId);
public int queryMessageCountByUserId(String userId);
public int insertMessage(String userId,String messageId,int amount,String status);
}
package com.zhuguang.jack.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
JdbcTemplate jdbcTemplate;
/*
* 更新数据库表,把账户余额减去amountd
*/
@Override
public void updateAmount(int amount, String userId) {
//1、农业银行转账3000,也就说农业银行jack账户要减3000
String sql = "update account set amount = amount + ?,update_time=now() where user_id = ?";
int count = jdbcTemplate.update(sql, new Object[] {amount, userId});
if (count != 1) {
throw new RuntimeException("订单创建失败,农业银行转账失败!");
}
}
public RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
simpleClientHttpRequestFactory.setConnectTimeout(3000);
simpleClientHttpRequestFactory.setReadTimeout(2000);
return new RestTemplate(simpleClientHttpRequestFactory);
}
@Override
public int queryMessageCountByUserId(String messageId) {
String sql = "select count(*) from message where message_id = ?";
int count = jdbcTemplate.queryForInt(sql, new Object[]{messageId});
return count;
}
@Override
public int insertMessage(String userId, String message_id,int amount, String status) {
String sql = "insert into message(user_id,message_id,amount,status) values(?,?,?)";
int count = jdbcTemplate.update(sql, new Object[]{userId, message_id,amount, status});
if(count == 1) {
logger.info("Ok");
}
return count;
}
}
activemq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:amq="http://activemq.apache.org/schema/core"
xmlns:jms="http://www.springframework.org/schema/jms"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms-4.1.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"
>
<context:component-scan base-package="com.zhuguang.jack" />
<mvc:annotation-driven />
<amq:connectionFactory id="amqConnectionFactory"
brokerURL="tcp://192.168.88.131:61616"
userName="system"
password="manager" />
<!-- 配置JMS连接工长 -->
<bean id="connectionFactory"
class="org.springframework.jms.connection.CachingConnectionFactory">
<constructor-arg ref="amqConnectionFactory" />
<property name="sessionCacheSize" value="100" />
</bean>
<!-- 定义消息队列(Queue) -->
<bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue">
<!-- 设置消息队列的名字 -->
<constructor-arg>
<value>zg.jack.queue</value>
</constructor-arg>
</bean>
<!-- 显示注入消息监听容器(Queue),配置连接工厂,监听的目标是demoQueueDestination,监听器是上面定义的监听器 -->
<bean id="queueListenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="demoQueueDestination" />
<property name="messageListener" ref="queueMessageListener" />
</bean>
<!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestination" ref="demoQueueDestination" />
<property name="receiveTimeout" value="10000" />
<!-- true是topic,false是queue,默认是false,此处显示写出false -->
<property name="pubSubDomain" value="false" />
</bean>
</beans>
OK~~~~~~~~~~~~大功告成!!!, 如果大家觉得满意并且对技术感兴趣请加群:171239762, 纯技术交流群,非诚勿扰。