Announcement Announcement Module
Collapse
No announcement yet.
Annotation based factory Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Annotation based factory

    Hi

    I am trying to figure out how to configure spring to use a factory/factory method for all interfaces annotated with a specific annotation.

    I created the custom stereotype annotation:
    Code:
    @Component
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {}
    that I use to annotate several different interfaces
    Code:
    @MyAnnotation
    public interface MyInterface1 {...}
    
    @MyAnnotation
    public interface MyInterface2 {...}
    
    ...
    
    @MyAnnotation
    public interface MyInterfaceN {...}
    and I have the factory that I use to instantiate beans for those interfaces (error checking omitted):
    Code:
    @Component
    public class MyFactory {
      public <T> T createBean(Class<T> type) {
        return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] { type }, new MyHandler());
      }
    }
    Right now, I have to individually declare each interface in the applicationContext.xml. Something like this for each interface:
    HTML Code:
    <bean class="MyInterface1" factory-bean="MyFactory" factory-method="createBean">
      <constructor-arg type="java.lang.Class" value="MyInterface1" />
    </bean>
    which is boring and error prone. As I am using <context:component-scan> I would like to find a way to "tell" the container to create a bean definition like above every time it finds an interface annotated with @MyAnnotation, the same way that annotating a class with @Component creates a bean definition like <bean class="MyClass1"/> so I don't have to declare all interfaces and the same factory in the applicationContext.xml over and over again. Something like Spring Data does where one must write only the interfaces and an automatic implementation is provided.

    Perhaps something I can include in the annotation itself that wires it to the factory or something like that?

    Does anyone know how can I accomplish that?

    Best Regards
    SVPace
    Last edited by svpace; Jan 6th, 2013, 02:57 AM.

  • #2
    Hi everyone,

    Since I couldn't get an answer on the forums I started digging in the source code and I figured out how to to this. I was a bit more complicated than I thought but once I found the proper extension points it was no really that hard.

    First I created a new stereotype annotation to mark my interfaces. I called it Fabrication (I've liked it because I believe that it communicates both, the idea that this is not a real bean and that it must be produced by another bean):
    Code:
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Fabrication {
    	String value() default "";
    	String factoryBeanName();
    	String factoryMethodName();
    }
    Then I created a sub-class of "ClassPathBeanDefinitionScanner" to handle the new stereotype:
    Code:
    public class ClassPathFabricationDefinitionScanner extends ClassPathBeanDefinitionScanner {
    
    	// constructors ommited
    	
    	@Override
    	protected void registerDefaultFilters() {
    		addIncludeFilter(new AnnotationTypeFilter(Fabrication.class));
    	}
    
    	@Override
    	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    		return beanDefinition.getMetadata().isInterface()
    			&& !Arrays.asList(beanDefinition.getMetadata().getInterfaceNames()).contains(Annotation.class.getName());
    	}
    
    	@Override
    	protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
    		try {
    			super.postProcessBeanDefinition(beanDefinition, beanName);
    			MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(beanDefinition.getResource());
    			AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
    
    			if(metadata.isAnnotated(Fabrication.class.getName())) {
    				AnnotationAttributes attributes =
    					AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(Fabrication.class.getName(), false));
    				String factoryBeanName = attributes.getString("factoryBeanName");
    				Assert.hasText(factoryBeanName, "'factoryBeanName' must not be blank");
    				String factoryMethodName = attributes.getString("factoryMethodName");
    				Assert.hasText(factoryBeanName, "'factoryMethodName' must not be blank");
    				beanDefinition.setFactoryBeanName(factoryBeanName);
    				beanDefinition.setFactoryMethodName(factoryMethodName);
    			}
    		} catch(IOException exception) {
    			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", exception);
    		}
    	}
    }
    Basically I had to override:
    • "registerDefaultFilters" to register @Fabrication instead of @Component;
    • "isCandidateComponent" to detect non-annotation interfaces instead of concrete classes;
    • "postProcessBeanDefinition" to set the factory bean name and method
    Then I created a custom XML configuration "fabrication-scan" based on a bean definition parser extending "ComponentScanBeanDefinitionParser":
    Code:
    public class FabricationScanBeanDefinitionParser extends ComponentScanBeanDefinitionParser {
    	@Override
    	protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
    		return new ClassPathFabricationDefinitionScanner(readerContext.getRegistry(), useDefaultFilters);
    	}
    }
    Overriding "createScanner" to create my new "ClassPathFabricationDefinitionScanner".

    Creating the schema for "fabrication-scan" was more complicated than it had to be because the complex-type within "component-scan" in "spring-context.xsd" was unnamed and therefore I could not reuse/extend it, so I copied and pasted it into the "fabrication-scan" definition (I would be glad to submit a patch for this if someone could point me out where to send it to).

    That was basically it. Now all I have to do is add "<fabrication-scan base-package="a.b.c"/>" to my application context and annotate my fabrications with @Fabrication:
    Code:
    @Fabrication("MyFactory", "createBean")
    public interface MyInterface1 {...}
    And that is all folks

    Hope it helps someone else. Again, I would be glad to contribute this code if someone could point me out how to do it.

    Best Regards
    SVPace

    Comment

    Working...
    X