Search code examples
jacksongsonpojohornetq

HornetQ - how to send and convert POJO


I'm using HornetQ in embedded mode.

I'm trying to send from Publisher to Consumer a POJO object:

public class Pojo implements Serializable {

    private Integer id;
    private String  name;
    private String  phone;

    // constructors, getters & setters
}

My idea is to convert the POJO to a Map and send each properties via ClientMessage. (In this way, the Consumer will be able to filter messages by POJO's properties)

To achieve this, I'm using Jackson ObjectMapper.

Publisher

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> pojoMap = mapper.convertValue(new Pojo(13, "name", "phone"), new TypeReference<Map<String, Object>>() {});
pojoMap.forEach(message::putObjectProperty);
producer.send(message);

Consumer

consumer.setMessageHandler(message -> {
    ObjectMapper mapper = new ObjectMapper();
    mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    Pojo pojo = mapper.convertValue(mapJson, Pojo.class);
});

The problem is that during deserializing (in Consumer) ObjectMapper throws an exception:

java.lang.IllegalArgumentException: Cannot deserialize instance of 'java.lang.String' out of START_OBJECT token
at [Source: UNKNOWN; line: -1, column: -1] (through reference chain: org.hornetq.core.example.Pojo["phone"])
...

From what I understood, ObjectMapper is looking for 'phone' value and wants a String, but it finds an Object and crashes.

How can I solve this problem?

Are there alternatives?

I also tried to use Gson instead of Jackson, but it returns me the same error.

The funny fact is that if you send an object which doesn't have any String parameters, it works without any problems.

(Shouldn't be necessary, but if you want) Here you can find the entire classes of:


Solution

  • This is not really obvious, but Jackson fails while deserializing org.hornetq.api.core.SimpleString that Jackson is not aware of. The same goes to Gson, since SimpleString is not a standard class.

    You have two options here:

    • Either normalize the incoming message payload to standard objects.
    • Or implement a custom SimpleString serializer/deserializer.

    Option #1: Normalization

    By the way, Message.toMap() returns message system properties as well (your code in PasteBin seems to use it), so they can collide with your custom object properties (I don't use HornetQ so I can use wrong terms). I believe, the properties should be normalized like this:

    public static Map<String, Object> getNormalizedPropertiesFrom(final Message message) {
        return message.getPropertyNames()
                .stream()
                .collect(Collectors.toMap(
                        SimpleString::toString,
                        simpleString -> {
                            final Object objectProperty = message.getObjectProperty(simpleString);
                            if ( objectProperty instanceof SimpleString ) {
                                return objectProperty.toString();
                            }
                            return objectProperty;
                        }
                ));
    }
    

    This, unlike Message.toMap(), will discard system properties like durable, address, messageID, expiration, type, priority, and timestamp. So, what you need here is mapper.convertValue(getNormalizedPropertiesFrom(message), Pojo.class).

    Option #2: Custom (de)serialization

    In this case, you can simplify properties extraction

    public static Map<SimpleString, Object> getPropertiesFrom(final Message message) {
        return message.getPropertyNames()
                .stream()
                .collect(Collectors.toMap(Function.identity(), message::getObjectProperty));
    }
    

    But your ObjectMapper instance must be aware of how SimpleString is (de)serialized. Note that ObjectMapper.convertValue() (I believe) uses intermediate objects while converting, it requires a serializer for this scenario (Message -> Map<SimpleString, Object> via custom serialization -> some intermediate representation via built-in deserialization -> Pojo).

    final ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new SimpleModule()
                    .addSerializer(SimpleString.class, new JsonSerializer<SimpleString>() {
                        @Override
                        public void serialize(@Nonnull final SimpleString simpleString, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider)
                                throws IOException {
                            jsonGenerator.writeString(simpleString.toString());
                        }
                    })
            )
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    

    For performance reasons, you should use single ObjectMapper instance.

    Both options will produce:

    Pojo{id=13, name=name, phone=phone}

    for the Pojo class,

    {phone=phone, name=name, id=13}

    for the properties, and something like

    ClientMessage[messageID=8, durable=false, address=q49589558,userID=null,properties=TypedProperties[id=13,phone=phone,name=name]]

    for the client message.