詳細聊聊RabbitMQ竟無法反序列化List問題

前言

最近在接到瞭一個需求,大概是通過RabbitMq給xx子系統同步用戶數據,要提供單個同步和批量同步。內心暗喜這不簡單的很嘛。三下五除二就把代碼給寫完瞭,大概長這樣:

public void syncUserSingle(User user) {
    // 省略一大堆業務代碼
    rabbitTemplate.convertAndSend("q_sync_user_single", user);
}

public void syncUserBatch(List<User> userList) {
    // 省略一大堆業務代碼
    rabbitTemplate.convertAndSend("q_sync_user_batch", userList);
}

但是在聯調的過程中,遇到瞭一個比較奇葩的問題。單個用戶進行同步時,子系統可以正常消費。然後進行批量同步的時候,子系統報錯瞭。並拋出java.lang.ClassCastException提示 LinkedHashMap cannot xxxx class 。於是負責子系統的哥們笑嘻嘻的(表面笑嘻嘻)走過來對我說,不是約定List 為啥發個Map過來?

看到這個錯誤,著實讓我摸不到頭腦。頓時一堆疑問湧上心頭, 為啥單個對象可以,List就不行呢?我發的是List 數據,為啥變成Map瞭?雖然一大堆疑問,但是隻能笑嘻嘻的說,我檢查一下哈。

問題重現

項目依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/>
    </parent>
    <!-- 省略部分信息 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>
</project>

發送方

初始化隊列

@Configuration
public class QueueConfig {
    @Bean
    public Queue test() {
        return new Queue("test");
    }
}

配置RabbitTemplete

@Configuration
public class RabbitTemplateConfig {
    @Autowired
    public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
        // 設置Json消息轉換器
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    }
}

發送接口

@Controller
@RequestMapping("/test")
public class TestController {

    @Resource
    private RabbitTemplate template;

    @GetMapping("/send")
    public void send() {
        template.convertAndSend("test", Collections.singletonList(new User(20, "不一樣的科技宅")));
    }
}

User類

@Data
@AllArgsConstructor
public class User {
    /**
     * 年齡
     */
    private Integer age;

    /**
     * 姓名
     */
    private String name;
}

接收方

監聽配置

@Configuration
public class RabbitListenerConfig {

    @Bean
    public SimpleRabbitListenerContainerFactory customFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
                                                              ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        // 設置消息轉換器
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        configurer.configure(factory, connectionFactory);
        return factory;
    }

}

接收方

@Service
public class UserService {

    public void save(List<User> userList) {
        userList.forEach(System.out::println);
    }
    
}
@Componentpublic class Receiver {    @Resource    private UserService userService;    @RabbitListener(queues = "test", containerFactory = "customFactory")    public void receive(@Payload List<User> msg) {        userService.save(msg);    }}

錯誤日志

❝好傢夥果然失敗瞭,這百分百必現的bug呀。❞

分析問題原因

首先錯誤信息是在消費端拋出來的,按理應該是消費端出問題概率較大。但是如果和他說的一樣,我生產端發送的消息就是錯誤的,從而導致消費端出問題呢?這對這個疑問,我先斷開消費端,然後發送一條消息,並通過Rabbitmq的管控臺來查看消息的內容是否正確。

消息內容如下圖所示:

通過上圖可以發現,消息體(payload)是一個標準的json串,並且TypeId也是List,並不是錯誤信息中的LinkedHashMap。哈哈哈,到此可以石錘是消費端反序列化的問題瞭。趕緊把鍋甩出去,抽他呀的(自嗨而已),我寫的代碼怎麼可能有bug。

對我愛學習的我,肯定不願意就這樣算瞭。必須刨根問底,給他上一課。於是我在google一圈發現這竟然是這個bug。有個老哥也發現瞭,並提交瞭一個issues: spring-ampq/issues/1279。

大致是說:嘗試從 Spring Boot 2.3.1 升級到 2.3.3,然後再升級到 2.3.6。錯誤信息依然是:List<Foo> foos是LikedHashMap,而不是Foo對象。並通過遠程調試確認瞭這種情況。出於某種原因,他認為沒有正確使用泛型類型。恢復到 Spring-AMQP 2.2.7 使它再次工作,並且對象確實是Foo。

然後garyrussell這個人說:他們添加瞭對抽象類反序列化的支持,如果配置不正確,這會對消息轉換器產生一些副作用。然後調查瞭一下,確認這是一個錯誤。是由於List是抽象的,新代碼認為它不能反序列化。

解決方法是:

converter.setAlwaysConvertToInferredType(true);

後面還提到在 GH-1729: Fix JSON Regression修復這個問題,修復的代碼如下:

通過閱讀代碼發現,修改前的邏輯是: 如果推斷類型是抽象的,則返回false也就代表不能轉換成推斷類型。然後被轉換成LinkedHashMap。這也就是出現 LinkedHashMap cannot cast xxxx class的主要原因。

修改後變成瞭:如果推斷類型是抽象的並且不是容器類型,返回false。也就意味著,雖然推斷類型是抽象的,但是如果是容器類型,並且容器內的對象不是抽象的,則可以被轉換。這樣一來避免瞭上述問題的產生瞭。

前面還提到瞭通過增加配置來解決。解決起來就相對簡單粗暴瞭,始終轉換推斷類型。

解決辦法

到此問題分析完畢,簡單總結一下解決方法。主要有兩種:

1、在消費端開啟如下配置即可:

// 始終轉換推斷類型
converter.setAlwaysConvertToInferredType(true);

2、升級版本:由於GH-1729: Fix JSON Regression合並到瞭2.2.13.RELEASE。所以隻需要將 spring-amqp 升級到 2.2.13.RELEASE 或以上。或者升級SpringBoot版本到2.3.7.RELEASE。

總結

到此這篇關於RabbitMQ竟無法反序列化List的文章就介紹到這瞭,更多相關RabbitMQ無法反序列化List內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: