I would like to be able to use Spring using setter injection into Scala components. Unfortunately, Scala's native setters are named differently from than the JavaBeans standard, foo_=
rather than setFoo
. Scala does provide a couple of workarounds for this, annotations which force the creation of JavaBeans setters/getters as well as native Scala ones, but that requires annotating every component that I wish to inject. Much more convenient would be to override the BeanWrapper
used by Spring with one which knew how to handle Scala-style getters and setters.
There doesn't appear to be any documentation on how to do such a thing or whether it is even feasible, nor any online examples of anyone else doing it. So before diving into the source, I figured I'd check here
Instead, when you start up your application, Spring Boot dynamically wires up beans and settings and applies them to your application context. Building applications with Spring Boot is really fast, especially for web applications.
Loading Beans from XML Configuration While there are multiple ways of doing this, the recommended way is to create a seperate configuration class to load this xml bean definition file. The key part of the definition is @ImportResource({"classpath*:applicationContext. xml"}) . applicationContext.
ApplicationContext provides the following: Bean factory methods for accessing application components. The ability to load file resources in a generic way. The ability to publish events to registered listeners.
Interesting question, You might find following links useful
http://www.grails.org/Extended+Data+Binding+Plugin#Application-wide DataBinder and BeanWrapper configuration
http://blog.krecan.net/2008/06/17/spring-field-injection/
It looks like AbstractAutowireCapableBeanFactory
(where most of the work with BeanWrapper is done) is hardcoded to use BeanWrapperImpl
. No point of extension there. BeanWrapperImpl
uses CachedIntrospectionResults
which uses Introspector
in turn. Looks like there is no way to configure any of these dependencies. We can try to use standard points of extension: BeanPostProcessor
or BeanFactoryPostProcessor
.
Using just BeanPostProcessor
will not work, because if we are doing something like this:
<bean id="beanForInjection" class="com.test.BeanForInjection">
<property name="bean" ref="beanToBeInjected"/>
</bean>
where BeanForInjection
is a Scala class
package com.test
import com.other.BeanToBeInjected
class BeanForInjection {
var bean : BeanToBeInjected = null;
}
and BeanToBeInjected
is a bean we want to inject, then we will catch exception before BeanPostProcessor
will have a chance to step in. Beans gets populated with values before any callbacks of BeanPostProcessor
called.
But we can use BeanFactoryPostProcessor
to 'hide' properties that are expected to be injected through Scala-like setters and apply them latter.
Something lilke this:
package com.other;
import ...
public class ScalaAwareBeanFactoryPostProcessor implements BeanFactoryPostProcessor, PriorityOrdered {
... PriorityOrdered related methods...
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String[] beanNames = beanFactory.getBeanDefinitionNames();
for (String currentName : beanNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(currentName);
processScalaProperties(beanDefinition);
}
}
protected void processScalaProperties(BeanDefinition beanDefinition) {
String className = beanDefinition.getBeanClassName();
try {
Set<PropertyValue> scalaProperties = new HashSet<PropertyValue>();
for (PropertyValue propertyValue : beanDefinition.getPropertyValues().getPropertyValueList()) {
String scalaSetterName = ScalaAwarePostProcessorUtils.getScalaSetterName(propertyValue.getName());
BeanInfo beanInfo = getBeanInfo(className);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
for (MethodDescriptor md : methodDescriptors) {
if (scalaSetterName.equals(md.getName())) {
boolean isScalaProperty = true;
for (PropertyDescriptor pd : propertyDescriptors) {
if (propertyValue.getName().equals(pd.getName())) {
isScalaProperty = false;
}
}
if (isScalaProperty) {
scalaProperties.add(propertyValue);
}
}
}
}
if (!scalaProperties.isEmpty()) {
beanDefinition.setAttribute(ScalaAwarePostProcessorUtils.SCALA_ATTRIBUTES_KEY, scalaProperties);
}
for (PropertyValue propertyValue : scalaProperties) {
beanDefinition.getPropertyValues().removePropertyValue(propertyValue);
}
} catch (ClassNotFoundException e) {
} catch (IntrospectionException e) {
}
}
private BeanInfo getBeanInfo(String className) throws ClassNotFoundException, IntrospectionException {
Class beanClass = Class.forName(className);
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
cleanIntrospectorCache(beanClass);
return beanInfo;
}
private void cleanIntrospectorCache(Class beanClass) {
Class classToFlush = beanClass;
do {
Introspector.flushFromCaches(classToFlush);
classToFlush = classToFlush.getSuperclass();
}
while (classToFlush != null);
}
}
This implementation simply checks is any bean has properties that are not listed as properties and also have Scala-like setters. All properties that match this contract are removed from properties list and saved as attributes of the bean. Now, all we need is to pull this attributes (if any) for every bean and apply them. There is where we need BeanPostProcessor
(AutowiredAnnotationBeanPostProcessor can be a good example of BeanPostProcessor).
package com.other;
public class ScalaAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements PriorityOrdered, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
... Order related stuff...
public void setBeanFactory(BeanFactory beanFactory) {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}
@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
try {
InjectionMetadata metadata = findScalaMetadata(beanFactory.getBeanDefinition(beanName), bean.getClass());
metadata.inject(bean, beanName, pvs);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of Scala dependencies failed", ex);
}
return pvs;
}
private InjectionMetadata findScalaMetadata(BeanDefinition beanDefinition, Class<?> beanClass) throws IntrospectionException {
LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();
Set<PropertyValue> scalaProperties = (Set<PropertyValue>) beanDefinition.getAttribute(ScalaAwarePostProcessorUtils.SCALA_ATTRIBUTES_KEY);
if (scalaProperties != null) {
for (PropertyValue pv : scalaProperties) {
Method setter = ScalaAwarePostProcessorUtils.getScalaSetterMethod(beanClass, pv.getName());
if (setter != null) {
Method getter = ScalaAwarePostProcessorUtils.getScalaGetterMethod(beanClass, pv.getName());
PropertyDescriptor pd = new PropertyDescriptor(pv.getName(), getter, setter);
elements.add(new ScalaSetterMethodElement(setter, pd));
}
}
}
return new InjectionMetadata(beanClass, elements);
}
private class ScalaSetterMethodElement extends InjectionMetadata.InjectedElement {
protected ScalaSetterMethodElement(Member member, PropertyDescriptor pd) {
super(member, pd);
}
@Override
protected Object getResourceToInject(Object target, String requestingBeanName) {
Method method = (Method) this.member;
MethodParameter methodParam = new MethodParameter(method, 0);
DependencyDescriptor dd = new DependencyDescriptor(methodParam, true);
return beanFactory.resolveDependency(dd, requestingBeanName);
}
}
}
Simply create these two beans in your context:
<bean class="com.other.ScalaAwareBeanFactoryPostProcessor"/>
<bean class="com.other.ScalaAwareBeanPostProcessor"/>
Note:
This is not a final solution. It will work for classes, but it will not work for simple types:
<bean id="beanForInjection" class="com.test.BeanForInjection">
<property name="bean" ref="beanToBeInjected"/>
<property name="name" value="skaffman"/>
</bean>
Solution will work for bean
, but not for name
. This can be fixed, but at this point I think you will be better off just using @BeanInfo annotation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With