Using ResolvableTypeProvider to Handle Generic Events in Spring
Spring's event listener mechanism is useful for decoupling code. Consider a scenario where you need to synchronize data changes for different entities like Person or Order to an external service after database operations. A straightforward approach couples the synchornization logic directly with the business logic.
boolean success = addPerson(person);
if (success) {
sendToServer(person, "add");
}
Using Spring events can decouple these concerns.
boolean success = addPerson(person);
if (success) {
applicationContext.publishEvent(new PersonEvent(person, "add"));
}
Define the entity and a specific event class.
@Data
public class Person {
private String name;
public Person(String name) { this.name = name; }
}
@Data
public class PersonEvent {
private Person person;
private String action;
public PersonEvent(Person person, String action) {
this.person = person;
this.action = action;
}
}
A controller publishes the event.
@RestController
public class EventPublisher {
@Autowired
private ApplicationContext ctx;
@GetMapping("/publishPerson")
public void publishPersonEvent() {
ctx.publishEvent(new PersonEvent(new Person("Alex"), "add"));
}
}
A listener handles it.
@Component
public class PersonEventHandler {
@EventListener
public void processPersonEvent(PersonEvent event) {
System.out.println("Received: " + event);
}
}
This works. However, for multiple entities like Order, Account, etc., creating a separate event class for each leads to redundancy. A generic event class seems appropriate.
@Data
class GenericEvent<T> {
private T payload;
private String action;
public GenericEvent(T payload, String action) {
this.payload = payload;
this.action = action;
}
}
Publish events using GenericEvent.
ctx.publishEvent(new GenericEvent<>(new Person("Alex"), "add"));
ctx.publishEvent(new GenericEvent<>(new Order("Order123"), "update"));
Attempting to write separate listeners for each type fails due to type erasure.
@EventListener
public void handlePerson(GenericEvent<Person> event) { /* Not invoked */ }
@EventListener
public void handleOrder(GenericEvent<Order> event) { /* Not invoked */ }
Spring cannot distinguish between GenericEvent<Person> and GenericEvent<Order> at runtime because generics are erased. The listeners will not be triggered.
Spring provides a solution via the ResolvableTypeProvider interface. Implement it in the generic event class to provide runtime type information.
@Data
class GenericEvent<T> implements ResolvableTypeProvider {
private T payload;
private String action;
public GenericEvent(T payload, String action) {
this.payload = payload;
this.action = action;
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(
getClass(),
ResolvableType.forInstance(getPayload())
);
}
}
Now, specific listeners work correctly.
@EventListener
public void processPersonEvent(GenericEvent<Person> event) {
System.out.println("Person event: " + event.getPayload().getName());
}
@EventListener
public void processOrderEvent(GenericEvent<Order> event) {
System.out.println("Order event: " + event.getPayload().getOrderName());
}
When an event is published, Spring's AbstractApplicationEventMulticaster retrieves listeners. It uses ResolvableType to resolve the generic type. The ResolvableType.forInstance method checks if the event implements ResolvableTypeProvider. If it does, it calls getResolvableType() to obtain the concrete type, such as GenericEvent<Person>. This allows Spring to match the event to the correct listener method. With out ResolvableTypeProvider, the resolved type would be GenericEvent<?>, preventing listener matching.
The event publishing mechanism is valuable for scenarios like cache invalidation post-data change, notifying downstream systems via message queues, or handling statistics and monitoring asynchronously. It adheres to the single responsibility principle by separating core business logic from secondary concerns.