-
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
-
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