Poor's man domain events
While investigating @EventListeners in spring I’ve noticed very interesting annotation - @DomainEvents. In this post I’m going to check what can be achieved using @DomainEvents annotation and how to effectively use it with old good JPA.
Let’s start with short introduction to domain event idea if you are not yet familiar with it. Now let’s go back to the @DomainEvents annotation in spring-data. Spring-data by default provides support for this annotation and it’s complementary annotation @AfterDomainEventPublication. The idea behind is very simple. In order to broadcast events from your aggregate root all you need to do is to annotate a method with @DomainEvents which will return events which happen to the entity since it was loaded from the persistent store. Spring will register EventPublishingRepositoryProxyPostProcessor which will detect those methods and execute them. Finally, it will pass the results to ApplicationEventPublished#publish.
Basic implementation can be done in few lines of code. What’s more, you can take advantage of AbstractAggregateRoot class already present in spring:
public class AbstractAggregateRoot {
private transient final @Transient List<Object> domainEvents = new ArrayList<Object>();
protected <T> T registerEvent(T event) {
Assert.notNull(event, "Domain event must not be null!");
this.domainEvents.add(event);
return event;
}
@AfterDomainEventPublication
protected void clearDomainEvents() {
this.domainEvents.clear();
}
@DomainEvents
protected Collection<Object> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
@JsonIgnore
@Deprecated
public List<Object> getDomainEvents() {
return (List<Object>) domainEvents();
}
}
The drawback is that it introduces yet another state to your object and/or complicate objects hierarchy, but it might be worth it.
Now everything is great unless you are using JPA. With JPA it is common to use Unit of Work pattern and because of implementation of the invoke method of EventPublishingRepositoryProxyPostProcessor.EventPublishingMethodInterceptor entity events will be broadcasted only when you call the save method which you usually don’t. Most of the times entity already exists in the DB, then it is loaded and it resides in the hibernate session so there is no need to save entity by hand. It will be handled by the AOP and the dirty checking magic.
So what can we do to take advantage of @DomainEvents in JPA? The most obvious (sometimes the safest and good enough) way will be to implement aspect which will get executed around all @Transactional methods. The problem with the aspect is that it knows nothing about transaction template…
In order to make it work we need to go deeper and implement Hibernate interceptor which will handle domain events propagation. First let’s handle @DomainEvents broadcast since we’ll not be able to use current spring implementation 1:1.
If you are too busy to read all the code the responsibility of below class it to find methods annotated with @DomainEvents and @AfterDomainEventPublication and execute them on the entity. Check out the tests to see it in action and how can it be used.
@RequiredArgsConstructor
class DomainEventsHolder {
private static final boolean UNIQUE_ANNOTATION = true;
private final ApplicationEventPublisher eventPublisher;
private final Object entity;
public void publishAndClearEvents() {
getEvents().forEach(eventPublisher::publishEvent);
clearEvents();
}
private Collection<Object> getEvents() {
return domainEventsMethod()
.map(method -> (Collection<Object>) ReflectionUtils.invokeMethod(method, entity))
.orElse(emptyList());
}
private void clearEvents() {
final AnnotationDetectionMethodCallback<AfterDomainEventPublication> methodCallback = new AnnotationDetectionMethodCallback<>(
AfterDomainEventPublication.class,
UNIQUE_ANNOTATION);
ReflectionUtils.doWithMethods(entity.getClass(), methodCallback);
final Method method = methodCallback.getMethod();
if (method != null) {
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, entity);
}
}
public boolean hasDomainEvents() {
return domainEventsMethod().isPresent();
}
private Optional<Method> domainEventsMethod() {
final AnnotationDetectionMethodCallback<DomainEvents> methodCallback = new AnnotationDetectionMethodCallback<>(
DomainEvents.class,
UNIQUE_ANNOTATION);
ReflectionUtils.doWithMethods(entity.getClass(), methodCallback);
return Optional
.ofNullable(methodCallback.getMethod())
.map(method -> {
ReflectionUtils.makeAccessible(method);
return method;
});
}
}
Now all we need to do is to implement interceptor:
@Slf4j
@RequiredArgsConstructor
@Component
class HibernateInterceptor extends EmptyInterceptor {
private static final boolean NOT_PARALLEL = false;
private final ApplicationEventPublisher eventPublisher;
@Override
public void postFlush(Iterator entities) {
final Stream<Object> entitiesStream = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(entities, Spliterator.ORDERED),
NOT_PARALLEL);
entitiesStream
.map(this::createEventHolder)
.filter(DomainEventsHolder::hasDomainEvents)
.forEach(DomainEventsHolder::publishAndClearEvents);
}
private DomainEventsHolder createEventHolder(Object entity) {
return new DomainEventsHolder(eventPublisher, entity);
}
}
And finally register hibernate’s interceptor in the spring context:
class HibernateConfiguration extends HibernateJpaAutoConfiguration {
@Autowired
private HibernateInterceptor hibernateInterceptor;
public HibernateConfiguration(DataSource dataSource, JpaProperties jpaProperties, ObjectProvider<JtaTransactionManager> jtaTransactionManager, ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
super(dataSource, jpaProperties, jtaTransactionManager, transactionManagerCustomizers);
}
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
super.customizeVendorProperties(vendorProperties);
vendorProperties.put("hibernate.ejb.interceptor", hibernateInterceptor);
}
}
To prove the point we can implement simple main method which will log some stuff:
@Component
class DescriptionChangListener {
@EventListener
public void onDescriptionChange(DescriptionUpdated event) {
log.info("Description of {}, modified from {}, to {}",
event.getEntityId(),
event.getOldDescription(),
event.getNewDescription());
}
}
public static void main(String[] args) {
final ConfigurableApplicationContext ctx = SpringApplication.run(DomainEventsApplication.class, args);
final TransactionalService transactionalService = ctx.getBean(TransactionalService.class);
final TxTemplateService txTemplateService = ctx.getBean(TxTemplateService.class);
final EntityPersister entityPersister = ctx.getBean(EntityPersister.class);
final Long entityId = entityPersister.save(new AnyEntity("initial description")).getId();
log.info("Entity {}", entityPersister.load(entityId));
transactionalService.updateEntity(entityId, "transactional description");
log.info("Entity {}", entityPersister.load(entityId));
txTemplateService.updateDescription(entityId, "tx template description");
log.info("Entity {}", entityPersister.load(entityId));
}
When implementing your own solution carefully consider when to handle @DomainEvents, there are other phases you might want to hookup up to. Also be aware that this way some of the events might get lost if one of the operations on the aggregate root fails. using this as a working mule of the event sourcing system might not be the best idea :)
source code can be found on my github
@RequiredArgsConstructor and @Slf4j are from Project Lombok.
Everything else is plain java and spring.
If you've enjoyed or found this post useful you might also like:
16 Oct 2017 #java #spring #howto #hibernate