Spring custom @Enable annotation meta-annotated with @ComponentScan
Asked Answered
W

2

6

I'm trying to write my own @Enable annotation for Spring framework, which should be used as follows:

package com.example.package.app;

@SpringBootApplication
@com.example.annotations.EnableCustom("com.example.package.custom")
public class MyApplication {}

I followed Component scan using custom annotation, but this poses several limitations:

  1. I cannot make the base package property dynamic, i.e. I cannot pass "com.example.package.base", but need to pre-define the package at the configuration.

    I had a look at @AliasFor, but cannot get it to work.

  2. When I leave out the base package, scanning starts from the defining package of the annotation, not from the package of the annotated class. In above's example, it would only scan and create beans for classes in com.example.annotations, but not for com.example.package.*.

    I had a look at EntityScanPackages.Registrar.class which is imported in @EntityScan annotation, but it is an internal class and my annotation cannot import.

Everything works if I put @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class)) on MyApplication class, but stops working when this is moved to a meta-annotation of @EnableCustom. How to tell Spring Framework to consider @EnableCustom as a different way of specifying @ComponentScan with some default values. I tried meta-annotating my annotation with @Configuration, @Component and others, to no avail:

@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = ApplicationService.class))
public @interface EnableApplicationServices {
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] value() default {};
}

Where can I find documentation for this or what starting point would you recommend? My long term goal is to have a Spring Boot starter which can be used by a multitude of projects.


A M(N)WE can be found in the following repository: https://github.com/knittl/stackoverflow/tree/spring-enable-annotation

Here's a rundown of the package structures:

// com.example.annotations.EnableCustom.java
@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
// this annotation is never honored:
@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = MyAnnotation.class))
//@Import(EnableCustom.EnableCustomConfiguration.class)
public @interface EnableCustom {
    // this annotation works in combination with @Import, but scans the wrong packages.
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION,
                    value = MyAnnotation.class))
    class EnableCustomConfiguration {}
}

// file:com.example.app.Application.java
@SpringBootApplication
@EnableCustom("com.example.app.custom.services")
// @ComponentScan(
//         includeFilters = @ComponentScan.Filter(
//                 type = FilterType.ANNOTATION,
//                 value = MyAnnotation.class)) // <- this would work, but I want to move it to a custom annotation
public class Application {
}

// file:com.example.app.custom.services.MyService
@MyAnnotation
public class MyService {
    public MyService() {
        System.out.println("Look, I'm a bean now!");
    }
}

// file:com.example.annotations.services.WrongService.java
@MyAnnotation
public class WrongService {
    public WrongService() {
        System.out.println("I'm in the wrong package, I must not be instantiated");
    }
}
Warring answered 23/5, 2020 at 11:44 Comment(3)
Do you want to make your own @ComponentScan annotation if I'm right?Synaesthesia
@AnishB. Yes, that is basically what I want to do. I want to create a custom annotation which enables component scanning with pre-defined settings (i.e. creating beans for classes with another custom annotation), but flexible enough so that users of the annotation can still configure the base packages. If a custom annotation is not the right approach to this, but another solution exists, that's also fine. But it looks like most other libraries or frameworks rely on some sort of @Enable… annotation.Warring
Hi, I have registered component on myannotation it's working with normal componentscan. Working on EnableCustom do you need filter in that.Synaesthesia
W
5

With the help of Fabio Formosa's answer, the missing bits filled in from this answer, and some inspiration from @EntityScan annotation, I finally managed to get this to work. A compilable, working example can be found at https://github.com/knittl/stackoverflow/tree/spring-enable-annotation-working.

In a nutshell: building on Fabio's answer, it is important to properly configure a ClassPathScanningCandidateComponentProvider instance with include filters and then run it against all provided base classes. @AliasFor(annotation = Import.class, …) does not seem to be required and can be aliased to another attribute, e.g. basePackages of the same annotation.

The minimum implementation is as follows:

@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableCustom.EnableCustomConfiguration.class)
public @interface EnableCustom {
    @AliasFor(attribute = "basePackages")
    String[] value() default {};

    @AliasFor(attribute = "value")
    String[] basePackages() default {};

    class EnableCustomConfiguration implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        private static final BeanNameGenerator BEAN_NAME_GENERATOR = AnnotationBeanNameGenerator.INSTANCE;
        private Environment environment;

        @Override
        public void setEnvironment(final Environment environment) {
            this.environment = environment;
        }

        @Override
        public void registerBeanDefinitions(
                final AnnotationMetadata metadata,
                final BeanDefinitionRegistry registry) {
            final AnnotationAttributes annotationAttributes = new AnnotationAttributes(
                    metadata.getAnnotationAttributes(EnableCustom.class.getCanonicalName()));

            final ClassPathScanningCandidateComponentProvider provider
                    = new ClassPathScanningCandidateComponentProvider(false, environment);
            provider.addIncludeFilter(new AnnotationTypeFilter(MyAnnotation.class, true));

            final Set<String> basePackages
                    = getBasePackages((StandardAnnotationMetadata) metadata, annotationAttributes);

            for (final String basePackage : basePackages) {
                for (final BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {
                    final String beanClassName = BEAN_NAME_GENERATOR.generateBeanName(beanDefinition, registry);
                    if (!registry.containsBeanDefinition(beanClassName)) {
                        registry.registerBeanDefinition(beanClassName, beanDefinition);
                    }
                }
            }
        }

        private static Set<String> getBasePackages(
                final StandardAnnotationMetadata metadata,
                final AnnotationAttributes attributes) {
            final String[] basePackages = attributes.getStringArray("basePackages");
            final Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));

            if (packagesToScan.isEmpty()) {
                // If value attribute is not set, fallback to the package of the annotated class
                return Collections.singleton(metadata.getIntrospectedClass().getPackage().getName());
            }

            return packagesToScan;
        }
    }
}
Warring answered 27/5, 2020 at 17:3 Comment(0)
S
2

Use a custom annotation @EnableAnnotation with basePackages attribute

@EnableAnnotation(basePackages =  "write-here-a-base-package")
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class SampleSimpleApplication implements CommandLineRunner {

  public static void main(String[] args) throws Exception {
   SpringApplication.run(SampleSimpleApplication.class, args);
  }
}

@EnableAnnotation is defined so:

@Retention(RUNTIME)
@Target(TYPE)
@Import(EnableAnnotationConfigRegistrar.class)
public @interface EnableAnnotation {

  String[] basePackages() default "*";

  @AliasFor(annotation = Import.class, attribute = "value")
  Class<?>[] value() default { EnableAnnotationConfigRegistrar.class };

}

Finally, the EnableAnnotationConfigRegistrar.class scans programmatically:

public class EnableAnnotationConfigRegistrar implements ImportBeanDefinitionRegistrar {

 @Override
 public void registerBeanDefinitions(AnnotationMetadata enableAnnotationMetadata,
  BeanDefinitionRegistry registry) {
   AnnotationAttributes enableAnnotationAttributes = new AnnotationAttributes(
   enableAnnotationMetadata.getAnnotationAttributes(EnableAnnotation.class.getName()));

   String[] basePackages = enableAnnotationAttributes.getStringArray("basePackages");
   AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(basePackages);
   }

}
Sheeb answered 25/5, 2020 at 23:7 Comment(2)
Thank you for your answer! Where would I put the filter for the component scan to only create beans annotated with @MyAnnotation? What exactly must be implemented in the registerBeanDefinitions method? In other words: how to scan with the proper filter (similar to @ComponentScan) and how to add the beans to the application context? Another issue I have noticed: when scanning, the registrar is called recursively, scanning the same base package over and over again (the registrar is outside of the base packages, but the @Enable annotated class is contained in the base packages).Warring
I figured it out. I will provide the details tomorrow so that future developers can build on it.Warring

© 2022 - 2024 — McMap. All rights reserved.