I'm writing a JMS messaging library where I want to have an abstract listener listen for a typed event (or subtype of that event).
public abstract class AbstractTypedEventHandler<E extends TypedEvent<T>, T> implements EventHandler<E> {
@Getter
private final ObjectMapper handlerMapper;
@Getter
private final TypeReference<E> type;
protected AbstractTypedEventHandler(TypeReference<E> eventType) {
this(JsonMapper.builder().configure(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS, false).build(),
eventType);
}
protected AbstractTypedEventHandler(ObjectMapper handlerMapper, TypeReference<E> eventType) {
this.handlerMapper = handlerMapper;
this.type = eventType;
}
@Override
public boolean parseMessage(JsonNode text, Tracer tracer) throws IOException {
E event = handlerMapper.treeToValue(text, getType());
handleEvent(event)
}
public abstract boolean handleEvent(E event);
}
For convenience sake I've written another abstract handler to handle the basic use case of using the basic typed event:
public abstract class TypedEventHandler<T> extends AbstractTypedEventHandler<TypedEvent<T>, T> {
protected TypedEventHandler() {
super(new TypeReference<>() {});
}
protected TypedEventHandler(ObjectMapper mapper) {
super(mapper, new TypeReference<>() {});
}
}
The problem with this is it seems that at runtime when the user inherits the handler: public class PojoEventHandler extends TypedEventHandler<Pojo>
due to generics being thrown away at runtime it correctly deduces that I have a type reference of TypedEvent
but doesn't seem to correctly understand the inner class, instead serializing it to LinkedHashMap
public class PojoEventHandler extends TypedEventHandler<Pojo> {
@Override
public boolean handleEvent(TypedEvent<Pojo> event) {
var pojo = event.getResource(); // throws
}
}
}
Here's the specific error:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class a.b.c.service.Pojo (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; a.b.c.service.Pojo is in unnamed module of loader 'app')
at a.b.c.service.eventhandlers.PojoEventHandler.handleEvent(PojoEventHandler.java:38)
at a.b.c.service.eventhandlers.PojoEventHandler.handleEvent(PojoEventHandler.java:24)
at a.b.c.framework.common.EventHandler.executeHandler(EventHandler.java:57)
at a.b.c.framework.eventhandlers.AbstractTypedEventHandler.parseMessage(AbstractTypedEventHandler.java:83)
at a.b.c.framework.common.jms.JmsMessageHandler.onMessage(JmsMessageHandler.java:79)
...
If you inspect the runtime through a debugger, you can see that Jackson actually correctly deserializes the outer type as TypedEvent
but provides a generic LinkedHashMap
for the inner type.
If I was to ask the user to pass Class<T> class
in the constructor of the handler, is there a way to use the Jackson TypeFactory to to add the inner class information as a praramirized reference at runtime? TypeFactory.createParameterizedType(...)
doesn't seem to accept TypeReference for any of it's overloads, only static Class<T>
for the outer type. Is there some other workaround that would allow me to add the inner type information assuming I design the library to ask the user for the Class<T>
?
When you call new TypeReference<>() {}
, the inferred type is TypeReference<TypedEvent<T>>
. Notice that it doesn't contain Pojo
. The TypeReference
still doesn't have access to Pojo
. When you create a PojoEventHandler
, it's getType().getType()
is TypedEvent<T>
.
The only way to get a TypeReference<TypedEvent<Pojo>>
is by creating the TypeReference
when the concrete type is Pojo
, e.g. by making the Pojo
class instantiate it.
Add the following constructor to TypedEventHandler
(you might as well remove the other one):
protected TypedEventHandler(TypeReference<TypedEvent<T>> eventType) {
super(eventType);
}
Then call this constructor in PojoEventHandler
:
PojoEventHandler() {
super(new TypeReference<>() {});
}
Now, a PojoEventHandler
creates the TypeReference
, the infered type is TypeReference<TypedEvent<Pojo>>
, and getType().getType()
is TypedEvent<Pojo>
.
Unfortunately it requires more work, but that's one of the disadvantages of type erasure. The only way to get the concrete type is by having access to a super class (method return type, parameter type, some others) where the concrete type is already filled in. And in your case, that doesn't happen in the TypedEventHandler
but only in the PojoEventHandler
.