45

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
  • 321,842
  • 108
  • 597
  • 820

7 Answers7

55

Use getBeansWithAnnotation() method to get beans with annotation.

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

Here is similar discussion.

Community
  • 1
  • 1
Avinash T.
  • 2,280
  • 2
  • 16
  • 23
38

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.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • 1
    Great answer, thanks for sharing. If you could briefly explain in which situations this is not enough and why? – User1291 May 20 '19 at 07:53
  • @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. – kaqqao May 20 '19 at 08:01
  • 1
    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? – User1291 May 20 '19 at 09:19
  • 1
    @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. – kaqqao May 20 '19 at 09:50
  • 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. – User1291 May 20 '19 at 10:21
26

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

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
13

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
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
Grzegorz Oledzki
  • 23,614
  • 16
  • 68
  • 106
  • 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. – Aaron Digulla Jan 10 '13 at 08:32
2

To get all annotated beans:

context.getBeansWithAnnotation(Foo.class)

This returns a Map<String, Object> where the key is the bean name.

To get the annotation class:

context.findAnnotationOnBean(beanName, Foo.class);

This can be helpful when the annotation has values (@Foo(weight=100)).

Chadi
  • 737
  • 1
  • 6
  • 21
0

This is how to get annotated beans.

@Autowired
private ApplicationContext ctx;

public void processAnnotation() {
    // Getting annotated beans with names
    Map<String, Object> allBeansWithNames = ctx.getBeansWithAnnotation(TestDetails.class);
    //If you want the annotated data
    allBeansWithNames.forEach((beanName, bean) -> {
        TestDetails testDetails = (TestDetails) ctx.findAnnotationOnBean(beanName, TestDetails.class);
        LOGGER.info("testDetails: {}", testDetails);
    });
}
0

In my case getBeansWithAnnotation was returning an empty list. My mistake was to not adding retention and target on my custom annotation.

Adding these lines no top of my annotation fixed it.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
MHogge
  • 5,408
  • 15
  • 61
  • 104