Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interceptors #18

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package de.samples.schulung.quarkus.domain;

import de.samples.schulung.quarkus.domain.Customer.CustomerState;
import de.samples.schulung.quarkus.shared.FireEvent;
import de.samples.schulung.quarkus.shared.LogPerformance;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
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;
Expand All @@ -14,13 +15,10 @@
import java.util.stream.Stream;

@ApplicationScoped
@RequiredArgsConstructor
public class CustomersService {

private final Map<UUID, Customer> customers = new HashMap<>();

private final Event<CustomerCreatedEvent> eventPublisher;

public long getCount() {
return customers.size();
}
Expand All @@ -40,10 +38,11 @@ public Optional<Customer> findCustomerByUuid(@NotNull UUID uuid) {
return Optional.ofNullable(customers.get(uuid));
}

@LogPerformance(Logger.Level.DEBUG)
@FireEvent(CustomerCreatedEvent.class)
public void createCustomer(@Valid @NotNull Customer customer) {
customer.setUuid(UUID.randomUUID());
customers.put(customer.getUuid(), customer);
eventPublisher.fireAsync(new CustomerCreatedEvent(customer));
}

public void updateCustomer(@Valid @NotNull Customer customer) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.samples.schulung.quarkus.shared;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Optional;

class AnnotationUtils {

private AnnotationUtils() {
}

static <A extends Annotation> Optional<A> findAnnotation(Method method, Class<A> annotationClass) {
return Optional
.ofNullable(method.getAnnotation(annotationClass))
.or(() -> findAnnotation(method.getDeclaringClass(), annotationClass));
}

static <A extends Annotation> Optional<A> findAnnotation(Class<?> clazz, Class<A> annotationClass) {
return Optional
.ofNullable(clazz.getAnnotation(annotationClass));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package de.samples.schulung.quarkus.shared;

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;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package de.samples.schulung.quarkus.shared;

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)
@Interceptor
@FireEvent(Object.class)
public class FireEventInterceptor {

@Inject
Event<Object> eventPublisher;

@SneakyThrows
private static <T> T createEventObject(InvocationContext invocation, Class<T> eventType) {
return eventType
.getConstructor(invocation.getMethod().getParameterTypes())
.newInstance(invocation.getParameters());
}

@AroundInvoke
public Object fireEvent(InvocationContext invocation) throws Exception {
final Optional<FireEvent> annotation = AnnotationUtils
.findAnnotation(invocation.getMethod(), FireEvent.class);
@SuppressWarnings("unchecked") final Optional<Class<Object>> eventType = AnnotationUtils
.findAnnotation(invocation.getMethod(), FireEvent.class)
.map((FireEvent publishEvent) -> (Class<Object>) publishEvent.value());
final FireEvent.FireMode mode = annotation
.map(FireEvent::mode)
.orElse(FireEvent.FireMode.SYNC_AND_ASYNC);
final Optional<Object> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.samples.schulung.quarkus.shared;

import jakarta.enterprise.util.Nonbinding;
import jakarta.interceptor.InterceptorBinding;
import org.jboss.logging.Logger;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.TYPE
})
@Documented
@Inherited
@InterceptorBinding
public @interface LogPerformance {

@Nonbinding
Logger.Level value() default Logger.Level.INFO;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.samples.schulung.quarkus.shared;

import io.quarkus.arc.log.LoggerName;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import org.jboss.logging.Logger;

@Interceptor
@LogPerformance
@Priority(10)
public class LogPerformanceInterceptor {

@LoggerName("performance")
Logger logger;

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 invocationContext) throws Exception {
final var methodName = invocationContext.getMethod().getName();
final var level = findLevel(invocationContext);
var ts1 = System.currentTimeMillis();
try {
return invocationContext.proceed(); // Weiterleitung an das Original
} finally {
var ts2 = System.currentTimeMillis();
logger.logf(
level,
"Dauer der Methode '%s': %d ms",
new Object[]{methodName, ts2 - ts1}
);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,17 @@

import de.samples.schulung.quarkus.domain.Customer;
import de.samples.schulung.quarkus.domain.CustomersService;
import de.samples.schulung.quarkus.utilities.ProfileWithMockedLogger;
import io.quarkus.arc.log.LoggerName;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.spi.InjectionPoint;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.time.LocalDate;
import java.time.Month;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

Expand All @@ -28,6 +21,7 @@
import static org.mockito.Mockito.*;

@QuarkusTest
@TestProfile(ProfileWithMockedLogger.class)
class CustomersLoggingTests {

/*
Expand Down Expand Up @@ -76,38 +70,4 @@ void shouldLogWhenCustomerCreated() throws InterruptedException {

}

// just for this class, not for all tests!
@ApplicationScoped
static class LoggerMocksProducer {
private final Map<String, Logger> loggers = new HashMap<>();

private Optional<LoggerName> findLoggerName(InjectionPoint injectionPoint) {
return injectionPoint.getQualifiers()
.stream()
.filter(q -> q.annotationType().equals(LoggerName.class))
.findFirst()
.map(LoggerName.class::cast);
}

private Logger createLogger(String name) {
return Mockito.mock(Logger.class);
}

@Produces
@Dependent
@LoggerName("")
Logger getMockedLogger(InjectionPoint injectionPoint) {
return findLoggerName(injectionPoint)
.map(LoggerName::value)
.map(name -> loggers.computeIfAbsent(name, this::createLogger))
.orElseThrow(() -> new IllegalStateException("Unable to derive the logger name at " + injectionPoint));
}

@PreDestroy
void clear() {
loggers.clear();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.samples.schulung.quarkus.shared;

import de.samples.schulung.quarkus.utilities.ProfileWithMockedLogger;
import io.quarkus.arc.log.LoggerName;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verifyNoInteractions;

@QuarkusTest
@TestProfile(ProfileWithMockedLogger.class)
public class LogPerformanceInterceptorTest {

@Inject
SampleBean sampleBean;
@LoggerName("performance")
Logger logger;

@BeforeEach
void setup() {
Mockito.reset(logger);
}

@Test
@DisplayName("[SHARED] No @LogPerformance -> no logging")
void given_whenNoAnnotation_thenDoNotLog() {
sampleBean.dontLog();
verifyNoInteractions(logger);
}

@Test
@DisplayName("[SHARED] @LogPerformance -> default level logging")
void given_whenAnnotation_thenLogDefaultLevel() {
sampleBean.logDefaultLevel();
Mockito.verify(logger).logf(
eq(Logger.Level.INFO),
anyString(),
any(Object[].class)
);
}

@Test
@DisplayName("[SHARED] @LogPerformance(DEBUG) -> debug logging")
void given_whenAnnotationWithDebugLevel_thenLogDebugLevel() {
sampleBean.logDebugLevel();
Mockito.verify(logger).logf(
eq(Logger.Level.DEBUG),
anyString(),
any(Object[].class)
);
}


// Initialized for all tests, but does not disturb
@ApplicationScoped
static class SampleBean {

void dontLog() {
}

@LogPerformance
void logDefaultLevel() {
}

@LogPerformance(Logger.Level.DEBUG)
void logDebugLevel() {
}

}

}
Loading
Loading