乐趣区

关于java: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<User> 为啥发个 Map 过去?

  看到这个谬误,着实让我摸不到头脑。登时一堆疑难用上心头,为啥单个对象能够,List 就不行呢?我发的是 List<User> 数据,为啥变成 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);
  1. 降级版本:因为 GH-1729: Fix JSON Regression 合并到了 2.2.13.RELEASE。所以只须要将 spring-amqp 降级到 2.2.13.RELEASE 或以上。或者降级 SpringBoot 版本到 2.3.7.RELEASE。

结尾

  如果感觉对你有帮忙,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够顺手点个关注哦,谢谢。

  我是不一样的科技宅,每天提高一点点,体验不一样的生存。咱们下期见!

退出移动版