4

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?

jwilner
  • 6,348
  • 6
  • 35
  • 47
  • A mapping from Class to String, seems quite strange. You have no restriction on the class, like an interface it must implement, and Strings should only be displayed. Could you elaborate on the the use-case, I could see a case from String to Class, but not the other way around. - And my eyes hurt when you use static initializers, especially when you are using Spring ;-) – Klaus Groenbaek May 01 '17 at 22:07
  • These are disparate object types which effectively need to be tagged for a cross cutting audit system. I could enforce an interface with a method that returns the tag, but because that value never varies at runtime, and it's not important to any of the implementing classes, I think an annotation is more appropriate. It is very much metadata. – jwilner May 01 '17 at 22:22
  • So you have an instance of the class, and then call `MappingLookUp. getMapping(instance.getClass())` to get the audit String ? – Klaus Groenbaek May 02 '17 at 15:52

2 Answers2

4

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...
}
nicholas.hauschild
  • 42,483
  • 9
  • 127
  • 120
  • Your answer makes sense, but doesn't quite fit my use case (this is my fault for not making clear). I am not annotating Spring managed beans, but data objects. – jwilner May 01 '17 at 17:35
0

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()};
            }
Community
  • 1
  • 1
StanislavL
  • 56,971
  • 9
  • 68
  • 98