From 7fb646b8ec390e57979f45f4df8cb4a5c063a9d5 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Wed, 19 Jun 2024 22:45:41 +0200 Subject: [PATCH 1/5] Introduce interceptor to fire events --- .../quarkus/domain/CustomersService.java | 7 +-- .../shared/interceptors/AnnotationUtils.java | 25 ++++++++ .../shared/interceptors/FireEvent.java | 49 +++++++++++++++ .../interceptors/FireEventInterceptor.java | 62 +++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEvent.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java index b9e68f0..c8e08ef 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java @@ -1,8 +1,8 @@ package de.schulung.sample.quarkus.domain; import de.schulung.sample.quarkus.domain.events.CustomerCreatedEvent; +import de.schulung.sample.quarkus.shared.interceptors.FireEvent; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Event; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; @@ -19,9 +19,6 @@ public class CustomersService { private final Map customers = new HashMap<>(); - // TODO: can we use an interceptor - private final Event eventPublisher; - public Stream getAll() { return this.customers .values() @@ -33,10 +30,10 @@ public Stream getByState(@NotNull Customer.CustomerState state) { .filter(c -> c.getState() == state); } + @FireEvent(CustomerCreatedEvent.class) public void createCustomer(@Valid Customer customer) { customer.setUuid(UUID.randomUUID()); customers.put(customer.getUuid(), customer); - eventPublisher.fire(new CustomerCreatedEvent(customer)); } public Optional getByUuid(@NotNull UUID uuid) { diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java new file mode 100644 index 0000000..9793790 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java @@ -0,0 +1,25 @@ +package de.schulung.sample.quarkus.shared.interceptors; + +import lombok.experimental.UtilityClass; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Optional; + +@UtilityClass +class AnnotationUtils { + Optional findAnnotation(Method method, Class annotationClass) { + Optional result = Optional + .ofNullable(method.getAnnotation(annotationClass)); + // since Java 9, we could simply use Optional#or(...) + if (result.isEmpty()) { + result = findAnnotation(method.getDeclaringClass(), annotationClass); + } + return result; + } + + Optional findAnnotation(Class clazz, Class annotationClass) { + return Optional.ofNullable(clazz.getAnnotation(annotationClass)); + } + +} \ No newline at end of file diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEvent.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEvent.java new file mode 100644 index 0000000..aa1c04e --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEvent.java @@ -0,0 +1,49 @@ +package de.schulung.sample.quarkus.shared.interceptors; + +import jakarta.enterprise.util.Nonbinding; +import jakarta.interceptor.InterceptorBinding; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.lang.annotation.*; + +/** + * Annotate a method to get an event fired after method execution. + */ +@Inherited +@Documented +@InterceptorBinding +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FireEvent { + + /** + * The event class. This class needs a constructor with the same parameters as the method. + * + * @return the event class + */ + @Nonbinding Class value(); + + /** + * Whether the event has to be fired synchronously, asynchronously or both. + * Defaults to both. + * + * @return the mode + */ + @Nonbinding FireMode mode() default FireMode.SYNC_AND_ASYNC; + + @RequiredArgsConstructor + @Getter(AccessLevel.PACKAGE) + enum FireMode { + + ONLY_SYNC(true, false), + ONLY_ASYNC(false, true), + SYNC_AND_ASYNC(true, true); + + private final boolean fireSync; + private final boolean fireAsync; + + } + +} \ No newline at end of file diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java new file mode 100644 index 0000000..a058ab9 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java @@ -0,0 +1,62 @@ +package de.schulung.sample.quarkus.shared.interceptors; + +import jakarta.annotation.Priority; +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import lombok.SneakyThrows; + +import java.util.Optional; + +@Priority(5) // do this before validation to allow publishing events on exception too +@Interceptor +@FireEvent(Object.class) +public class FireEventInterceptor { + + @Inject + Event eventPublisher; + + @SneakyThrows + private static T createEventObject(InvocationContext invocation, Class eventType) { + return eventType + .getConstructor(invocation.getMethod().getParameterTypes()) + .newInstance(invocation.getParameters()); + } + + @AroundInvoke + public Object fireEvent(InvocationContext invocation) throws Exception { + final Optional annotation = AnnotationUtils + .findAnnotation(invocation.getMethod(), FireEvent.class); + @SuppressWarnings("unchecked") final Optional> eventType = AnnotationUtils + .findAnnotation(invocation.getMethod(), FireEvent.class) + .map((FireEvent publishEvent) -> (Class) publishEvent.value()); + final FireEvent.FireMode mode = annotation + .map(FireEvent::mode) + .orElse(FireEvent.FireMode.SYNC_AND_ASYNC); + final Optional event = eventType + .map(clazz -> createEventObject(invocation, clazz)); + // if something is wrong until here, we do not invoke the service's create-method + // now, we invoke the service + final Object result = invocation.proceed(); + // if an exception occured, the event is not fired + // now, we fire the event + event.ifPresent( + e -> eventType + .map(eventPublisher::select) + .ifPresent(publisher -> { + // fire synchronous events + if (mode.isFireSync()) { + publisher.fire(e); + } + // if no error occured, fire asynchronous events + if (mode.isFireAsync()) { + publisher.fireAsync(e); + } + }) + ); + // and we need to return the service's result to the invoker (the controller) + return result; + } +} From c3c73cbe0d894637bac8c79578376f6c5054b09f Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 10:07:04 +0200 Subject: [PATCH 2/5] Introduce interceptor. --- .../quarkus/domain/CustomersService.java | 2 ++ .../shared/interceptors/LogPerformance.java | 13 +++++++++ .../LogPerformanceInterceptor.java | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java index b9e68f0..84e86d1 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java @@ -1,6 +1,7 @@ package de.schulung.sample.quarkus.domain; import de.schulung.sample.quarkus.domain.events.CustomerCreatedEvent; +import de.schulung.sample.quarkus.shared.interceptors.LogPerformance; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Event; import jakarta.validation.Valid; @@ -33,6 +34,7 @@ public Stream getByState(@NotNull Customer.CustomerState state) { .filter(c -> c.getState() == state); } + @LogPerformance public void createCustomer(@Valid Customer customer) { customer.setUuid(UUID.randomUUID()); customers.put(customer.getUuid(), customer); diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java new file mode 100644 index 0000000..f2c4125 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java @@ -0,0 +1,13 @@ +package de.schulung.sample.quarkus.shared.interceptors; + +import jakarta.interceptor.InterceptorBinding; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@InterceptorBinding +public @interface LogPerformance { +} diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java new file mode 100644 index 0000000..f7d1877 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java @@ -0,0 +1,29 @@ +package de.schulung.sample.quarkus.shared.interceptors; + +import io.quarkus.arc.log.LoggerName; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import org.jboss.logging.Logger; + +@Interceptor +@LogPerformance +public class LogPerformanceInterceptor { + + @LoggerName("perf") + Logger log; + + @AroundInvoke + public Object logPerformance(InvocationContext ic) throws Exception { + var methodName = ic.getMethod().getName(); + var ts1 = System.currentTimeMillis(); + try { + return ic.proceed(); + } finally { + var ts2 = System.currentTimeMillis(); + log.info("Methode " + methodName + ": " + (ts2-ts1) + "ms"); + } + } + +} From a4efc9b455478149ace52f63f653e511888b3f29 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 10:11:25 +0200 Subject: [PATCH 3/5] Introduce configurable logging level. --- .../de/schulung/sample/quarkus/domain/CustomersService.java | 3 ++- .../sample/quarkus/shared/interceptors/LogPerformance.java | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java index 84e86d1..aaf93ab 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java @@ -7,6 +7,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.jboss.logging.Logger; import java.util.HashMap; import java.util.Map; @@ -34,7 +35,7 @@ public Stream getByState(@NotNull Customer.CustomerState state) { .filter(c -> c.getState() == state); } - @LogPerformance + @LogPerformance(Logger.Level.WARN) public void createCustomer(@Valid Customer customer) { customer.setUuid(UUID.randomUUID()); customers.put(customer.getUuid(), customer); diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java index f2c4125..0006032 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformance.java @@ -1,6 +1,8 @@ package de.schulung.sample.quarkus.shared.interceptors; +import jakarta.enterprise.util.Nonbinding; import jakarta.interceptor.InterceptorBinding; +import org.jboss.logging.Logger; import java.lang.annotation.*; @@ -10,4 +12,8 @@ @Inherited @InterceptorBinding public @interface LogPerformance { + + @Nonbinding + Logger.Level value() default Logger.Level.INFO; + } From b85ccb1663f64e8cdfb7baa4d9aa3d4c79ffffa5 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 10:36:46 +0200 Subject: [PATCH 4/5] Introduce interceptor to fire events. --- .../sample/quarkus/domain/CustomersService.java | 2 +- .../shared/interceptors/AnnotationUtils.java | 13 +++++-------- .../shared/interceptors/FireEventInterceptor.java | 2 +- .../interceptors/LogPerformanceInterceptor.java | 15 ++++++++++++--- .../src/main/resources/application.properties | 5 +++++ 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java index d30d5f8..3355393 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/domain/CustomersService.java @@ -32,7 +32,7 @@ public Stream getByState(@NotNull Customer.CustomerState state) { .filter(c -> c.getState() == state); } - @LogPerformance(Logger.Level.WARN) + @LogPerformance(Logger.Level.DEBUG) @FireEvent(CustomerCreatedEvent.class) public void createCustomer(@Valid Customer customer) { customer.setUuid(UUID.randomUUID()); diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java index 9793790..e6798cc 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/AnnotationUtils.java @@ -9,17 +9,14 @@ @UtilityClass class AnnotationUtils { Optional findAnnotation(Method method, Class annotationClass) { - Optional result = Optional - .ofNullable(method.getAnnotation(annotationClass)); - // since Java 9, we could simply use Optional#or(...) - if (result.isEmpty()) { - result = findAnnotation(method.getDeclaringClass(), annotationClass); - } - return result; + return Optional + .ofNullable(method.getAnnotation(annotationClass)) + .or(() -> findAnnotation(method.getDeclaringClass(), annotationClass)); } Optional findAnnotation(Class clazz, Class annotationClass) { - return Optional.ofNullable(clazz.getAnnotation(annotationClass)); + return Optional + .ofNullable(clazz.getAnnotation(annotationClass)); } } \ No newline at end of file diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java index a058ab9..2888cec 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/FireEventInterceptor.java @@ -10,7 +10,7 @@ import java.util.Optional; -@Priority(5) // do this before validation to allow publishing events on exception too +@Priority(5) @Interceptor @FireEvent(Object.class) public class FireEventInterceptor { diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java index f7d1877..c8f1cb8 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/shared/interceptors/LogPerformanceInterceptor.java @@ -1,7 +1,7 @@ package de.schulung.sample.quarkus.shared.interceptors; import io.quarkus.arc.log.LoggerName; -import jakarta.inject.Inject; +import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; @@ -9,20 +9,29 @@ @Interceptor @LogPerformance +@Priority(10) public class LogPerformanceInterceptor { - @LoggerName("perf") + @LoggerName("performance") Logger log; + private Logger.Level findLevel(InvocationContext ic) { + return AnnotationUtils + .findAnnotation(ic.getMethod(), LogPerformance.class) + .map(LogPerformance::value) + .orElse(Logger.Level.INFO); + } + @AroundInvoke public Object logPerformance(InvocationContext ic) throws Exception { var methodName = ic.getMethod().getName(); + var level = findLevel(ic); var ts1 = System.currentTimeMillis(); try { return ic.proceed(); } finally { var ts2 = System.currentTimeMillis(); - log.info("Methode " + methodName + ": " + (ts2-ts1) + "ms"); + log.log(level, "Methode " + methodName + ": " + (ts2 - ts1) + "ms"); } } diff --git a/customer-api-provider/src/main/resources/application.properties b/customer-api-provider/src/main/resources/application.properties index e69de29..ed9d6f8 100644 --- a/customer-api-provider/src/main/resources/application.properties +++ b/customer-api-provider/src/main/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.log.category."performance".level=DEBUG +quarkus.log.min-level=DEBUG +quarkus.log.console.enable=true +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n +quarkus.log.console.level=DEBUG \ No newline at end of file From ad5aa8484724302854e933b7f39fbb52d30b6f2c Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 10:59:05 +0200 Subject: [PATCH 5/5] Remove unnecessary mock --- .../sample/quarkus/domain/CustomersServiceTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/domain/CustomersServiceTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/domain/CustomersServiceTests.java index 7853ea5..0609e88 100644 --- a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/domain/CustomersServiceTests.java +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/domain/CustomersServiceTests.java @@ -1,11 +1,8 @@ package de.schulung.sample.quarkus.domain; -import de.schulung.sample.quarkus.domain.events.CustomerCreatedEvent; -import jakarta.enterprise.event.Event; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; @@ -16,8 +13,9 @@ @ExtendWith(MockitoExtension.class) class CustomersServiceTests { - @Mock - Event event; + // nicht mehr notwendig wegen @FireEvent interceptor + //@Mock + //Event event; @InjectMocks CustomersService service;