diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java index 776a9b5e..5a54e29a 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java @@ -21,13 +21,17 @@ import java.util.Arrays; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.ProxyMethodInvocation; import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModuleIdentifier; import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.ArchitecturallyEvidentType.ReferenceMethod; import org.springframework.modulith.core.FormattableType; import org.springframework.modulith.core.SpringBean; import org.springframework.util.Assert; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaMethod; class DefaultObservedModule implements ObservedModule { @@ -51,7 +55,16 @@ class DefaultObservedModule implements ObservedModule { */ @Override public String getName() { - return module.getName(); + return getIdentifier().toString(); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.observability.ObservedModule#getIdentifier() + */ + @Override + public ApplicationModuleIdentifier getIdentifier() { + return module.getIdentifier(); } /* @@ -69,37 +82,7 @@ class DefaultObservedModule implements ObservedModule { */ @Override public String getInvokedMethod(MethodInvocation invocation) { - - Method method = invocation.getMethod(); - - if (module.contains(method.getDeclaringClass())) { - return toString(invocation.getMethod(), module); - } - - if (!ProxyMethodInvocation.class.isInstance(invocation)) { - return toString(invocation.getMethod(), module); - } - - // For class-based proxies, use the target class - - var advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy(); - var targetClass = advised.getTargetClass(); - - if (module.contains(targetClass)) { - return toString(targetClass, method, module); - } - - // For JDK proxies, find original interface the method was logically declared on - - for (Class type : advised.getProxiedInterfaces()) { - if (module.contains(type)) { - if (Arrays.asList(type.getMethods()).contains(method)) { - return toString(type, method, module); - } - } - } - - return toString(invocation.getMethod(), module); + return toString(findModuleLocalMethod(invocation), module); } /* @@ -144,11 +127,61 @@ class DefaultObservedModule implements ObservedModule { .orElse(null); } - private static String toString(Method method, ApplicationModule module) { - return toString(method.getDeclaringClass(), method, module); + /* + * (non-Javadoc) + * @see org.springframework.modulith.observability.ObservedModule#isEventListenerInvocation(org.aopalliance.intercept.MethodInvocation) + */ + @Override + public boolean isEventListenerInvocation(MethodInvocation invocation) { + + var method = findModuleLocalMethod(invocation); + var type = module.getArchitecturallyEvidentType(method.getDeclaringClass()); + + return type.isEventListener() + && type.getReferenceMethods() + .map(ReferenceMethod::getMethod) + .map(JavaMethod::reflect) + .anyMatch(method::equals); } - private static String toString(Class type, Method method, ApplicationModule module) { + private Method findModuleLocalMethod(MethodInvocation invocation) { + + Method method = invocation.getMethod(); + + if (module.contains(method.getDeclaringClass())) { + return invocation.getMethod(); + } + + if (!ProxyMethodInvocation.class.isInstance(invocation)) { + return invocation.getMethod(); + } + + // For class-based proxies, use the target class + + var advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy(); + var targetClass = advised.getTargetClass(); + + if (module.contains(targetClass)) { + + return AopUtils.getMostSpecificMethod(method, targetClass); + } + + // For JDK proxies, find original interface the method was logically declared on + + for (Class type : advised.getProxiedInterfaces()) { + if (module.contains(type)) { + if (Arrays.asList(type.getMethods()).contains(method)) { + return AopUtils.getMostSpecificMethod(method, targetClass); + } + } + } + + return invocation.getMethod(); + } + + private static String toString(Method method, ApplicationModule module) { + + var type = method.getDeclaringClass(); var typeName = module.getType(type.getName()) .map(FormattableType::of) diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModule.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModule.java index 345c38a5..763c0d8a 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModule.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModule.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; import org.springframework.lang.Nullable; import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModuleIdentifier; import org.springframework.modulith.core.ApplicationModules; import com.tngtech.archunit.core.domain.JavaClass; @@ -29,8 +30,27 @@ import com.tngtech.archunit.core.domain.JavaClass; */ interface ObservedModule { + /** + * Returns the name of the application module. + * + * @return will never be {@literal null}. + * @deprecated since 1.3, use {@link #getIdentifier()} instead. + */ + @Deprecated(forRemoval = true) String getName(); + /** + * Returns the {@link ApplicationModuleIdentifier} of the underlying module. + * + * @return will never be {@literal null}. + */ + ApplicationModuleIdentifier getIdentifier(); + + /** + * Returns the human-readable name of the module. + * + * @return will never be {@literal null}. + */ String getDisplayName(); /** @@ -60,4 +80,13 @@ interface ObservedModule { */ @Nullable ObservedModuleType getObservedModuleType(Class type, ApplicationModules modules); + + /** + * Returns whether the given {@link MethodInvocation} is the invocation of an event listener as opposed to a standard + * method invocation on a Spring bean. + * + * @param invocation must not be {@literal null}. + * @since 1.3 + */ + boolean isEventListenerInvocation(MethodInvocation invocation); } diff --git a/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/DefaultObservedModuleUnitTests.java b/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/DefaultObservedModuleUnitTests.java new file mode 100644 index 00000000..f4ac3927 --- /dev/null +++ b/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/DefaultObservedModuleUnitTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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.observability; + +import static org.assertj.core.api.Assertions.*; + +import example.sample.ObservedComponent; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; +import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.ArchitecturallyEvidentType; +import org.springframework.modulith.test.TestApplicationModules; + +/** + * Unit tests for {@link DefaultObservedModule}. + * + * @author Oliver Drotbohm + */ +class DefaultObservedModuleUnitTests { + + static final ApplicationModules modules = TestApplicationModules.of("example"); + + ApplicationModule module = modules.getModuleByName("sample").orElseThrow(); + ArchitecturallyEvidentType type = module.getArchitecturallyEvidentType(ObservedComponent.class); + ObservedModule observedModule = new DefaultObservedModule(module); + + @Test // GH-927 + void detectsEventListenerInvocation() throws Exception { + + assertThat(observedModule.isEventListenerInvocation(forMethod("on", Object.class))).isTrue(); + assertThat(observedModule.isEventListenerInvocation(forMethod("someMethod"))).isFalse(); + } + + private static MethodInvocation forMethod(String name, Class... parameterTypes) throws Exception { + + var method = ObservedComponent.class.getDeclaredMethod(name, parameterTypes); + + return new MethodInvocation() { + + @Override + public Object proceed() throws Throwable { + return null; + } + + @Override + public Object getThis() { + return null; + } + + @Override + public AccessibleObject getStaticPart() { + return method; + } + + @Override + public Object[] getArguments() { + return new Object[] {}; + } + + @Override + public Method getMethod() { + return method; + } + }; + } +}