GH-746 - Fix observability interception for components declaring event listeners.

This commit is contained in:
Oliver Drotbohm
2024-07-29 23:43:53 +02:00
parent 7ebee3c838
commit 73050ed23a
4 changed files with 66 additions and 15 deletions

View File

@@ -60,6 +60,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-api</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>

View File

@@ -16,6 +16,7 @@
package org.springframework.modulith.observability;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
@@ -38,10 +39,13 @@ import org.springframework.util.ReflectionUtils;
class ObservedModuleType {
private static Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
private static Predicate<Method> IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers())
&& !(ReflectionUtils.isObjectMethod(it) || IGNORED_TYPES.contains(it.getDeclaringClass()));
private final ApplicationModules modules;
private final ObservedModule module;
private final ArchitecturallyEvidentType type;
private final Predicate<Method> methodsToInterceptFilter;
/**
* Creates a new {@link ObservedModuleType} for the given {@link ApplicationModules}, {@link ObservedModule} and
@@ -60,6 +64,12 @@ class ObservedModuleType {
this.modules = modules;
this.module = module;
this.type = type;
Predicate<Method> isReferenceMethod = candidate -> type.isEventListener() && type.getReferenceMethods() //
.map(ReferenceMethod::getMethod) //
.anyMatch(it -> it.reflect().equals(candidate));
this.methodsToInterceptFilter = IS_USER_METHOD.or(isReferenceMethod);
}
/**
@@ -80,20 +90,14 @@ class ObservedModuleType {
}
/**
* Returns a predicate to filter the methods to intercept. For event listeners it's the listener methods only. For
* everything else, all (public) methods will be intercepted.
* Returns a predicate to filter the methods to intercept. All user declared methods are intercepted, except from
* well-known interfaces ({@code Advised}, {@code TargetClassAware}). For event listeners, package-protected methods
* are supported as well.
*
* @return
* @return will never be {@literal null}.
*/
public Predicate<Method> getMethodsToIntercept() {
if (!type.isEventListener()) {
return it -> !(ReflectionUtils.isObjectMethod(it) || IGNORED_TYPES.contains(it.getDeclaringClass()));
}
return candidate -> type.getReferenceMethods() //
.map(ReferenceMethod::getMethod) //
.anyMatch(it -> it.reflect().equals(candidate));
return methodsToInterceptFilter;
}
private boolean listensToOtherModulesEvents() {

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2023-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 example.sample;
import org.springframework.aop.TargetClassAware;
import org.springframework.aop.framework.Advised;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* @author Oliver Drotbohm
*/
@Component
public abstract class ObservedComponent implements Advised, TargetClassAware {
@Async
public void someMethod() {}
@SuppressWarnings("unused")
private void someInternalMethod() {}
@ApplicationModuleListener
void on(Object event) {}
}

View File

@@ -17,7 +17,7 @@ package org.springframework.modulith.observability;
import static org.assertj.core.api.Assertions.*;
import example.sample.SampleComponent;
import example.sample.ObservedComponent;
import example.sample.SampleConfiguration;
import org.junit.jupiter.api.Test;
@@ -38,17 +38,19 @@ class ObservedModuleTypeUnitTests {
static final ApplicationModules modules = TestApplicationModules.of("example");
ApplicationModule module = modules.getModuleByName("sample").orElseThrow();
ArchitecturallyEvidentType type = module.getArchitecturallyEvidentType(SampleComponent.class);
ArchitecturallyEvidentType type = module.getArchitecturallyEvidentType(ObservedComponent.class);
ObservedModuleType observedType = new ObservedModuleType(modules, new DefaultObservedModule(module), type);
@Test // GH-106
@Test // GH-106, GH-744
void onlyExposesUserMethodsAsToBeIntercepted() {
assertThat(observedType.getMethodsToIntercept()).satisfies(it -> {
assertThat(it.test(ReflectionUtils.findMethod(SampleComponent.class, "someMethod"))).isTrue();
assertThat(it.test(ReflectionUtils.findMethod(ObservedComponent.class, "someMethod"))).isTrue();
assertThat(it.test(ReflectionUtils.findMethod(ObservedComponent.class, "on", Object.class))).isTrue();
assertThat(it.test(ReflectionUtils.findMethod(ObservedComponent.class, "someInternalMethod"))).isFalse();
assertThat(it.test(ReflectionUtils.findMethod(Object.class, "toString"))).isFalse();
assertThat(it.test(ReflectionUtils.findMethod(Advised.class, "getTargetClass"))).isFalse();
});