GH-927 - Introduce ObservedModule.isEventListenerInvocation(MethodInvocation).

Also introduce ObservedModule.getIdentifier() as a replacement for the now deprecated ObservedModule.getName().
This commit is contained in:
Oliver Drotbohm
2024-11-08 17:08:51 +01:00
parent 169cf67128
commit d921c717c6
3 changed files with 181 additions and 35 deletions

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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;
}
};
}
}