GH-206 - Event Publication Registry now uses custom Clock instance if configured.

We now consider a user defined Clock bean in the application context to obtain the Instant to use as publication date for event publications.
This commit is contained in:
Oliver Drotbohm
2023-07-19 17:16:20 +02:00
parent 77bfc48ebe
commit 303a6802d8
7 changed files with 122 additions and 13 deletions

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.events;
import java.time.Clock;
import java.time.Instant;
import java.util.Optional;
@@ -49,13 +50,28 @@ public interface CompletableEventPublication extends EventPublication {
CompletableEventPublication markCompleted();
/**
* Creates a {@link CompletableEventPublication} for the given event an listener identifier.
* Creates a {@link CompletableEventPublication} for the given event an listener identifier using a default
* {@link Instant}. Prefer using {@link #of(Object, PublicationTargetIdentifier, Instant)} with a dedicated
* {@link Instant} obtained from a {@link Clock}.
*
* @param event must not be {@literal null}.
* @param id must not be {@literal null}.
* @return
* @return will never be {@literal null}.
* @see #of(Object, PublicationTargetIdentifier, Instant)
*/
static CompletableEventPublication of(Object event, PublicationTargetIdentifier id) {
return new DefaultEventPublication(event, id);
return new DefaultEventPublication(event, id, Instant.now());
}
/**
* Creates a {@link CompletableEventPublication} for the given event an listener identifier and publication date.
*
* @param event must not be {@literal null}.
* @param id must not be {@literal null}.
* @param publicationDate must not be {@literal null}.
* @return will never be {@literal null}.
*/
static CompletableEventPublication of(Object event, PublicationTargetIdentifier id, Instant publicationDate) {
return new DefaultEventPublication(event, id, publicationDate);
}
}

View File

@@ -39,15 +39,17 @@ class DefaultEventPublication implements CompletableEventPublication {
*
* @param event must not be {@literal null}.
* @param targetIdentifier must not be {@literal null}.
* @param publicationDate must not be {@literal null}.
*/
DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier) {
DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier, Instant publicationDate) {
Assert.notNull(event, "Event must not be null!");
Assert.notNull(targetIdentifier, "PublicationTargetIdentifier must not be null!");
Assert.notNull(publicationDate, "Publication date must not be null!");
this.event = event;
this.targetIdentifier = targetIdentifier;
this.publicationDate = Instant.now();
this.publicationDate = publicationDate;
this.completionDate = Optional.empty();
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.events;
import java.time.Clock;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
@@ -40,17 +41,21 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultEventPublicationRegistry.class);
private final EventPublicationRepository events;
private final Clock clock;
/**
* Creates a new {@link DefaultEventPublicationRegistry} for the given {@link EventPublicationRepository}.
*
* @param events must not be {@literal null}.
* @param clock must not be {@literal null}.
*/
public DefaultEventPublicationRegistry(EventPublicationRepository events) {
public DefaultEventPublicationRegistry(EventPublicationRepository events, Clock clock) {
Assert.notNull(events, "EventPublicationRepository must not be null!");
Assert.notNull(clock, "Clock must not be null!");
this.events = events;
this.clock = clock;
}
/*
@@ -58,10 +63,11 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
* @see org.springframework.modulith.events.EventPublicationRegistry#store(java.lang.Object, java.util.stream.Stream)
*/
@Override
public void store(Object event, Stream<PublicationTargetIdentifier> listeners) {
public Collection<EventPublication> store(Object event, Stream<PublicationTargetIdentifier> listeners) {
listeners.map(it -> map(event, it))
.forEach(events::create);
return listeners.map(it -> map(event, it))
.map(events::create)
.toList();
}
/*
@@ -118,7 +124,7 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub
private EventPublication map(Object event, PublicationTargetIdentifier targetIdentifier) {
EventPublication result = CompletableEventPublication.of(event, targetIdentifier);
var result = CompletableEventPublication.of(event, targetIdentifier, clock.instant());
LOGGER.debug("Registering publication of {} for {}.", //
result.getEvent().getClass().getName(), result.getTargetIdentifier().getValue());

View File

@@ -36,7 +36,7 @@ public interface EventPublicationRegistry {
* @param event must not be {@literal null}.
* @param listeners must not be {@literal null}.
*/
void store(Object event, Stream<PublicationTargetIdentifier> listeners);
Collection<EventPublication> store(Object event, Stream<PublicationTargetIdentifier> listeners);
/**
* Returns all {@link EventPublication}s that have not been completed yet.

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.events.config;
import java.time.Clock;
import java.time.Duration;
import java.util.Arrays;
@@ -22,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -55,8 +57,9 @@ class EventPublicationConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository) {
return new DefaultEventPublicationRegistry(repository);
EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository,
ObjectProvider<Clock> clock) {
return new DefaultEventPublicationRegistry(repository, clock.getIfAvailable(() -> Clock.systemUTC()));
}
@Bean

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.events;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.AdditionalAnswers.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Unit tests for {@link DefaultEventPublicationRegistry}.
*
* @author Oliver Drotbohm
*/
@ExtendWith(MockitoExtension.class)
class DefaultEventPublicationRegistryUnitTests {
@Mock EventPublicationRepository repository;
@Mock Clock clock;
@Test // GH-206
void usesCustomClockIfConfigured() {
when(repository.create(any())).then(returnsFirstArg());
var now = Instant.now();
var clock = Clock.fixed(now, ZoneId.systemDefault());
var registry = new DefaultEventPublicationRegistry(repository, clock);
var identifier = PublicationTargetIdentifier.of("id");
var publications = registry.store(new Object(), Stream.of(identifier));
assertThat(publications).hasSize(1).element(0).satisfies(it -> {
assertThat(it.getPublicationDate()).isEqualTo(now);
assertThat(it.getTargetIdentifier()).isEqualTo(identifier);
});
}
}

View File

@@ -17,7 +17,10 @@ package org.springframework.modulith.events.config;
import static org.assertj.core.api.Assertions.*;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
@@ -32,11 +35,13 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContex
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.events.EventPublicationRepository;
import org.springframework.modulith.events.config.EventPublicationConfiguration.AsyncPropertiesDefaulter;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.ProxyAsyncConfiguration;
import org.springframework.scheduling.aspectj.AspectJAsyncConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Unit tests for {@link EventPublicationConfiguration}.
@@ -105,6 +110,21 @@ class EventPublicationConfigurationIntegrationTests {
});
}
@Test // GH-206
void wiresCustomClockIntoEventPublicationRegistryIfConfigured() {
var clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
basicSetup()
.withBean(Clock.class, () -> clock)
.run(context -> {
var registry = context.getBean(EventPublicationRegistry.class);
assertThat(ReflectionTestUtils.getField(registry, "clock")).isSameAs(clock);
});
}
private <T> ContextConsumer<AssertableApplicationContext> expect(Function<Shutdown, T> extractor,
T expected) {