views:

109

answers:

2

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

+1  A: 

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/

+3  A: 

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.

Georgy Bolyuba