Parawata
Parawata

Reputation: 71

In a Spring Boot application how can I scan for a method return type and the annoatation value using the return type annoatation as a filter?

trying to scan for return type objects that are annoated with a custom annoation but the ClassPathScanningCandidateComponentProvider does not find them even though package is correct and metadata shows the methods annoated with them.

I've added a filter to my existing ClassPathScanningCandidateComponentProvider to include return types on methods annotated with a custom annoation, i.e.

@Procedure(name="PROC_MAPPING_NAME")
@CursorList(cursorType=CursorObject.class)CursorHolder<CursorObject> getCursor(@Param("col_1") @NonNull String param);

Filter is added like so

ClassPathScanningCandidateComponentProvider scanner =
   new ClassPathScanningCandidateComponentProvider(false);
   //existing filters
   ....
   //new filter
   scanner.addIncludeFilter(new AnnotationTypeFilter(CursorList.class));
   //scan packages
return Arrays.stream(packages)
  .map(scanner::findCandidateComponents)
  .flatMap(Collection::stream)
  .map(BeanDefinition::getBeanClassName)
  .map(name -> {
    try {
      return Class.forName(name);
    } 
    catch (ClassNotFoundException e) {
      //handle exception
    }
  }).collect(Collectors.toList());

Works fine for all the existing filters and the package where the new annotation is used is definitely included as I've stepped into the code. It just fails to meet the criteria required to be considered a match and returned.

I've also tested this annotation at class level and it is then found without issue. But, it does not seem to work at attribute level or method or param level.

So stupid question given the class name. Is the ClassPathScanningCandidateComponentProvider only suitable for scanning for annoation usage when the annotation is included at class level? i.e. only finds annoated components?

If that is the case has anyone any suggestions for finding annotations and the objects they annoate where the object is a return type? Or is this just not possible?

Thanks

Upvotes: 1

Views: 1323

Answers (1)

Tomoki Sato
Tomoki Sato

Reputation: 638

It seems that AnnotationTypeFilter matches only classes as far as I investigated its javadoc and implementation.

Fortunately, we can customize AnnotationTypeFilter in order to make it match methods.

The following is an example.

public class CustomAnnotationTypeFilter extends AnnotationTypeFilter {

    public CustomAnnotationTypeFilter(
        Class<? extends Annotation> annotationType) {
        super(annotationType);
    }

    @Override
    protected boolean matchSelf(MetadataReader metadataReader) {
        AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
        return metadata.hasAnnotatedMethods(getAnnotationType().getName());
    }
}

....

ClassPathScanningCandidateComponentProvider scanner =
    new ClassPathScanningCandidateComponentProvider(false);

scanner.addIncludeFilter(new CustomAnnotationTypeFilter(MyAnnotationA.class));

....

Our CustomAnnotationTypeFilter matches a class like:

public class Hoge {

    @MyAnnotationA String doSomething() {
        return "";
    }
}

NOTE

Declaring a method with annotations in front of it means to annotate the method (not the return type) with the annotations.

@Procedure(name="PROC_MAPPING_NAME")
@CursorList(cursorType=CursorObject.class)CursorHolder<CursorObject> getCursor(...) {
 ....
};

The example above means that you annotate getCursor method with Procedure and CursorList. In such cases, you need to check whether methods have an annotation when you create a custom filter.

EDIT_1 (2022/3/16)

I added some examples considering the comments below. The following CustomAnnotationTypeFilter matches Baz, Foo, Fuga and Hoge.

public interface SomeInterface {
    @MyAnnotationA
    void doSomething();
}

public class Baz {

    public SomeInterface doSomething() {
        return null;
    }
}

public class Foo implements SomeInterface {

    @Override
    public void doSomething() {
    }
}

public class Fuga implements IFuga {
    @Override
    public SomeInterface doSomething() {
        return null;
    }
}

public class Hoge {

    @MyAnnotationA String doSomething() {
        return "";
    }
}

public class CustomAnnotationTypeFilter extends AnnotationTypeFilter {

    public CustomAnnotationTypeFilter(
        Class<? extends Annotation> annotationType) {
        super(annotationType);
    }

    @Override
    protected boolean matchSelf(MetadataReader metadataReader) {
        if (super.matchSelf(metadataReader)) {
            return true;
        }
        String className = metadataReader.getClassMetadata().getClassName();
        try {
            Class<?> clazz = ClassUtils.forName(className, getClass().getClassLoader());
            if (annotationExistsOnAMethod(clazz)) {
                System.out.printf("1: matches %s\n", className);
                return true; // matches 'Hoge.java' and 'Foo.java'
            }

            for (Method m : clazz.getDeclaredMethods()) {
                if (annotationExistsOnAMethod(m.getReturnType())) {
                    System.out.printf("2: matches %s\n", className);
                    return true; // matches 'Baz.java' and 'Fuga.java'
                }
            }

        } catch (ClassNotFoundException e) {
            // ignore
        }
        return false;
    }

    private boolean annotationExistsOnAMethod(Class<?> clazz) {
        for (Method m : clazz.getDeclaredMethods()) {
            if (AnnotationUtils.findAnnotation(m, this.getAnnotationType()) != null) {
                return true;
            }
        }
        return false;
    }
}

Scanning components to find annotations is a complex problem. It is not equivalent to / similar to searching with grep command.

You have to explicitly specify the location that the target annotation is placed on. For example, if methods can be annotated with the annotation, you have to check methods explicitly like AnnotationUtils.findAnnotation(method, this.getAnnotationType()) (as the example above shows). Also you may have to check the return types like the example above in a certain situation.

It is helpful to use the utility classes like org.springframework.core.annotation.AnnotationUtils when searching for annotations. Because some methods of such utilities can find annotations even when they are not directly present on the given target itself(See also its javadoc).

Upvotes: 2

Related Questions