/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.core.spring; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.framework.AopProxy; import org.springframework.aop.framework.AopProxyFactory; import org.springframework.aop.framework.DefaultAopProxyFactory; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config .ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.config .ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.support.ChildBeanDefinition; import org.springframework.validation.DataBinder; /** * <p> * Extension to the {@link BeanNameAutoProxyCreator} that configures the * {@link ProxyFactory} to allow cglib proxies for beans that don't have a * default constructor. * </p> * <p> * What it actually does is setting a custom aop factory to the * {@link ProxyFactory} that will modify the created proxy to use constructor * arguments when necessary. * </p> * * @author pablo.saavedra */ public class ConstructorArgumentsBeanNameAutoProxyCreator extends BeanNameAutoProxyCreator { /** * Serial version. */ private static final long serialVersionUID = 6056671988239456196L; /** * The logger. */ private static Logger log = LoggerFactory .getLogger(ConstructorArgumentsBeanNameAutoProxyCreator.class); /** * Thread local variable for storing the name of the bean that's currently * under creation. */ private transient ThreadLocal<String> currentBeanName = new ThreadLocal<String>(); /** * Create an AOP proxy for the given bean. * * @param beanClass * the class of the bean * * @param beanName * the name of the bean * * @param specificInterceptors * the set of interceptors that is specific to this bean (may be * empty, but not null) * * @param targetSource * the TargetSource for the proxy, already pre-configured to access * the bean * * @return the AOP proxy for the bean * * @see #buildAdvisors */ protected Object createProxy(final Class<?> beanClass, final String beanName, final Object[] specificInterceptors, final TargetSource targetSource) { this.currentBeanName.set(beanName); return super.createProxy(beanClass, beanName, specificInterceptors, targetSource); } /** * Customizes the {@link ProxyFactory} to use a custom AOP proxy factory. * * @param proxyFactory * The {@link ProxyFactory} to customize */ protected void customizeProxyFactory(final ProxyFactory proxyFactory) { log.trace("Entering customizeProxyFactory('...')"); super.customizeProxyFactory(proxyFactory); String beanName = currentBeanName.get(); if (log.isDebugEnabled()) { log.debug("Bean proxy to customize is " + beanName); } proxyFactory.setAopProxyFactory(new ConstructorArgsAopProxyFactory( (ConfigurableListableBeanFactory) getBeanFactory(), beanName)); log.trace("Leaving customizeProxyFactory"); } /** * <p> * Custom AOP Proxy Factory that sets the constructor arguments to use for * Cglib proxies that are proxying beans with no default constructors. * </p> * <p> * It uses the {@link ConfigurableListableBeanFactory} to access the * {@link BeanDefinition} and its constructor arguments, if any. * </p> * <p> * It will not work with proxies that don't contain a method with the * signature setConstructorArguments(Object[],Class[]). * </p> * * @author pablo.saavedra * */ static class ConstructorArgsAopProxyFactory implements AopProxyFactory { /** * The name of the method to set the constructor arguments. */ private static final String SET_CTOR_METHOD = "setConstructorArguments"; /** * Bean factory to obtains the bean definitions. */ private ConfigurableListableBeanFactory beanFactory; /** * The name of the bean definition to get the constructor arguments from. */ private String beanName; /** * Actual factory to delegate proxy creation to. */ private AopProxyFactory delegate = new DefaultAopProxyFactory(); /** Creates a CustomAopProxyFactory with the given bean factory. * * @param theBeanFactory * The bean factory to use. Cannot be <code>null</code> * @param theBeanName * The name of the bean that's going to be proxied. Cannot be * <code>null</code> */ ConstructorArgsAopProxyFactory( final ConfigurableListableBeanFactory theBeanFactory, final String theBeanName) { Validate.notNull(theBeanFactory); Validate.notEmpty(theBeanName); beanFactory = theBeanFactory; beanName = theBeanName; } /** * <p> * Creates an AOP proxy using the {@link DefaultAopProxyFactory} * implementation, and the sets the constructor arguments if the AOP proxy * is not a JDKDynamicProxy (i-e- is a Cglib proxy), and the proxied bean * has no default constructor. * </p> * <p> * In case of any failure during the process, it'll just return the proxy * returned by its delegate. * </p> * * @param config * The proxy configuration. * @return An AOP proxy, possibly modified to use constructor arguments * @see DefaultAopProxyFactory#createAopProxy(AdvisedSupport) */ public AopProxy createAopProxy(final AdvisedSupport config) { // Create proxy AopProxy proxy = delegate.createAopProxy(config); if (log.isDebugEnabled()) { log.debug("Proxy created, its an instance of " + proxy.getClass().getName()); } // If it's a JDK proxy, just return it if (AopUtils.isJdkDynamicProxy(proxy)) { return proxy; } try { log.debug("Proxy is a Cglib proxy, checking for default constructor"); // If the class has a default constructor, returning the // unmodified proxy config.getTargetClass().getConstructor(new Class[0]); log.debug("Default constructor found, returning proxy as is"); return proxy; } catch (NoSuchMethodException nsme) { // Class has no default constructor, let's set the parameters log.debug("No default constructor, starting argument filling"); } try { log.debug("Starting set constructor arguments"); Method setConstructorArgumentsMethod = proxy.getClass() .getDeclaredMethod(SET_CTOR_METHOD, new Class[] {Object[].class, Class[].class }); Constructor<?>[] targetConstructors = config.getTargetClass() .getConstructors(); if (log.isDebugEnabled()) { log.debug("Found " + targetConstructors.length + " constructors"); } for (Constructor<?> constructor : targetConstructors) { Class<?>[] argumentTypes = constructor.getParameterTypes(); log.debug("Obtaining constructor arguments " + "based on parameter types"); Object[] arguments = getConstructorArguments(argumentTypes, config, beanName); if (arguments == null) { log.debug("Arguments could not be obtained, " + "will try with next constructor if any"); continue; } log.debug("Arguments successfully obtained, " + "attemping to set them in the proxy"); try { setConstructorArgumentsMethod.setAccessible(true); setConstructorArgumentsMethod.invoke(proxy, new Object[] { arguments, argumentTypes }); log.debug("Constructor arguments setted successfully, " + "returning modified proxy"); return proxy; } catch (Exception e) { log.warn("Error while invoking setConstructor method, " + "trying with next constructor", e); } } log.debug("All constructors checked, " + "no matching arguments were found"); } catch (SecurityException e) { log.warn("Security exception while obtaining " + SET_CTOR_METHOD + " method", e); } catch (NoSuchMethodException e) { log.warn(SET_CTOR_METHOD + " method not found", e); } log.warn("The bean has no default constructor, " + "but no matching arguments could be set, " + "proxy creation might fail"); return proxy; } /** * <p> * Obtains the constructor arguments of the given parameter types for the * bean configuration defined in the given {@link AdvisedSupport}. * </p> * <p> * It will return <code>null</code> if the parameters cannot be obtained * for any reason. * </p> * * @param parameterTypes * The types of the parameters to fetch. * @param config * The aop proxy config * @param theBeanName * The name of the bean definition to obtain the constructor * arguments from * @return An object array with the actual parameter values, sorted by their * type according to the parameterTypes or <code>null</code>. */ protected Object[] getConstructorArguments( final Class<?>[] parameterTypes, final AdvisedSupport config, final String theBeanName) { if (log.isTraceEnabled()) { log.trace("Obtaining bean definitions for target class " + config.getTargetClass().getName()); } BeanDefinition bd = beanFactory.getBeanDefinition(theBeanName); log.debug("Obtaining constructor arguments"); ConstructorArgumentValues constructorArgs = getConstructorArguments(bd); if (constructorArgs.getArgumentCount() != parameterTypes.length) { if (log.isDebugEnabled()) { log.debug("Constructor arguments size doesn't " + "match parameter types size (" + constructorArgs.getArgumentCount() + " vs " + parameterTypes.length + "), returning to avoid an exception"); } return null; } List<Object> argValues = new ArrayList<Object>(parameterTypes.length); log.debug("Assembling constructor arguments"); for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; ValueHolder argValue; argValue = constructorArgs.getIndexedArgumentValue(i, parameterType); if (argValue == null) { throw new RuntimeException("Could not find value for argument " + i); } if (log.isDebugEnabled()) { log.debug("Obtained value holder for argument: " + argValue); } Object actualValue = getActualValue(argValue); if (actualValue != null && !parameterType.isInstance(actualValue)) { // The argument types do not match, try to convert them. DataBinder converter = new DataBinder(null); try { actualValue = converter.convertIfNecessary( actualValue, parameterType); } catch (Exception e) { log.warn("The actual value of the parameter does not match " + "the expected argument type. Got " + actualValue.getClass() + " but expected " + parameterType); return null; } } argValues.add(actualValue); } if (log.isTraceEnabled()) { log.trace("All the constructor arguments were obtained: " + argValues.toString()); } return argValues.toArray(); } /** Extracts the actual value from the given {@link ValueHolder}, resolving * bean references through the bean factory. * * @param holder The holder to resolve * * @return The actual value of the holder */ protected Object getActualValue(final ValueHolder holder) { Validate.notNull(holder, "The value holder cannot be null."); log.trace("Start getActualValue"); Object value = holder.getValue(); if (value instanceof BeanDefinitionHolder) { // Constructor argument is an inner bean throw new IllegalArgumentException( "Inner bean definitions are not supported"); } if (value instanceof BeanReference) { BeanReference ref = (BeanReference) value; if (log.isDebugEnabled()) { log.debug("Value is a reference to " + ref.getBeanName() + ", resolving reference"); } value = beanFactory.getBean(ref.getBeanName()); } else if (value instanceof TypedStringValue) { TypedStringValue typedStringValue = (TypedStringValue) value; value = typedStringValue.getValue(); } if (log.isTraceEnabled()) { log.trace("Finishing getActualValue, actual value is " + value); } return value; } /** * Returns the {@link ConstructorArgumentValues} for the given * {@link BeanDefinition}. It'll check parent bean definitions if it has * any and no arguments are found. * * @param bd * The {@link BeanDefinition} to obtain the constructor arguments * from * @return The constructor argument list for the bean definition, of size 0 * if none are found. Never <code>null</code> */ protected ConstructorArgumentValues getConstructorArguments( final BeanDefinition bd) { BeanDefinition copy = bd; ConstructorArgumentValues args = copy.getConstructorArgumentValues(); while ((args.getArgumentCount() == 0) && (copy instanceof ChildBeanDefinition)) { ChildBeanDefinition cbd = (ChildBeanDefinition) copy; copy = beanFactory.getBeanDefinition(cbd.getParentName()); args = copy.getConstructorArgumentValues(); } return args; } } }