jwilner
jwilner

Reputation: 6606

Spring classpath component scanning

I need to build mappings for classes (literally a Map<Class<?>, String>), which won't vary at runtime, and keeping things decoupled is a priority. Since I'm in a Spring application, I thought I'd use an annotation and ClassPathScanningCandidateComponentProvider more or less like so:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Mapping {
     String value();
}

And:

public class MappingLookUp {
    private static final Map<Class<?>, String> MAPPING_LOOK_UP;
    static {
        Map<Class<?>, String> lookUp = new HashMap<>();
        ClassPathScanningCandidateComponentProvider scanningCandidateComponentProvider = new ClassPathScanningCandidateComponentProvider(false);
        scanningCandidateComponentProvider.addIncludeFilter(new AnnotationTypeFilter(Mapping.class));
        for (BeanDefinition beanDefinition : scanningCandidateComponentProvider.findCandidateComponents("blah")) {
            Class<?> clazz;
            try {
                clazz = Class.forName(beanDefinition.getBeanClassName());
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            Mapping mapping = AnnotationUtils.getAnnotation(clazz, Mapping.class);
            if (mapping == null) {
                throw new IllegalStateException("This should never be null");
            }
            lookUp.put(clazz, mapping.value());
        }
        MAPPING_LOOK_UP = Collections.unmodifiableMap(lookUp);
    }

    public static String getMapping(Class<?> clazz) {
        ...
    }
}

Although I believe this will work, this feels like:

  1. a lot to put in a static initialization
  2. a hacky use of the scanning component provider, even though it's commonly recommended for this purpose; BeanDefinition makes it sound like it's intended for finding Spring beans rather than general class definitions.

To be clear, the annotated values are data classes -- not Spring-managed beans -- so a BeanPostProcessor pattern doesn't fit, and indeed, that's why it feels awkward to use the scanning component provider that, to me, seems intended for discovery of Spring managed beans.

Is this the proper way to be implementing this pattern? Is it a proper application of the provider? Is there a feasible alternative without pulling in other classpath scanning implementations?

Upvotes: 4

Views: 2794

Answers (2)

StanislavL
StanislavL

Reputation: 57381

I asked a very similar question recently How to get list of Interfaces from @ComponentScan packages and finally implemented the first of suggested approaches.

You can see the code https://github.com/StanislavLapitsky/SpringSOAProxy see https://github.com/StanislavLapitsky/SpringSOAProxy/blob/master/core/src/main/java/org/proxysoa/spring/service/ProxyableScanRegistrar.java and of course initialization annotation https://github.com/StanislavLapitsky/SpringSOAProxy/blob/master/core/src/main/java/org/proxysoa/spring/annotation/ProxyableScan.java the key thing is to add @Import({ProxyableScanRegistrar.class})

The key code is

public class ProxyableScanRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private Environment environment;

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

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // Get the ProxyableScan annotation attributes
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ProxyableScan.class.getCanonicalName());

        if (annotationAttributes != null) {
            String[] basePackages = (String[]) annotationAttributes.get("value");

            if (basePackages.length == 0) {
                // If value attribute is not set, fallback to the package of the annotated class
                basePackages = new String[]{((StandardAnnotationMetadata) metadata).getIntrospectedClass().getPackage().getName()};
            }

Upvotes: 0

nicholas.hauschild
nicholas.hauschild

Reputation: 42849

I will suggest this doesn't look like it is done in a very Spring-y way.

If I were to be doing this, I would utilize Spring's BeanPostProcessor or BeanFactoryPostProcessor. Both of these allow for introspection on all Bean's in Spring's BeanFactory, and would allow you to get away from the static-ness of your current setup, as the PostProcessors are just Spring Bean's themselves.

class MappingLookup implements BeanPostProcessor {
  private final Map<Class<?>, String> lookup = new HashMap<>();

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) {
    // check bean's class for annotation...
    // add to lookup map as necessary...
    // make sure to return bean (javadoc explains why)
    return bean;
  }

  public String getMapping(Class<?> clazz) {
    // ...
  }

  // omitted other methods...
}

Upvotes: 4

Related Questions