今天我們來討論如何在專案開發中優雅地使用RocketMQ。本文分為三部分,第一部分實現SpringBoot與RocketMQ的整合,第二部分解決在使用RocketMQ過程中可能遇到的一些問題並解決他們,第三部分介紹如何封裝RocketMQ以便更好地使用。
在SpringBoot中整合RocketMQ,只需要簡單四步:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
rocketmq:
consumer:
group: springboot_consumer_group
# 一次拉取訊息最大值,注意是拉取訊息的最大值而非消費最大值
pull-batch-size: 10
name-server: 10.5.103.6:9876
producer:
# 傳送同一類訊息的設定為同一個group,保證唯一
group: springboot_producer_group
# 傳送訊息超時時間,預設3000
sendMessageTimeout: 10000
# 傳送訊息失敗重試次數,預設2
retryTimesWhenSendFailed: 2
# 非同步訊息重試此處,預設2
retryTimesWhenSendAsyncFailed: 2
# 訊息最大長度,預設1024 * 1024 * 4(預設4M)
maxMessageSize: 4096
# 壓縮訊息閾值,預設4k(1024 * 4)
compressMessageBodyThreshold: 4096
# 是否在內部傳送失敗時重試另一個broker,預設false
retryNextServer: false
@RestController
public class NormalProduceController {
@Setter(onMethod_ = @Autowired)
private RocketMQTemplate rocketmqTemplate;
@GetMapping("/test")
public SendResult test() {
Message<String> msg = MessageBuilder.withPayload("Hello,RocketMQ").build();
SendResult sendResult = rocketmqTemplate.send(topic, msg);
}
}
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(topic = "your_topic_name", consumerGroup = "your_consumer_group_name")
public class MyConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
// 處理訊息的邏輯
System.out.println("Received message: " + message);
}
}
以上4步即可實現SpringBoot與RocketMQ的整合,這部分屬於基礎知識,不做過多說明。
以下是一些在SpringBoot中使用RocketMQ時常遇到的問題,現在為您逐一解決。
啟動專案時會在紀錄檔中看到如下告警
RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.InternalThreadLocalMap).
RocketMQLog:WARN Please initialize the logger system properly.
此時我們只需要在啟動類中設定環境變數 rocketmq.client.logUseSlf4j
為 true 明確指定RocketMQ的紀錄檔框架
@SpringBootApplication
public class RocketDemoApplication {
public static void main(String[] args) {
/*
* 指定使用的紀錄檔框架,否則將會告警
* RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.InternalThreadLocalMap).
* RocketMQLog:WARN Please initialize the logger system properly.
*/
System.setProperty("rocketmq.client.logUseSlf4j", "true");
SpringApplication.run(RocketDemoApplication.class, args);
}
}
同時還得在組態檔中調整紀錄檔級別,不然在控制檯會一直看到broker的紀錄檔資訊
logging:
level:
RocketmqClient: ERROR
io:
netty: ERROR
在使用Java8後經常會使用LocalDate/LocalDateTime
這兩個時間型別欄位,然而RocketMQ原始設定並不支援Java時間型別,當我們傳送的實體訊息中包含上述兩個欄位時,消費端在消費時會出現如下所示的錯誤。
比如生產者的程式碼如下:
@GetMapping("/test")
public void test(){
//普通訊息無返回值,只負責傳送訊息⽽不等待伺服器迴應且沒有回撥函數觸發。
RocketMessage rocketMessage = RocketMessage.builder().
id(1111L).
message("hello,world")
.localDate(LocalDate.now())
.localDateTime(LocalDateTime.now())
.build();
rocketmqTemplate.convertAndSend(destination,rocketMessage);
}
消費者的程式碼如下:
@Component
@RocketMQMessageListener(consumerGroup = "springboot_consumer_group",topic = "consumer_topic")
public class RocketMQConsumer implements RocketMQListener<RocketMessage> {
@Override
public void onMessage(RocketMessage message) {
System.out.println("消費訊息-" + message);
}
}
消費者開始消費時會出現型別轉換異常錯誤Cannot construct instance of java.time.LocalDate
,錯誤詳情如下:
原因:RocketMQ內建使用的轉換器是RocketMQMessageConverter,轉換Json時使用的是MappingJackson2MessageConverter,但是這個轉換器不支援時間型別。
解決辦法:需要自定義訊息轉換器,將MappingJackson2MessageConverter進行替換,並新增支援時間模組
@Configuration
public class RocketMQEnhanceConfig {
/**
* 解決RocketMQ Jackson不支援Java時間型別設定
* 原始碼參考:{@link org.apache.rocketmq.spring.autoconfigure.MessageConverterConfiguration}
*/
@Bean
@Primary
public RocketMQMessageConverter enhanceRocketMQMessageConverter(){
RocketMQMessageConverter converter = new RocketMQMessageConverter();
CompositeMessageConverter compositeMessageConverter = (CompositeMessageConverter) converter.getMessageConverter();
List<MessageConverter> messageConverterList = compositeMessageConverter.getConverters();
for (MessageConverter messageConverter : messageConverterList) {
if(messageConverter instanceof MappingJackson2MessageConverter){
MappingJackson2MessageConverter jackson2MessageConverter = (MappingJackson2MessageConverter) messageConverter;
ObjectMapper objectMapper = jackson2MessageConverter.getObjectMapper();
objectMapper.registerModules(new JavaTimeModule());
}
}
return converter;
}
}
在使用RocketMQ時,通常會在程式碼中直接指定訊息主題(topic),而且開發環境和測試環境可能共用一個RocketMQ環境。如果沒有進行處理,在開發環境傳送的訊息就可能被測試環境的消費者消費,測試環境傳送的訊息也可能被開發環境的消費者消費,從而導致資料混亂的問題。
為了解決這個問題,我們可以根據不同的環境實現自動隔離。通過簡單設定一個選項,如dev、test、prod等不同環境,所有的訊息都會被自動隔離。例如,當傳送的訊息主題為consumer_topic
時,可以自動在topic後面加上環境字尾,如consumer_topic_dev
。
那麼,我們該如何實現呢?
可以編寫一個設定類實現BeanPostProcessor,並重寫postProcessBeforeInitialization方法,在監聽器範例初始化前修改對應的topic。
BeanPostProcessor是Spring框架中的一個介面,它的作用是在Spring容器範例化、設定完bean之後,在bean初始化前後進行一些額外的處理工作。
具體來說,BeanPostProcessor介面定義了兩個方法:
- postProcessBeforeInitialization(Object bean, String beanName): 在bean初始化之前進行處理,可以對bean做一些修改等操作。
- postProcessAfterInitialization(Object bean, String beanName): 在bean初始化之後進行處理,可以進行一些清理或者其他操作。
BeanPostProcessor可以在應用程式中對Bean的建立和初始化過程進行攔截和修改,對Bean的生命週期進行干預和操作。它可以對所有的Bean類範例進行增強處理,使得開發人員可以在Bean初始化前後自定義一些操作,從而實現自己的業務需求。比如,可以通過BeanPostProcessor來實現注入某些必要的屬性值、加入某一個物件等等。
實現方案如下:
rocketmq:
enhance:
# 啟動隔離,用於啟用設定類EnvironmentIsolationConfig
# 啟動後會自動在topic上拼接啟用的組態檔,達到自動隔離的效果
enabledIsolation: true
# 隔離環境名稱,拼接到topic後,topic_dev,預設空字串
environment: dev
@Configuration
public class EnvironmentIsolationConfig implements BeanPostProcessor {
@Value("${rocketmq.enhance.enabledIsolation:true}")
private boolean enabledIsolation;
@Value("${rocketmq.enhance.environment:''}")
private String environmentName;
/**
* 在裝載Bean之前實現引數修改
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof DefaultRocketMQListenerContainer){
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
//拼接Topic
if(enabledIsolation && StringUtils.hasText(environmentName)){
container.setTopic(String.join("_", container.getTopic(),environmentName));
}
return container;
}
return bean;
}
}
2023-03-23 17:04:59.726 [main] INFO o.a.r.s.support.DefaultRocketMQListenerContainer:290 - running container: DefaultRocketMQListenerContainer{consumerGroup='springboot_consumer_group', nameServer='10.5.103.6:9876', topic='consumer_topic_dev', consumeMode=CONCURRENTLY, selectorType=TAG, selectorExpression='*', messageModel=CLUSTERING}
在解釋為什麼要二次封裝之前先來看看RocketMQ官方檔案中推薦的最佳實踐
訊息傳送成功或者失敗要列印訊息紀錄檔,用於業務排查問題。
如果訊息量較少,建議在消費入口方法列印訊息,消費耗時等,方便後續排查問題。
RocketMQ 無法避免訊息重複(Exactly-Once),所以如果業務對消費重複非常敏感,務必要在業務層面進行去重處理。可以藉助關聯式資料庫進行去重。首先需要確定訊息的唯一鍵,可以是msgId,也可以是訊息內容中的唯一標識欄位,例如訂單Id等。
上面三個步驟基本每次傳送訊息或者消費訊息都要實現,屬於重複動作。
接下來討論的是在RocketMQ中傳送訊息時選擇何種訊息型別最為合適。
在RocketMQ中有四種可選格式:
對於如何選擇訊息型別,需要考慮到消費者在不檢視訊息傳送者的情況下,如何獲取訊息的含義。因此,在這種情況下,使用第三種方式即根據業務封裝對應實體類的方式最為合適,也是大多數開發者在傳送訊息時的常用方式。
有了上面兩點結論以後我們來看看為什麼要對RocketMQ二次封裝。
按照上述最佳實踐,一個完整的訊息傳遞鏈路從生產到消費應包括 準備訊息、傳送訊息、記錄訊息紀錄檔、處理傳送失敗、記錄接收訊息紀錄檔、處理業務邏輯、例外處理和異常重試 等步驟。
雖然使用原生RocketMQ可以完成這些動作,但每個生產者和消費者都需要編寫大量重複的程式碼來完成相同的任務,這就是需要進行二次封裝的原因。我們希望通過二次封裝,**生產者只需準備好訊息實體並呼叫封裝後的工具類傳送,而消費者只需處理核心業務邏輯,其他公共邏輯會得到統一處理。 **
在二次封裝中,關鍵是找出框架在日常使用中所涵蓋的許多操作,以及區分哪些操作是可變的,哪些是不變的。以上述例子為例,實際上只有生產者的訊息準備和消費者的業務處理是可變的操作,需要根據需求進行處理,而其他步驟可以固定下來形成一個模板。
當然,本文提到的二次封裝不是指對原始碼進行封裝,而是針對工具的原始使用方式進行的封裝。可以將其與Mybatis和Mybatis-plus區分開來。這兩者都能完成任務,只不過Mybatis-plus更為簡單便捷。
實現二次封裝需要建立一個自定義的starter,這樣其他專案只需要依賴此starter即可使用封裝功能。同時,在自定義starter中還需要解決文章第二部分中提到的一些問題。
程式碼結構如下所示:
/**
* 訊息實體,所有訊息都需要繼承此類
* 公眾號:JAVA日知錄
*/
@Data
public abstract class BaseMessage {
/**
* 業務鍵,用於RocketMQ控制檯檢視消費情況
*/
protected String key;
/**
* 傳送訊息來源,用於排查問題
*/
protected String source = "";
/**
* 傳送時間
*/
protected LocalDateTime sendTime = LocalDateTime.now();
/**
* 重試次數,用於判斷重試次數,超過重試次數傳送異常警告
*/
protected Integer retryTimes = 0;
}
後面所有傳送的訊息實體都需要繼承此實體類。
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class RocketMQEnhanceTemplate {
private final RocketMQTemplate template;
@Resource
private RocketEnhanceProperties rocketEnhanceProperties;
public RocketMQTemplate getTemplate() {
return template;
}
/**
* 根據系統上下文自動構建隔離後的topic
* 構建目的地
*/
public String buildDestination(String topic, String tag) {
topic = reBuildTopic(topic);
return topic + ":" + tag;
}
/**
* 根據環境重新隔離topic
* @param topic 原始topic
*/
private String reBuildTopic(String topic) {
if(rocketEnhanceProperties.isEnabledIsolation() && StringUtils.hasText(rocketEnhanceProperties.getEnvironment())){
return topic +"_" + rocketEnhanceProperties.getEnvironment();
}
return topic;
}
/**
* 傳送同步訊息
*/
public <T extends BaseMessage> SendResult send(String topic, String tag, T message) {
// 注意分隔符
return send(buildDestination(topic,tag), message);
}
public <T extends BaseMessage> SendResult send(String destination, T message) {
// 設定業務鍵,此處根據公共的引數進行處理
// 更多的其它基礎業務處理...
Message<T> sendMessage = MessageBuilder.withPayload(message).setHeader(RocketMQHeaders.KEYS, message.getKey()).build();
SendResult sendResult = template.syncSend(destination, sendMessage);
// 此處為了方便檢視給紀錄檔轉了json,根據選擇選擇紀錄檔記錄方式,例如ELK採集
log.info("[{}]同步訊息[{}]傳送結果[{}]", destination, JSONObject.toJSON(message), JSONObject.toJSON(sendResult));
return sendResult;
}
/**
* 傳送延遲訊息
*/
public <T extends BaseMessage> SendResult send(String topic, String tag, T message, int delayLevel) {
return send(buildDestination(topic,tag), message, delayLevel);
}
public <T extends BaseMessage> SendResult send(String destination, T message, int delayLevel) {
Message<T> sendMessage = MessageBuilder.withPayload(message).setHeader(RocketMQHeaders.KEYS, message.getKey()).build();
SendResult sendResult = template.syncSend(destination, sendMessage, 3000, delayLevel);
log.info("[{}]延遲等級[{}]訊息[{}]傳送結果[{}]", destination, delayLevel, JSONObject.toJSON(message), JSONObject.toJSON(sendResult));
return sendResult;
}
}
這裡封裝了一個訊息傳送類,實現了紀錄檔記錄以及自動重建topic的功能(即生產者實現環境隔離),後面專案中只需要注入RocketMQEnhanceTemplate來實現訊息的傳送。
@Slf4j
public abstract class EnhanceMessageHandler<T extends BaseMessage> {
/**
* 預設重試次數
*/
private static final int MAX_RETRY_TIMES = 3;
/**
* 延時等級
*/
private static final int DELAY_LEVEL = EnhanceMessageConstant.FIVE_SECOND;
@Resource
private RocketMQEnhanceTemplate rocketMQEnhanceTemplate;
/**
* 訊息處理
*
* @param message 待處理訊息
* @throws Exception 消費異常
*/
protected abstract void handleMessage(T message) throws Exception;
/**
* 超過重試次數訊息,需要啟用isRetry
*
* @param message 待處理訊息
*/
protected abstract void handleMaxRetriesExceeded(T message);
/**
* 是否需要根據業務規則過濾訊息,去重邏輯可以在此處處理
* @param message 待處理訊息
* @return true: 本次訊息被過濾,false:不過濾
*/
protected boolean filter(T message) {
return false;
}
/**
* 是否異常時重複傳送
*
* @return true: 訊息重試,false:不重試
*/
protected abstract boolean isRetry();
/**
* 消費異常時是否丟擲異常
* 返回true,則由rocketmq機制自動重試
* false:消費異常(如果沒有開啟重試則訊息會被自動ack)
*/
protected abstract boolean throwException();
/**
* 最大重試次數
*
* @return 最大重試次數,預設5次
*/
protected int getMaxRetryTimes() {
return MAX_RETRY_TIMES;
}
/**
* isRetry開啟時,重新入隊延遲時間
* @return -1:立即入隊重試
*/
protected int getDelayLevel() {
return DELAY_LEVEL;
}
/**
* 使用模板模式構建訊息消費框架,可自由擴充套件或刪減
*/
public void dispatchMessage(T message) {
// 基礎紀錄檔記錄被父類別處理了
log.info("消費者收到訊息[{}]", JSONObject.toJSON(message));
if (filter(message)) {
log.info("訊息id{}不滿足消費條件,已過濾。",message.getKey());
return;
}
// 超過最大重試次數時呼叫子類方法處理
if (message.getRetryTimes() > getMaxRetryTimes()) {
handleMaxRetriesExceeded(message);
return;
}
try {
long now = System.currentTimeMillis();
handleMessage(message);
long costTime = System.currentTimeMillis() - now;
log.info("訊息{}消費成功,耗時[{}ms]", message.getKey(),costTime);
} catch (Exception e) {
log.error("訊息{}消費異常", message.getKey(),e);
// 是捕獲異常還是丟擲,由子類決定
if (throwException()) {
//丟擲異常,由DefaultMessageListenerConcurrently類處理
throw new RuntimeException(e);
}
//此時如果不開啟重試機制,則預設ACK了
if (isRetry()) {
handleRetry(message);
}
}
}
protected void handleRetry(T message) {
// 獲取子類RocketMQMessageListener註解拿到topic和tag
RocketMQMessageListener annotation = this.getClass().getAnnotation(RocketMQMessageListener.class);
if (annotation == null) {
return;
}
//重新構建訊息體
String messageSource = message.getSource();
if(!messageSource.startsWith(EnhanceMessageConstant.RETRY_PREFIX)){
message.setSource(EnhanceMessageConstant.RETRY_PREFIX + messageSource);
}
message.setRetryTimes(message.getRetryTimes() + 1);
SendResult sendResult;
try {
// 如果訊息傳送不成功,則再次重新傳送,如果傳送異常則丟擲由MQ再次處理(異常時不走延遲訊息)
sendResult = rocketMQEnhanceTemplate.send(annotation.topic(), annotation.selectorExpression(), message, getDelayLevel());
} catch (Exception ex) {
// 此處捕獲之後,相當於此條訊息被訊息完成然後重新傳送新的訊息
//由生產者直接傳送
throw new RuntimeException(ex);
}
// 傳送失敗的處理就是不進行ACK,由RocketMQ重試
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
throw new RuntimeException("重試訊息傳送失敗");
}
}
}
使用模版設計模式定義了訊息消費的骨架,實現了紀錄檔列印,例外處理,異常重試等公共邏輯,訊息過濾(查重)、業務處理則交由子類實現。
@Configuration
@EnableConfigurationProperties(RocketEnhanceProperties.class)
public class RocketMQEnhanceAutoConfiguration {
/**
* 注入增強的RocketMQEnhanceTemplate
*/
@Bean
public RocketMQEnhanceTemplate rocketMQEnhanceTemplate(RocketMQTemplate rocketMQTemplate){
return new RocketMQEnhanceTemplate(rocketMQTemplate);
}
/**
* 解決RocketMQ Jackson不支援Java時間型別設定
* 原始碼參考:{@link org.apache.rocketmq.spring.autoconfigure.MessageConverterConfiguration}
*/
@Bean
@Primary
public RocketMQMessageConverter enhanceRocketMQMessageConverter(){
RocketMQMessageConverter converter = new RocketMQMessageConverter();
CompositeMessageConverter compositeMessageConverter = (CompositeMessageConverter) converter.getMessageConverter();
List<MessageConverter> messageConverterList = compositeMessageConverter.getConverters();
for (MessageConverter messageConverter : messageConverterList) {
if(messageConverter instanceof MappingJackson2MessageConverter){
MappingJackson2MessageConverter jackson2MessageConverter = (MappingJackson2MessageConverter) messageConverter;
ObjectMapper objectMapper = jackson2MessageConverter.getObjectMapper();
objectMapper.registerModules(new JavaTimeModule());
}
}
return converter;
}
/**
* 環境隔離設定
*/
@Bean
@ConditionalOnProperty(name="rocketmq.enhance.enabledIsolation", havingValue="true")
public EnvironmentIsolationConfig environmentSetup(RocketEnhanceProperties rocketEnhanceProperties){
return new EnvironmentIsolationConfig(rocketEnhanceProperties);
}
}
public class EnvironmentIsolationConfig implements BeanPostProcessor {
private RocketEnhanceProperties rocketEnhanceProperties;
public EnvironmentIsolationConfig(RocketEnhanceProperties rocketEnhanceProperties) {
this.rocketEnhanceProperties = rocketEnhanceProperties;
}
/**
* 在裝載Bean之前實現引數修改
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof DefaultRocketMQListenerContainer){
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
if(rocketEnhanceProperties.isEnabledIsolation() && StringUtils.hasText(rocketEnhanceProperties.getEnvironment())){
container.setTopic(String.join("_", container.getTopic(),rocketEnhanceProperties.getEnvironment()));
}
return container;
}
return bean;
}
}
@ConfigurationProperties(prefix = "rocketmq.enhance")
@Data
public class RocketEnhanceProperties {
private boolean enabledIsolation;
private String environment;
}
<dependency>
<groupId>com.jianzh5</groupId>
<artifactId>cloud-rocket-starter</artifactId>
</dependency>
rocketmq:
...
enhance:
# 啟動隔離,用於啟用設定類EnvironmentIsolationConfig
# 啟動後會自動在topic上拼接啟用的組態檔,達到自動隔離的效果
enabledIsolation: true
# 隔離環境名稱,拼接到topic後,topic_dev,預設空字串
environment: dev
@RestController
@RequestMapping("enhance")
@Slf4j
public class EnhanceProduceController {
//注入增強後的模板,可以自動實現環境隔離,紀錄檔記錄
@Setter(onMethod_ = @Autowired)
private RocketMQEnhanceTemplate rocketMQEnhanceTemplate;
private static final String topic = "rocket_enhance";
private static final String tag = "member";
/**
* 傳送實體訊息
*/
@GetMapping("/member")
public SendResult member() {
String key = UUID.randomUUID().toString();
MemberMessage message = new MemberMessage();
// 設定業務key
message.setKey(key);
// 設定訊息來源,便於查詢
message.setSource("MEMBER");
// 業務訊息內容
message.setUserName("Java日知錄");
message.setBirthday(LocalDate.now());
return rocketMQEnhanceTemplate.send(topic, tag, message);
}
}
注意這裡使用的是封裝後的模板工具類,一旦在組態檔中啟動環境隔離,則生產者的訊息也自動傳送到隔離後的topic中。
@Slf4j
@Component
@RocketMQMessageListener(
consumerGroup = "enhance_consumer_group",
topic = "rocket_enhance",
selectorExpression = "*",
consumeThreadMax = 5 //預設是64個執行緒並行訊息,設定 consumeThreadMax 引數指定並行消費執行緒數,避免太大導致資源不夠
)
public class EnhanceMemberMessageListener extends EnhanceMessageHandler<MemberMessage> implements RocketMQListener<MemberMessage> {
@Override
protected void handleMessage(MemberMessage message) throws Exception {
// 此時這裡才是最終的業務處理,程式碼只需要處理資源類關閉異常,其他的可以交給父類別重試
System.out.println("業務訊息處理:"+message.getUserName());
}
@Override
protected void handleMaxRetriesExceeded(MemberMessage message) {
// 當超過指定重試次數訊息時此處方法會被呼叫
// 生產中可以進行回退或其他業務操作
log.error("訊息消費失敗,請執行後續處理");
}
/**
* 是否執行重試機制
*/
@Override
protected boolean isRetry() {
return true;
}
@Override
protected boolean throwException() {
// 是否丟擲異常,false搭配retry自行處理異常
return false;
}
@Override
protected boolean filter() {
// 訊息過濾
return false;
}
/**
* 監聽消費訊息,不需要執行業務處理,委派給父類別做基礎操作,父類別做完基礎操作後會呼叫子類的實際處理型別
*/
@Override
public void onMessage(MemberMessage memberMessage) {
super.dispatchMessage(memberMessage);
}
}
為了方便消費者對RocketMQ中的訊息進行處理,我們可以使用EnhanceMessageHandler來進行訊息的處理和邏輯的處理。
消費者實現了RocketMQListener的同時,可以繼承EnhanceMessageHandler來進行公共邏輯的處理,而核心業務邏輯需要自己實現handleMessage
方法。 如果需要對訊息進行過濾或者去重的處理,則可以重寫父類別的filter方法進行實現。這樣可以更加方便地對訊息進行處理,減輕開發者的工作量。
以上,就是今天的主要內容,希望對你有所幫助!