之前在《實現應用內分佈式事務管理(生產者)》章節已經重點介紹了生產者如何實現應用內的本地事務、分佈式事務,對於消費者同樣有事務管理的需求,本章節將重點介紹springboot下目前消費者本地事務。
<optional>true</optional><!-- optional=true,依賴不會傳遞,該項目依賴devtools;之後依賴SpringBoot1項目的項目如果想要使用devtools,需要重新引入 -->
package com.shf.activemq;
import javax.jms.Queue;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.EnableJms;
@SpringBootApplication
@EnableJms
public class AppConsumer
{
public static void main(String[] args) throws Exception {
SpringApplication.run(AppConsumer.class, args);
}
/**
* 定義點對點隊列
* @return
*/
@Bean
public Queue queue() {
return new ActiveMQQueue("my.queue");
}
}
事務驗證
首先來看看我們隊列中已有的數據情況:
1、監聽my.consuqueue隊列,並在消費消息後生成my.queue中消息:
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue;
/**
* 1、隊列中數據要麼重試指定次數後出列進入DLQ死信隊列要麼不出列
* 2、如果在同一個@JmsListener註解監聽方法體內,我們有不僅接受消息,同時也進行消息的發送,則一旦出現異常事務回滾,則無論是消費還是生產均能夠事務回滾
* @param text
* @throws Exception
*/
@JmsListener(destination = "my.consuqueue")
public void receiveQueue(String text) throws Exception {
System.out.println("消費者:來源於生產者的消息:"+text);
this.jmsMessagingTemplate.convertAndSend(this.queue, "生產者2辛苦生產的點對點消息成果");
System.out.println("生產者2:辛苦生產的點對點消息成果");
throw new Exception("出現異常了");
}
預期:如果存在異常,則不會在my.queue生產消息也不會消費my.consuqueue中消息,如果沒有異常則正常消費&生成:
1.1、通過throw 拋出異常,數據沒有變化:
監聽時間再長一點,其會在嘗試6次失敗後加入死信隊列
1.2、取消異常,
小結,預期驗證成功,本地事務生效:
a、隊列中數據要麼重試指定次數後出列進入DLQ死信隊列要麼不出列
b、如果在同一個@JmsListener註解監聽方法體內,我們有不僅接受消息,同時也進行消息的發送,則一旦出現異常事務回滾,則無論是消費還是生產均能夠事務回滾
c、在一定程度上我們通過自定義的響應隊列實現了應答模式;
2、在官方看到了一個@SendTo註解,其能夠很好的實現異步應答模式,在1的數據基礎上,我們繼續驗證:
/**
* 通過@SendTo實現監聽隊列消息後同步發送其他消息,事務生效(監聽消費、jmsMessagingTemplate發送、@SendTo3者在同一事務下)
* @SendTo 發佈的消息也爲隊列消息
* @param text
* @return
*/
@JmsListener(destination = "my.queue")
@SendTo("status")
public String listenQueue(String text) {
System.out.println("消費者:來源於生產者2的點對點消息:"+text);
this.jmsMessagingTemplate.convertAndSend(this.queue, "生產者辛苦生產的點對點消息成果");
System.out.println("生產者:辛苦生產的點對點消息成果");
System.out.println(1/0);
return "send status";
}
預期:沒有異常的情況下,my.queue被消費完,my.consuqueue隊列生成3條消息,並且在status中也生產了3條消息;有異常的情況下,3個隊列數據沒有變化,3個隊列處理在一個事務下。
2.1、拋出異常時,數據沒有變化:
2.2、無異常,隊列數據:
小結:
a、事務生效,@JmsListener、jmsMessagingTemplate、@SendTo 三者在同一事務下;
b、@SendTo能夠方便的實現應答模式;
3、下面我們嘗試下實體對象生產,首先定義一個User對象
class User implements Serializable{
private static final long serialVersionUID = 4375692247237084682L;
private String name;
private int age;
public User(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
然後定義監聽
/**
* 監聽到字符串狀態消息後發出User對象消息,事務生效
* 對象實體一定需要序列化,默認僅Supported message payloads are: String, byte array, Map<String,?>, Serializable object.
* @param text
* @return
*/
@JmsListener(destination = "status")
@SendTo("user")
public User listenStatus(String text) {
System.out.println("消費者:來源於生產者的消息:"+text);
User user=new User("帥帥",11);
// System.out.println(1/0);
return user;
}
小結:
a、我們生產的實體對象消息必須序列化,JMS默認Supported message payloads(消息體) are:String, byte array, Map<String,?>, Serializable object;
b、事務仍然生效;
4、消費對象實體
編寫監聽
/**
* 監聽直接獲取User對象實體,出現異常通過控制檯提示http://activemq.apache.org/objectmessage.html查看官方
* 需要設置spring.activemq.packages.trust-all(默認值爲false)爲true,也可以設置spring.activemq.packages.trusted指定包路徑
* 如果自定義實現ActiveMQConnectionFactory則可以設置其trustAllPackages屬性爲true
* @param text
* @return
*/
@JmsListener(destination = "user")
@SendTo("user2")
public User listenUser(User user) {
System.out.println("消費者:來源於生產者的用戶對象消息:"+user.toString());
User user2=new User("帥帥",22);
return user2;
}
添加配置
# default value s false
spring.activemq.packages.trust-all=true
控制檯打印消費的User對象
小結:
a、監聽直接獲取User對象實體,出現異常通過控制檯提示http://activemq.apache.org/objectmessage.html查看官方需要設置spring.activemq.packages.trust-all(默認值爲false)爲true,也可以設置spring.activemq.packages.trusted指定包路徑,如果自定義實現ActiveMQConnectionFactory則可以設置其trustAllPackages屬性爲true。
5、通過JmsResponse可以在運行中同比實現@sendTo功能:
/**
* 通過JmsResponse可以在運行中同比實現@sendTo功能,如果我們需要在header中攜帶部分屬性,則可以通過Message實現
* @param user
* @return
*/
@JmsListener(destination = "user2")
public JmsResponse<Message<User>> JmsResponseUser(User user) {
System.out.println("消費者:來源於生產者的用戶對象消息:"+user.toString());
Message<User> response = MessageBuilder
.withPayload(user)//消息體
.setHeader("code", 1234)//消息頭
.build();
return JmsResponse.forQueue(response, "headeruser");
}
控制檯
小結
a、通過JmsResponse可以實現@sendTo功能,並且可以通過Message攜帶Header信息;
6、監聽讀取header中的信息以及用戶信息
/**
* 監聽讀取header中的信息以及用戶信息
* @param message
*/
@JmsListener(destination = "headeruser")
public void listenMessageHeader(Message<User> message) {
System.out.println("消費者:來源於生產者的用戶對象消息:"+message.getPayload().toString()+";header中的code信息:"+message.getHeaders().get("code"));
}
控制檯
小結:
a、header信息的讀取同樣通過Message即可。
7、之前所有的消息處理全部基於隊列,在生產者模式下通過自定義JmsTemlate可以設置一個隊列模板一個發佈訂閱模板,那麼在消費者下,我們默認也是僅可監聽隊列消息,故我們需要調整配置:
# default value s false,just control queue message
spring.jms.pub-sub-domain=true
消費者監聽:
/**
* 需要設置spring.jms.pub-sub-domain爲true方可監聽發佈訂閱類消息
* 通過@SendTo實現監聽消息後同步發送其他消息,事務生效(N次重試)
* @SendTo 發佈的消息也爲發佈訂閱消息
* @param text
* @return
*/
@JmsListener(destination = "my.topic")
@SendTo("replyTopic")
public String listenTopic(String text) {
System.out.println("消費者:來源於生產者的發佈訂閱消息:"+text);
// System.out.println(1/0);
return "reply replyTopic";
}
控制檯
小結:
a、通過spring.jms.pub-sub-domain配置可以切換當前工程默認的監聽類型,但同時卻僅能監聽一個類型消費。後續章節將通過自定義配置實現同時監聽隊列和發佈訂閱兩個類型消息。
b、事務生效;
備註:通過JmsTemlate我們也可以很方便的實現應答模式,主要應用在同步場景下,通過Message的setJMSReplyTo方法傳遞接收應答的目的地,消費者獲取到此目的地發出響應信息,生產者接收處理;
附錄
本章節主要驗證均採用了@JmsListener實現異步消息消費,通過JmsTemlate可以實現消息的同步消費處理,其並不在本章的範疇。
大致配置如下:
@Configuration
public class JmsTemplateConfiguration {
private final JmsProperties properties;
private final ObjectProvider<DestinationResolver> destinationResolver;
private final ObjectProvider<MessageConverter> messageConverter;
public JmsTemplateConfiguration(JmsProperties properties,
ObjectProvider<DestinationResolver> destinationResolver,
ObjectProvider<MessageConverter> messageConverter) {
this.properties = properties;
this.destinationResolver = destinationResolver;
this.messageConverter = messageConverter;
}
@Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
jmsTemplate.setPubSubDomain(this.properties.isPubSubDomain());
DestinationResolver destinationResolver = (DestinationResolver) this.destinationResolver.getIfUnique();
if (destinationResolver != null) {
jmsTemplate.setDestinationResolver(destinationResolver);
}
MessageConverter messageConverter = (MessageConverter) this.messageConverter.getIfUnique();
if (messageConverter != null) {
jmsTemplate.setMessageConverter(messageConverter);
}
//deliveryMode, priority, timeToLive 的開關,要生效,必須配置爲true,默認false
jmsTemplate.setExplicitQosEnabled(true);
//DeliveryMode.NON_PERSISTENT=1:非持久 ; DeliveryMode.PERSISTENT=2:持久
jmsTemplate.setDeliveryMode(DeliveryMode.PERSISTENT);
//默認不開啓事務
System.out.println("是否開啓事務"+jmsTemplate.isSessionTransacted());
//如果session帶有事務,並且事務成功提交,則消息被自動簽收。如果事務回滾,則消息會被再次傳送。
//jmsTemplate.setSessionTransacted(true);
//不帶事務的session的簽收方式,取決於session的配置。
//默認消息確認方式爲1,即AUTO_ACKNOWLEDGE
System.out.println("是否消息確認方式"+jmsTemplate.getSessionAcknowledgeMode());
//消息的應答方式,需要手動確認,此時SessionTransacted必須被設置爲false,且爲Session.CLIENT_ACKNOWLEDGE模式
//Session.AUTO_ACKNOWLEDGE 消息自動簽收
//Session.CLIENT_ACKNOWLEDGE 客戶端調用acknowledge方法手動簽收
//Session.DUPS_OK_ACKNOWLEDGE 不必必須簽收,消息可能會重複發送
jmsTemplate.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
return jmsTemplate;
}
}
此時如果我們配置的消息簽收模式爲客戶端手動確認,則簡單看了下源碼,可能存在一定的問題
如果session支持事務則提交事務,反之如果是客戶端確認其message不爲空則簽收確認,但此時可能會有異常發生,不能被默認簽收,如果需要更好的顯示執行手動簽收,則需要繼承JmsTemplate對其doReceive改造並新增顯示acknowledge方法。