Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Cacheable breaks DependencyInjection

I stumbled upon a case where the AOP proxy created by using @Cacheable breaks the dependency injection in Spring 3.1.1. Here is my scenario:

I have an interface and a class implementing this interface using @Cacheable at the implemented method.

Example interface:

public interface ImgService {
    public byte[] getImage(String name);
}

Example implementation:

public class ImgServiceImpl implements ImgService {

    @Cacheable(cacheName = "someCache")
    public byte[] getImage(String name){//TODO};

    protected String someOtherMethod(){//};
}

I also have to JUnit test classes - one which injects the interface and one the implementation:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:META-INF/spring.xml" })
public class ImgServiceTest {

    @Inject
    private ImgService;
}

and

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:META-INF/spring.xml" })
public class ImgServiceImplTest {

    @Inject
    private ImgServiceImpl;
}

Dependency injection for the interface works fine. However, when I get to injecting the implementation in the second test class I get an "Injection of autowired dependencies failed". I was able to debug it and it appears that ClassUtils.isAssignableValue() wrongly compares the desired type to the proxy class. It is called by DefaultListableBeanFactory. What is even stranger is that if I remove the @Cacheable annotation from the implemented method and add it to some other protected/private method, dependency injection works fine again. Is this a bug and what would be the correct approach to handle this situation?

like image 743
kpentchev Avatar asked Aug 13 '12 15:08

kpentchev


2 Answers

It's not a bug, it's an expected side-effect of using JDK dynamic proxies for AOP implementation.

Since all calls to the cacheable method of ImgServiceImpl should go through the dynamic proxy of type ImgService, there is no way to inject this dependency into a field of type ImgServiceImpl.

When you move @Cacheable to private or protected method, injection works because @Cacheable doesn't take effect in this case - only public methods can be adviced using proxy-based AOP.

So, you should either declare fields to be injected as ImgService, or configure Spring to use target class-based proxies instead using proxy-target-class = "true".

Yet another option is to configure Spring to use AspectJ-based AOP implementation (requires compile-time or load-time weaving).

It's applicable to all AOP-based features provided by Spring (transactions, security, async execution, cache, custom aspects, etc).

See also:

  • 7.6 Proxying mechanisms
like image 171
axtavt Avatar answered Sep 20 '22 00:09

axtavt


OK, so here is the solution I came up finally. I implemented a simple method that attempts to extract the target object from the proxy based on its implementation of the org.springframework.aop.framework.Advised class:

@SuppressWarnings({"unchecked"})
public static <T> T getTargetObject(Object proxy, Class<T> targetClass) {
  if (AopUtils.isJdkDynamicProxy(proxy)) {
    try {
        return (T) ((Advised)proxy).getTargetSource().getTarget();
    } catch (Exception e) {
        return null;
    }
  } else {
    return (T) proxy;
  }
}

My implementation test class now looks like this:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:META-INF/spring.xml" })
public class ImgServiceImplTest {

    @Inject
    private ImgService imgService;

    private ImgServiceImpl imgServiceImpl;

    @PostConstruct
    public void setUp() {
        imgServiceImpl = getTargetObject(imgService, ImgServiceImpl.class);
    }


}
like image 29
kpentchev Avatar answered Sep 18 '22 00:09

kpentchev