Search code examples
jsonspring-bootspring-jmsconverters

Spring Boot JMS - Generic JSON messages without _type property


I'm implementing JMS in a Spring Boot application. Everything is going well. However I'm very surprised at the tight coupling between JSON messages and Java objects. I am looking for some direction on a more flexible solution.

Going through examples and using the MappingJackson2MessageConverter, everything is great as long as you are sending and receiving in the same application. Under the covers it's extremely tightly coupled to the java object. If I have a simple java object called person:

package acme.receivingapp.dto;

public class Person {
    private String firstName;
    private String lastName;
...
}

When the JmsTemplate turns that into a message the JSON looks generic enough:

{"firstName":"John", "lastName":"Doe"}

However it includes this property:

"_type" : "acme.superapp.dto.Person"

If the JmsListener isn't using that exact Java class, it throws an exception. That's true even if the class is functionally the same as in this example where it's effectively the same class but just in a different package:

package wonderco.sendingapp.dto;

public class Person {
    private String firstName;
    private String lastName;
...
}

We will be receiving messages from many external entities from mainframes, python apps, .Net, etc. I cannot require them to include our object types in a _type property.

I could create my own MessageConverter specifically for a Person object, but if we have hundreds of more messages / java classes it would be unwieldy to have so many message converters. I would need to design something more generic that can work for any type of JSON message / java class.

Before I go down the path of designing my own generic solution is there anything more generic that works like Spring RestControllers and Spring RestTemplates in the sense that the JSON messages aren't so tightly coupled to the very specific Java classes? I feel like I can't possibly be the first person trying to crack this nut.


Solution

  • I think I've got a handle on this. I'll try to explain it to hopefully help the next person who is new to Spring / JMS.

    As M.Deinum points out, unlike a REST endpoint, a queue could potentially contain many different types of messages. Even if your implementation will only have one type of message per queue. Because queues allow any number of different messages that was the design for the provided MappingJackson2MessageConverter. Because the assumption was made there will always be multiple types of messages, there must be a mechanism to determine how to unmarshal the JSON for different types of messages into the correct type of Java Object.

    All the examples you'll find of using a MappingJackson2MessageConverter will have this setup in them:

    MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
     
    converter.setTypeIdPropertyName("_type");
    

    That's telling the message converter to set the object type in a property called _type when creating a message or to read the object type from that property when reading a message. There's no magic in that _type property. It's not a standard. It's just what the Spring folks used in their examples and then a bazillion people cut and pasted it. So for your own messages, you can change that to a more appropriate property name if you like. So in my example, I might call the property acme_receivingapp_message_type if I wanted. I would then tell the external entities sending me messages to include that property with the message type.

    By default, the MappingJackson2MessageConverter will write the object type into whatever property name you chose (_type or whatever) as the fully qualified class name. In my example, it's acme.receivingapp.dto.Person. When a message is received, it looks at the type property to determine what type of Java object to create from the JSON.

    Pretty straightforward so far, but still not very convenient if the people sending me messages are not using Java. Even if I can convince everyone to send me acme.receivingapp.dto.Person, what happens if I refactor that class from Person to Human? Or even just restructure the packages? Now I've got to go back and tell the 1,000 external entities to stop sending the property as acme.receivingapp.dto.Person and now send it as acme.receivingapp.dto.Human?

    Like I stated in my original question, the message and Java class are being very tightly coupled together which doesn't work when you are dealing with external systems/entities.

    The answer to my problem is right in the name of the **Mapping**Jackson2MessageConverter message converter. The key there is the "mapping". Mapping refers to mapping message types to Java classes which is what we want. It's just that, by default, because no mapping information is provided, the MappingJackson2MessageConverter simply uses the fully qualified java class names for creating and receiving messages. All we need to do is provide the mapping information to the message converter so it can map from friendly message-types (e.g.. "Person") to specific classes within our application (e.g. acme.receivingapp.dto.Person).

    If you wanted your external systems/entities that will be sending you messages to simply include the property acme_receivingapp_message_type : Person and you wanted that unmarshalled to an acme.receivingapp.dto.Person object when it's received on your end, you'd setup your message converter like this:

    @Bean
    public MessageConverter jacksonJmsMessageConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("acme_receivingapp_message_type");
    
        // Set up a map to convert our friendly message types to Java classes.
        Map<String, Class<?>> typeIdMap = new HashMap<>();
        typeIdMap.put("Person", acme.receivingapp.dto.Person.class);
        converter.setTypeIdMappings(typeIdMap);
    
        return converter;
    }
    

    That solves the problem of tight coupling between the message type property and Java class names. But what if you'll only be dealing with a single message type in your queue and don't want the people sending messages to have to include any property to indicate the message type? Well MappingJackson2MessageConverter simply doesn't support that. I tried using a "null" key in the map and then leaving the property off the message and unfortunately it doesn't work. I wish it did support that "null" mapping to use when the property wasn't present.

    If you have the scenario where your queue will only deal with one type of message and you don't want the sender to have to include a special property to indicate the message type, you'll likely want to write your own message converter. That convertor will blindly unmarshal the JSON to the one java class you'll always be dealing with. Or maybe you opt to just receive it as a TextMessage and unmarshal it in your listener.

    Hopefully this helps someone because I found it quite confusing initially.