Preface

Recently, I received a request, probably to synchronize user data to the xx subsystem through RabbitMq, to provide single synchronization and batch synchronization. It's not easy to be secretly happy in my heart. The code is written after three times, five times, and two times. It looks like this:

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);
}

But in the process of joint debugging, we encountered a rather strange problem. When a single user synchronizes, the subsystem can consume normally. Then when performing batch synchronization, the subsystem reported an error. And throw java.lang.ClassCastException prompt LinkedHashMap cannot xxxx class . So the guy in charge of the subsystem smiled (smiling on the surface) and came over to me and said, "Isn't it an agreement to send a Map to List<User>?

Seeing this error, I really couldn't figure it out. Suddenly, a lot of questions are in my mind. Why is a single object OK, but not a List? What I sent is List<User> data, why has it become a Map? Although there are a lot of questions, I can only say with a smile, let me check.

Reproduce the problem

  • Project dependency
<?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>

sender

  • Initialize the queue
@Configuration
public class QueueConfig {
    @Bean
    public Queue test() {
        return new Queue("test");
    }
}
  • Configure RabbitTemplete
@Configuration
public class RabbitTemplateConfig {
    @Autowired
    public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
        // 设置Json消息转换器
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    }
}
  • Send interface
@Controller
@RequestMapping("/test")
public class TestController {

    @Resource
    private RabbitTemplate template;

    @GetMapping("/send")
    public void send() {
        template.convertAndSend("test", Collections.singletonList(new User(20, "不一样的科技宅")));
    }
}
  • User class
@Data
@AllArgsConstructor
public class User {
    /**
     * 年龄
     */
    private Integer age;

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

receiver

  • Monitor configuration
@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;
    }

}
  • receiver
@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);    }}

Error log

The good guy really failed. This is a 100% bug.

Analyze the cause of the problem

First of all, the error message is thrown on the consumer side. It should be that the consumer side has a higher probability of problems. But what if, as he said, the message sent by my production side is wrong, which causes problems on the consumer side? For this question, I disconnect the consumer first, then send a message, and check whether the content of the message is correct through Rabbitmq's control console.

The content of the message is shown in the figure below:

Can be found on the map, a message body (payload) is a standard json string, and also TypeId List , the error message is not LinkedHashMap . Hahaha, at this point, it is the problem of deserialization on the consumer side. Quickly throw the pot out and smoke him (self-heavy), how could the code I wrote have bugs.

For me who loves to learn, I definitely don't want to let it go. He must be thoroughly questioned and taught him a lesson. So I found out on Google that this turned out to be this bug. An old man also found out and submitted an issue: spring-ampq/issues/1279 .

Roughly speaking: try to upgrade from Spring Boot 2.3.1 to 2.3.3, and then upgrade to 2.3.6. The error message is still: List<Foo> foos is LikedHashMap, not Foo object. And confirmed this situation through remote debugging. For some reason, he believes that generic types are not used correctly. Reverting to Spring-AMQP 2.2.7 makes it work again, and the object is indeed Foo.

Then Garyrussell said: They added support for deserialization of abstract classes. If the configuration is not correct, this will have some side effects on the message converter. Then I investigated and confirmed that this was a mistake. Because List is abstract, the new code believes that it cannot be deserialized.

The solution is:

converter.setAlwaysConvertToInferredType(true);

It is mentioned later that GH-1729: Fix JSON Regression . The repaired code is as follows:

After reading the code, it is found that the logic before modification is: If the inferred type is abstract, returning false means that it cannot be converted to an inferred type. It is then converted to LinkedHashMap. This is the main reason for LinkedHashMap cannot cast xxxx class

After modification, it becomes: If the inferred type is abstract and not a container type, return false. This means that although the inferred type is abstract, if it is a container type, and the object in the container is not abstract, it can be converted. In this way, the above-mentioned problems are avoided.

It was also mentioned earlier to solve the problem by increasing the configuration. The solution is relatively simple and rude, and always convert the inferred type.

Solution

At this point, the analysis of the problem is complete, and a brief summary of the solution. There are two main types:

  1. Open the following configuration on the consumer side:
// 始终转换推断类型
converter.setAlwaysConvertToInferredType(true);
  1. Upgraded version: Since GH-1729: Fix JSON Regression merged into 2.2.13.RELEASE. So only need to upgrade spring-amqp to 2.2.13.RELEASE or above. Or upgrade the SpringBoot version to 2.3.7.RELEASE.

end

If you think it is helpful to you, you can comment more and like it a lot, or you can go to my homepage to see, maybe there are articles you like, or you can just follow them, thank you.

I am a different technology house. I make a little progress every day and experience a different life. See you next time!


不一样的科技宅
230 声望460 粉丝

每天进步一点点,体验不一样的生活。