preface

Recently we have received a request to synchronize user data to the XX subsystem via RabbitMq, providing both single synchronization and batch synchronization. It’s not so simple. The code will look like this:

public void syncUserSingle(User user) {
    // Omit a lot of business code
    rabbitTemplate.convertAndSend("q_sync_user_single", user);
}

public void syncUserBatch(List<User> userList) {
    // Omit a lot of business code
    rabbitTemplate.convertAndSend("q_sync_user_batch", userList);
}
Copy the code

However, in the process of joint investigation, a more bizarre problem was encountered. When a single user synchronizes, the subsystem can consume normally. Then when batch synchronization was performed, the subsystem reported an error. And throw the Java. Lang. ClassCastException prompt LinkedHashMap always XXXX class. Then responsible for the subsystem of the buddy smile xi xi (surface smile xi xi) came to me and said, is not agreed List why send a Map over?

When I saw this mistake, I really lost my head. Why can a single object work, but not a List? I sent a List of data, why changed to Map? Although a lot of questions, but can only smile happily say, I check ha.

Problem reproduction

  • Project depend on

      
<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>
Copy the code

The sender

  • Initializing the queue
@Configuration
public class QueueConfig {
    @Bean
    public Queue test(a) {
        return new Queue("test"); }}Copy the code
  • Configuration RabbitTemplete
@Configuration
public class RabbitTemplateConfig {
    @Autowired
    public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
        // Set the Json message converter
        rabbitTemplate.setMessageConverter(newJackson2JsonMessageConverter()); }}Copy the code
  • Send the interface
@Controller
@RequestMapping("/test")
public class TestController {

    @Resource
    private RabbitTemplate template;

    @GetMapping("/send")
    public void send(a) {
        template.convertAndSend("test", Collections.singletonList(new User(20."A different kind of tech geek."))); }}Copy the code
  • The User class
@Data
@AllArgsConstructor
public class User {
    /** * age */
    private Integer age;

    /** * name */
    private String name;
}
Copy the code

The receiving party

  • Listening to the configuration
@Configuration
public class RabbitListenerConfig {

    @Bean
    public SimpleRabbitListenerContainerFactory customFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        // Set the message converter
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        configurer.configure(factory, connectionFactory);
        returnfactory; }}Copy the code
  • The receiving party
@Service
public class UserService {

    public void save(List<User> userList) { userList.forEach(System.out::println); }}Copy the code
@Component
public class Receiver {

    @Resource
    private UserService userService;

    @RabbitListener(queues = "test", containerFactory = "customFactory")
    public void receive(@Payload List<User> msg) { userService.save(msg); }}Copy the code

The error log

The good guy really failed, this 100 percent must appear bug ah.

Analyze the cause of the problem

First of all, the error message is thrown out at the consumer end, which should be a higher probability of problems. But what if, as he says, I send the wrong message on the production side, causing problems on the consumer side? To answer this QUESTION, I disconnect the consumer and send a message through the Rabbitmq console to see if the message is correct.

The message content is as follows:

Payload is a standard JSON string, and TypeId is also a List, not a LinkedHashMap. Hahaha, this can be a consumer side deserialization problem. Get the pot out of here and smoke it. There’s no way my code is buggy.

I love learning, I certainly do not want to so. We have to get to the bottom of it and teach him a lesson. So I googled around and found out it was this bug. A dude also found it and submitted an issue: Spring-AMPQ /issues/1279.

Basically: Try upgrading from Spring Boot 2.3.1 to 2.3.3 and then to 2.3.6. The error message is still: List

foos is LikedHashMap, not Foo. This was confirmed through remote debugging. For some reason, he didn’t think generic types were being used correctly. Revert to Spring-AMQp 2.2.7 to make it work again, and the object is indeed Foo.

Then garyrussell this guy said: they added support for deserialization of abstract classes, which can have some side effects on message converters if not configured correctly. Then it looked into it and confirmed it was a mistake. Because the List is abstract, the new code assumes that it cannot be deserialized.

The solution:

converter.setAlwaysConvertToInferredType(true);
Copy the code

We will also Fix this problem in GH-1729: Fix JSON Regression with the following code:

Reading the code, the logic before the change is that if the inferred type is abstract, returning false means that the inferred type cannot be converted. It is then converted to LinkedHashMap. This is the main reason why LinkedHashMap cannot cast XXXX class appears.

If the inferred type is abstract and not a container type, return false. This means that although inferred types are abstract, they can be converted if they are container types and the objects inside the container are not abstract. This avoids the above problems.

The solution was also mentioned earlier by adding configuration. The solution is relatively straightforward, always converting inferred types.

The solution

To the end of this problem analysis, a simple summary of the solution. There are two main types:

  1. Enable the following configuration on the consumer side:
// Always convert inferred types
converter.setAlwaysConvertToInferredType(true);
Copy the code
  1. Updated version: Due to GH-1729: Fix JSON Regression merged to 2.2.13.RELEASE. So just upgrade spring-AMQP to 2.2.13.release or above. Or upgrade the SpringBoot version to 2.3.7.release.

At the end

If you feel helpful to you, you can comment more, more like oh, you can also go to my home page to have a look, maybe there is an article you like, you can also click a concern oh, thank you.

I am a different kind of tech nerd, making progress every day and experiencing a different life. See you next time!