Alex Theedom
Alex Theedom

Reputation: 1662

How to determine that the ApplicationConfig.class is marked with a custom annotation

I would like to enable application features based on the presence of a custom annotation that marks the ApplicationConfig.class as below:

@FooBar(enabled = true)
@Configuration
@ComponentScan(basePackageClasses = Application.class, excludeFilters = @Filter({Controller.class, Configuration.class}))
@EnableJpaRepositories("com.package.repository")
class ApplicationConfig { 
    // Application specific configs and bean defs.
}

The custom annotation is named FooBar:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FooBar   {
    boolean enabled() default true;
}

During application startup I would like to detect that this class (or any other class/bean) is annotated with this annotation.

Here is my attempt, partly based on this similar question it includes two ways to determine that the annotation is being used.

@Component
public class MyClassWithEventListeners implements ApplicationContextAware {

@Autowired
ApplicationEventPublisher applicationEventPublisher;

@Autowired
ApplicationContext applicationContext;


@EventListener
void contextRefreshedEvent(ContextRefreshedEvent event) {
    ApplicationContext applicationContext = event.getApplicationContext();
    applicationContext.getClassLoader();

    AutowireCapableBeanFactory autowireCapableBeanFactory = applicationContext.getAutowireCapableBeanFactory();

    String[] names = event.getApplicationContext().getBeanDefinitionNames();

    for (String name : names) {
        Object o = autowireCapableBeanFactory.getBean(name);
        if (AopProxyUtils.ultimateTargetClass(o).isAnnotationPresent(FooBar.class)) {
            System.out.println("Found class annotated with FooBar");
        }
    }

}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
    List<String> beanNames = getBeansWithAnnotation(FooBar.class);
    if(beanNames !=null){
        System.out.println("Found class annotated with FooBar");
    }
}

public List<String> getBeansWithAnnotation(Class<? extends Annotation> type) {
    Predicate<Map<String, Object>> filter = Predicates.alwaysTrue();
    return getBeansWithAnnotation(type, filter);
}

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

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

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

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

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

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

    return result;
}
}

Both the contextRefreshedEvent() and setApplicationContext() methods are called and neither are able to detect the custom annotation.

What I have observed is that my ApplicationConfig.class is present in the list of beans/classes but appears as follows:

com.package.config.ApplicationConfig$$EnhancerBySpringCGLIB$$15073fb3@196887

Upvotes: 0

Views: 506

Answers (3)

Alex Theedom
Alex Theedom

Reputation: 1662

I have looked in to this further and found two possible solutions. The first simply detects if the custom annotation is present:

@Component
public class MyClassWithEventListeners {

@EventListener
void contextRefreshedEvent(ContextRefreshedEvent event) throws NoSuchFieldException, IllegalAccessException {
    ApplicationContext applicationContext = event.getApplicationContext();
    String[] annotations = applicationContext.getBeanNamesForAnnotation(FooBar.class);
    if (annotations != null && annotations.length > 0) {
        System.out.println("Annotation found");
    } else {
        System.out.println("Annotation not found");
    }
}
}

The second uses reflection to get the value set in the annotation. In this case the value of enabled.

@Component
public class MyClassWithEventListeners {

@EventListener
void contextRefreshedEvent(ContextRefreshedEvent event) throws NoSuchFieldException, IllegalAccessException {
    ApplicationContext applicationContext = event.getApplicationContext();
    AnnotationConfigWebApplicationContext annotationContext = ((AnnotationConfigWebApplicationContext) applicationContext);

    for (Field field : annotationContext.getClass().getDeclaredFields()) {
        if ("annotatedClasses".equals(field.getName())) {
            field.setAccessible(true);
            Set<Class<?>> classes = (Set<Class<?>>) field.get(annotationContext);
            for (Class clazz : classes) {
                for (Annotation annotation : clazz.getDeclaredAnnotations()) {
                    if (annotation.annotationType().isAssignableFrom(FooBar.class)) {
                        enabled = ((ServiceRegistration) annotation).enabled();
                        System.out.println("Enabled: " + enabled);
                    }
                }
            }
        }
    }
}
}

This seem a little clumsy. I wonder if there is not a more elegant way to resolve this issue.

Upvotes: 0

Vilius
Vilius

Reputation: 77

Maybe you can use utilities provided by Class instead of utils provided by AoP?

@Test
public void testAccess(){
    SomeAnnotatedClass annotatedClassInstance = new SomeAnnotatedClass();

    Assert.assertNotNull(annotatedClassInstance.getClass().getAnnotation(FooBar.class));
}

Upvotes: 0

Stephane Nicoll
Stephane Nicoll

Reputation: 33141

The core container has actually support for that stuff so there is no need to hack the context. You may benefit from reading what's happening when one adds @EnableXYZ on a configuration class. For instance @EnableCaching or @EnableJms work pretty much the same way:

The interface is meta-annotated with @Import which leads to more beans being loaded by the context. The enabled part is a bit useless IMO. The presence or the absence of the annotation plays the same role and is much more explicit.

Upvotes: 2

Related Questions