Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring AOP Bean Injection Bug?

I've checked in the test project here: https://github.com/loesak/spring-aop-injection-bug

Given the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.loesoft</groupId>
    <artifactId>spring-aop-injection-bug</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <spring.framework.bom.version>4.2.0.RELEASE</spring.framework.bom.version>
        <spring.retry.version>1.1.2.RELEASE</spring.retry.version>
        <aspectj.aspectjweaver>1.8.7</aspectj.aspectjweaver>
        <java.version>1.7</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>${spring.framework.bom.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.retry</groupId>
                <artifactId>spring-retry</artifactId>
                <version>${spring.retry.version}</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectj.aspectjweaver}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

and Spring configuration:

package com.loesoft.spring.aop.injection.bug;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;

@Configuration
@EnableRetry
public class Proof {

    public static void main(String... args) {
        final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Proof.class);
        context.registerShutdownHook();
    }

    @Bean
    public BeanThing beanThing() {
        return new BeanThing();
    }

    @Bean
    @Autowired
    public BeanNeedy beanNeedy(BeanThing beanThing) {
        return new BeanNeedy(beanThing);
    }

    public static interface BeanInterface {
        public void doSomething();
    }

    public static class BeanThing implements BeanInterface {

        @Retryable
        public void doSomething() {
            System.out.println("BeanNeedingDependencies doing something");
        }

    }

    public static class BeanNeedy {

        private final BeanThing beanThing;

        public BeanNeedy(BeanThing beanThing) {
            this.beanThing = beanThing;
        }
    }

}

The following error is thrown by Spring:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'beanNeedy' defined in com.loesoft.spring.aop.injection.bug.Proof: Unsatisfied dependency expressed through constructor argument with index 0 of type [com.loesoft.spring.aop.injection.bug.Proof$BeanThing]: : No qualifying bean of type [com.loesoft.spring.aop.injection.bug.Proof$BeanThing] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.loesoft.spring.aop.injection.bug.Proof$BeanThing] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:464)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1123)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1018)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:305)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:834)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:537)
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:84)
    at com.loesoft.spring.aop.injection.bug.Proof.main(Proof.java:15)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.loesoft.spring.aop.injection.bug.Proof$BeanThing] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:1326)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1072)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:967)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:813)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 14 more

I've tracked this down to the fact that the bean "beanThing" ends up being a JdkDynamicAopProxy because it has interfaces and when trying to find a matching bean as an autowired candidate when creating bean "beanNeedy", the code ends up in ResolvableType#isInstance which does not check the underlying type for the JDK proxy type and does not find the created bean "beanThing" of type "BeanThing".

I can get around this in two ways. The first by setting the @EnableRetry annotation field "proxyTargetClass" to "true" or by removing the interface on the class BeanThing.

Is this a bug or am I missing some information about Spring AOP that I am not aware of? In general, this doesn't seem right to me. I feel Spring should be able to determine the underlying type of the Proxy bean unless there is some technical reason as to why it cannot. And if there is a technical reason as to why the underlying type of the JDK proxy could not be determined, then maybe Spring should have some additional checks to help the developer figure out what is going on.

Keep in mind that this is not related to Spring Retry (thus not tagged) as I've been able to reproduce this problem with other annotations that require the underlying bean to be wrapped with an AOP proxy.

like image 485
loesak Avatar asked Oct 13 '15 19:10

loesak


1 Answers

From the JDK Proxy javadocs:

A dynamic proxy class is a class that implements a list of interfaces specified at runtime when the class is created

This means that the proxy can only implement the interfaces of the bean, but is not an instance of it. You cannot substitute the real class with the proxy, as you do not have the methods and variables of the class, just the methods of the proxy.

I can get around this in two ways. The first by setting the @EnableRetry annotation field "proxyTargetClass" to "true" or by removing the interface on the class BeanThing.

If you set proxyTargetClass to true, instead of a JDK Proxy, Spring will create a CGLIB class (this also happens when the class has no interfaces, because you cannot create a JDK Proxy without interfaces). With CGLIB, a subclass of your bean is dinamically created that intercepts the method calls.

The CGLIB proxy is a BeanThing, because it inherits from it. The JDK Proxy is not a BeanThing, because it just implements its interfaces.

There is a third way to workaround your problem: instead of injecting the BeanThing class, inject the interface it implements (BeanInterface).

@Bean
public BeanInterface beanThing() {
    return new BeanThing();
}

// .....

public static class BeanNeedy {

    private final BeanInterface beanThing;

    public BeanNeedy(BeanInterface beanThing) {
        this.beanThing = beanThing;
    }
}

Configuration

In order to enable CGLIB, you can use the @EnableAspectJAutoProxy annotation with the proxyTargetClass set to true in one of your configuration classes.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class OneOfYourGlobalConfigConfigs {

}
like image 156
Ruben Avatar answered Nov 12 '22 16:11

Ruben