Skip to content

Commit

Permalink
feat: ignore listeners methods introduced by java generic type erasure (
Browse files Browse the repository at this point in the history
#463)

* feat: ignore listeners methods introduced by java generic type erasure

* feat: use isBridge method (wip)

* feat: add Inherited annotation

* feat(core): support multiple annotations on the same method

Co-authored-by: Timon Back <[email protected]>

---------

Co-authored-by: David Müller <[email protected]>
  • Loading branch information
timonback and sam0r040 authored Nov 17, 2023
1 parent 545e4a2 commit 756a41d
Show file tree
Hide file tree
Showing 19 changed files with 210 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncOperationBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand All @@ -17,6 +18,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@AsyncOperationBinding
@Inherited
public @interface AsyncGenericOperationBinding {
/**
* The name of the binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.stavshamir.springwolf.asyncapi.types.OperationData;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -33,6 +34,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Repeatable(AsyncListeners.class)
@Inherited
public @interface AsyncListener {
/**
* Mapped to {@link OperationData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationCollectors;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.util.StringValueResolver;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -66,8 +73,9 @@ private Stream<Method> getAnnotatedMethods(Class<?> type) {
log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName());

return Arrays.stream(type.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(annotationClass)
|| method.isAnnotationPresent(annotationClassRepeatable));
.filter(method -> !method.isBridge())
.filter(method -> AnnotationUtils.findAnnotation(method, annotationClass) != null
|| AnnotationUtils.findAnnotation(method, annotationClassRepeatable) != null);
}

private Stream<OperationData> toOperationData(Method method) {
Expand All @@ -80,7 +88,16 @@ private Stream<OperationData> toOperationData(Method method) {
Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method);

Class<AsyncListener> annotationClass = AsyncListener.class;
return Arrays.stream(method.getAnnotationsByType(annotationClass))
Set<AsyncListener> annotations = MergedAnnotations.from(
method,
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.standardRepeatables())
.stream(annotationClass)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(MergedAnnotation::withNonMergedAttributes)
.collect(MergedAnnotationCollectors.toAnnotationSet());

return annotations.stream()
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@interface AsyncListeners {
@Inherited
public @interface AsyncListeners {
AsyncListener[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand All @@ -15,6 +16,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Inherited
public @interface AsyncMessage {
/**
* Mapped to {@link Message#getDescription()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaderSchema;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation is mapped to {@link OperationData}
*/
@Retention(RetentionPolicy.CLASS)
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@Inherited
public @interface AsyncOperation {
/**
* Mapped to {@link OperationData#getChannelName()}
Expand Down Expand Up @@ -50,7 +52,8 @@

@Retention(RetentionPolicy.CLASS)
@Target({})
@interface Headers {
@Inherited
public @interface Headers {
/**
* Mapped to {@link AsyncHeaders#getSchemaName()}
*/
Expand All @@ -60,7 +63,8 @@

@Retention(RetentionPolicy.CLASS)
@Target({})
@interface Header {
@Inherited
public @interface Header {
/**
* Mapped to {@link AsyncHeaderSchema#getHeaderName()}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.stavshamir.springwolf.asyncapi.types.OperationData;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -32,6 +33,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Repeatable(AsyncPublishers.class)
@Inherited
public @interface AsyncPublisher {
/**
* Mapped to {@link OperationData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationCollectors;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.util.StringValueResolver;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

@Slf4j
Expand Down Expand Up @@ -66,8 +73,9 @@ private Stream<Method> getAnnotatedMethods(Class<?> type) {
log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName());

return Arrays.stream(type.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(annotationClass)
|| method.isAnnotationPresent(annotationClassRepeatable));
.filter(method -> !method.isBridge())
.filter(method -> AnnotationUtils.findAnnotation(method, annotationClass) != null
|| AnnotationUtils.findAnnotation(method, annotationClassRepeatable) != null);
}

private Stream<OperationData> toOperationData(Method method) {
Expand All @@ -80,7 +88,16 @@ private Stream<OperationData> toOperationData(Method method) {
Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method);

Class<AsyncPublisher> annotationClass = AsyncPublisher.class;
return Arrays.stream(method.getAnnotationsByType(annotationClass))
Set<AsyncPublisher> annotations = MergedAnnotations.from(
method,
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.standardRepeatables())
.stream(annotationClass)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(MergedAnnotation::withNonMergedAttributes)
.collect(MergedAnnotationCollectors.toAnnotationSet());

return annotations.stream()
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@interface AsyncPublishers {
@Inherited
public @interface AsyncPublishers {
AsyncPublisher[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.core.annotation.Order;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand All @@ -27,6 +28,7 @@ protected ProcessedOperationBinding mapToOperationBinding(TestOperationBinding b
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@AsyncOperationBinding
@Inherited
public @interface TestOperationBinding {
TestMessageBindingProcessor.TestMessageBinding operationBinding() default
@TestMessageBindingProcessor.TestMessageBinding();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.core.annotation.Order;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -37,5 +38,6 @@ private ProcessedMessageBinding mapToMessageBinding(

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Inherited
public @interface TestMessageBinding {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.core.annotation.Order;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand All @@ -30,6 +31,7 @@ public Optional<ProcessedOperationBinding> process(Method method) {

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Inherited
public @interface TestOperationBinding {
TestMessageBindingProcessor.TestMessageBinding operationBinding() default
@TestMessageBindingProcessor.TestMessageBinding();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -119,5 +120,6 @@ private static class SimpleFoo {

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TestChannelListener {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
Expand Down Expand Up @@ -54,7 +56,7 @@
"test.property.server1=server1",
"test.property.server2=server2"
})
class AsyncListenerAnnotationScannerTest {
class AsyncListenerAnnotationScannerIntegrationTest {

@Autowired
private AsyncListenerAnnotationScanner channelScanner;
Expand Down Expand Up @@ -243,6 +245,39 @@ void scan_componentHasAsyncMethodAnnotation() {
assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel));
}

@ParameterizedTest
@ValueSource(classes = {ClassImplementingInterfaceWithAnnotation.class})
void scan_componentHasOnlyDeclaredMethods(Class<?> clazz) {
// Given a class with a method, which is declared in a generic interface
setClassToScan(clazz);

// When scan is called
Map<String, ChannelItem> actualChannels = channelScanner.scan();

// Then the returned collection contains the channel with the actual method, excluding type erased methods
Message message = Message.builder()
.name(String.class.getName())
.title(String.class.getSimpleName())
.description(null)
.payload(PayloadReference.fromModelName(String.class.getSimpleName()))
.schemaFormat("application/vnd.oai.openapi+json;version=3.0.0")
.headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))
.bindings(EMPTY_MAP)
.build();

Operation operation = Operation.builder()
.description("test channel operation description")
.operationId("test-channel_publish")
.bindings(EMPTY_MAP)
.message(message)
.build();

ChannelItem expectedChannel =
ChannelItem.builder().bindings(null).publish(operation).build();

assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel));
}

private static class ClassWithoutListenerAnnotation {

private void methodWithoutAnnotation() {}
Expand Down Expand Up @@ -319,6 +354,36 @@ private void methodWithAnnotation(SimpleFoo payload) {}
private void methodWithoutAnnotation() {}
}

private static class ClassImplementingInterface implements ClassInterface<String> {

@AsyncListener(
operation =
@AsyncOperation(
channelName = "test-channel",
description = "test channel operation description"))
@Override
public void methodFromInterface(String payload) {}
}

interface ClassInterface<T> {
void methodFromInterface(T payload);
}

private static class ClassImplementingInterfaceWithAnnotation implements ClassInterfaceWithAnnotation<String> {

@Override
public void methodFromInterface(String payload) {}
}

interface ClassInterfaceWithAnnotation<T> {
@AsyncListener(
operation =
@AsyncOperation(
channelName = "test-channel",
description = "test channel operation description"))
void methodFromInterface(T payload);
}

@Data
@NoArgsConstructor
@Schema(description = "SimpleFoo Message Description")
Expand Down
Loading

0 comments on commit 756a41d

Please sign in to comment.