How can I find all beans with the custom annotation @Foo?

55,510

Solution 1

With the help of a couple of Spring experts, I found a solution: The source property of a BeanDefinition can be AnnotatedTypeMetadata. This interface has a method getAnnotationAttributes() which I can use to get the annotations of a bean method:

public List<String> getBeansWithAnnotation( Class<? extends Annotation> type, Predicate<Map<String, Object>> attributeFilter ) {

    List<String> result = Lists.newArrayList();

    ConfigurableListableBeanFactory factory = applicationContext.getBeanFactory();
    for( String name : factory.getBeanDefinitionNames() ) {
        BeanDefinition bd = factory.getBeanDefinition( name );

        if( bd.getSource() instanceof AnnotatedTypeMetadata ) {
            AnnotatedTypeMetadata metadata = (AnnotatedTypeMetadata) bd.getSource();

            Map<String, Object> attributes = metadata.getAnnotationAttributes( type.getName() );
            if( null == attributes ) {
                continue;
            }

            if( attributeFilter.apply( attributes ) ) {
                result.add( name );
            }
        }
    }
    return result;
}

gist with full code of helper class and test case

Solution 2

Use getBeansWithAnnotation() method to get beans with annotation.

Map<String,Object> beans = applicationContext.getBeansWithAnnotation(Foo.class);

Here is similar discussion.

Solution 3

UPDATE: Spring 5.2 changed the behavior of context.getBeansWithAnnotation(...) and it now correctly handles beans created via factory methods. So simply use that.

Original answer


While the accepted answer and Grzegorz's answer contain approaches that will work in all cases, I found a much much simpler one that worked equally well for the most common cases.

  1. Meta-annotate @Foo with @Qualifier:

    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Foo {
    }
    
  2. Sprinkle @Foo onto the factory methods, as described in the question:

    @Foo
    @Bean
    public IFooService service1() {
        return new SpecialFooServiceImpl();
    }
    

But it will also work on the type level:

@Foo
@Component
public class EvenMoreSpecialFooServiceImpl { ... }
  1. Then, inject all the instances qualified by @Foo, regardless of their type and creation method:

    @Autowired
    @Foo
    List<Object> fooBeans; 
    

fooBeans will then contain all the instances produced by a @Foo-annotated method (as required in the question), or created from a discovered @Foo annotated class.

The list can additionally be filtered by type if needed:

@Autowired
@Foo
List<SpecialFooServiceImpl> fooBeans;

The good part is that it will not interfere with any other @Qualifier (meta)annotations on the methods, nor @Component and others on the type level. Nor does it enforce any particular name or type on the target beans.

Solution 4

Short story

It is not enough to put @Foo on the a() method in order to make the a bean annotated with @Foo.

Long story

I didn't realize it before I started debugging Spring code, a breakpoint at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAnnotationOnBean(String, Class<A>) helped me understand it.

Of course, if you moved your annotation to the Named class:

  @Foo
  public static class Named {
  ...

and fixed some minor details of your test (annotation target, etc.) the test works.

After giving it a second thought, it's quite natural. When getBeansWithAnnotation() is called, the only information Spring has are the beans. And beans are objects, objects have classes. And Spring doesn't seem to need to store any additional information, incl. what was the factory method used to create the bean annotated with, etc.

EDIT There is an issue which requests to preserve annotations for @Bean methods: https://jira.springsource.org/browse/SPR-5611

It has been closed as "Won't fix" with the following workaround:

  • Employ a BeanPostProcessor
  • Use the beanName provided to the BPP methods to look up the associated BeanDefinition from the enclosing BeanFactory
  • Query that BeanDefinition for its factoryBeanName (the @Configuration bean) and factoryMethodName (the @Bean name)
  • use reflection to get hold of the Method the bean originated from
  • use reflection to interrogate any custom annotations from that method
Share:
55,510
Aaron Digulla
Author by

Aaron Digulla

I'm a software developer living in Switzerland. You can reach me at digulla at hepe dot com.

Updated on August 01, 2022

Comments

  • Aaron Digulla
    Aaron Digulla almost 2 years

    I have this spring configuration:

    @Lazy
    @Configuration
    public class MyAppConfig {
        @Foo @Bean
        public IFooService service1() { return new SpecialFooServiceImpl(); }
    }
    

    How can I get a list of all beans that are annotated with @Foo?

    Note: @Foo is a custom annotation defined by me. It's not one of the "official" Spring annotations.

    [EDIT] Following the suggestions of Avinash T., I wrote this test case:

    import static org.junit.Assert.*;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import java.lang.annotation.Retention;
    import java.lang.reflect.Method;
    import java.util.Map;
    import org.junit.Test;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Lazy;
    
    public class CustomAnnotationsTest {
    
        @Test
        public void testFindByAnnotation() throws Exception {
    
            AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext( CustomAnnotationsSpringCfg.class );
    
            Method m = CustomAnnotationsSpringCfg.class.getMethod( "a" );
            assertNotNull( m );
            assertNotNull( m.getAnnotation( Foo.class ) );
    
            BeanDefinition bdf = appContext.getBeanFactory().getBeanDefinition( "a" );
            // Is there a way to list all annotations of bdf?
    
            Map<String, Object> beans = appContext.getBeansWithAnnotation( Foo.class );
            assertEquals( "[a]", beans.keySet().toString() );
        }
    
    
        @Retention( RetentionPolicy.RUNTIME )
        @Target( ElementType.METHOD )
        public static @interface Foo {
    
        }
    
        public static class Named {
            private final String name;
    
            public Named( String name ) {
                this.name = name;
            }
    
            @Override
            public String toString() {
                return name;
            }
        }
    
        @Lazy
        @Configuration
        public static class CustomAnnotationsSpringCfg {
    
            @Foo @Bean public Named a() { return new Named( "a" ); }
                 @Bean public Named b() { return new Named( "b" ); }
        }
    }
    

    but it fails with org.junit.ComparisonFailure: expected:<[[a]]> but was:<[[]]>. Why?

  • Aaron Digulla
    Aaron Digulla over 11 years
    Thanks, I completely missed that. I wrote a test case but the test fails (see my edited question). Any idea why?
  • Aaron Digulla
    Aaron Digulla over 11 years
    That's because getBeansWithAnnotation() returns an empty map -> it doesn't find any beans. Why is that?
  • Nandkumar Tekale
    Nandkumar Tekale over 11 years
    @AaronDigulla : I am not sure but can you create any object other than String and check? I think in Spring we can not have String as a bean.
  • Nandkumar Tekale
    Nandkumar Tekale over 11 years
    @AaronDigulla : I am talking about @Foo @Bean public String a() { return "a"; }
  • Aaron Digulla
    Aaron Digulla over 11 years
    *: I updated my test case. It checks that the annotation is on the method and I'm using a non-String bean type. But it still fails :-(
  • Nandkumar Tekale
    Nandkumar Tekale over 11 years
    @AaronDigulla OK. Can you get your bean by bean name ? like, provide name to bean @Foo @Bean("myNameIsA") public Named a() { return new Named( "a" ); } so if we are getting it this way, we can go to next step to solve this issue. :)
  • Aaron Digulla
    Aaron Digulla over 11 years
    I can't use names. I want to group my beans by using several annotations (so a bean can be in several groups or none).
  • Nandkumar Tekale
    Nandkumar Tekale over 11 years
    I meant, if you could get bean by name, then you could get it by annotation too. So I suggested you to test it.
  • Hauke Ingmar Schmidt
    Hauke Ingmar Schmidt over 11 years
    The bean just does not have the annotation, only the bean factory method has - but you are not querying for methods.
  • Aaron Digulla
    Aaron Digulla over 11 years
    Thanks. It's a pity that I can't use annotations to create groups of beans in my Spring config without changing the types but I can probably find a way around it.
  • kaqqao
    kaqqao over 6 years
    Why is this answer upvoted so much? It completely misses the point!
  • Aaron Digulla
    Aaron Digulla about 5 years
    Sorry, this doesn't work for the reasons outlined in the answer by Grzegorz Oledzki.
  • User1291
    User1291 about 5 years
    Great answer, thanks for sharing. If you could briefly explain in which situations this is not enough and why?
  • kaqqao
    kaqqao about 5 years
    @User1291 You might not be able to add @Qualifier to @Foo because it's coming for a 3rd party, for example. My answer depends on your ability to modify the beans and annotations, and not only on inspecting the container from the outside.
  • User1291
    User1291 about 5 years
    Hm ... it looks like this doesn't work when I give the annotation properties (e.g. @Foo(weight = 100) won't get recognised as a @Foo-qualified anymore. Any ideas?
  • kaqqao
    kaqqao about 5 years
    @User1291 I think that's because Spring checks for the annotation equality between the injection point and the candidates, and @Foo and @Foo(weight = 100) are not equal. I think for your case you'd have to inspect the container as described in the other answers.
  • User1291
    User1291 about 5 years
    Yup, you're right, if I specifically search for @Foo(weight = 100) on the collection, it autowires them again. Shame, I had hoped I could somehow turn off the property-checks and get them all regardless of their weights. Thanks, though.