I've been making increasingly heavy use of the new @Bean
configuration style in Spring 3, as a more type-safe alternative to XML bean definition files. Occasionally, though, this type-safety can prevent you do what should be valid things, due to a combination of Java's lack of type expressiveness, and Spring scoped proxies.
A full unit test which demonstrates the problem is below, but briefly put I have a class ServiceBean
, which implements interfaces ServiceA
and ServiceB
. This bean is a scoped proxy (session-scoped in this case). I also have beans ClientA
and ClientB
, which are injected with objects of type ServiceA
and ServiceB
respectively.
In Spring XML config, there's no problem with this. Spring generates a JDK-proxy for the ServiceBean
, which implements both interfaces, and both are injected into the client beans. It's all reflective, and the types are fine at runtime.
Try this in @Bean
-style, though, and you have problems. Here's the demonstrative test.
Firstly, the services:
public interface ServiceA {}
public interface ServiceB {}
public class ServiceBean implements ServiceA, ServiceB {}
Now, the clients:
public class ClientA {
public ClientA(ServiceA service) {}
}
public class ClientB {
public ClientB(ServiceB service) {}
}
Now, the Spring bean definitions:
@Configuration
public class ScopedProxyConfig {
@Bean @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES)
public ServiceBean services() {
return new ServiceBean();
}
@Bean
public ClientA clientA() {
return new ClientA(services());
}
@Bean
public ClientB clientB() {
return new ClientB(services());
}
}
And finally, the unit test and support context:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class ScopedProxyTest {
private @Resource ClientA clientA;
private @Resource ClientB clientB;
public @Test void test() {
assertThat(clientA, is(notNullValue()));
assertThat(clientB, is(notNullValue()));
}
}
<beans>
<context:annotation-config/>
<bean class="test.ScopedProxyConfig"/>
</beans>
(XML namespaces omitted for clarity).
This all compiles nicely. Run the test, though, and you get a type casting runtime exception:
Caused by: java.lang.ClassCastException: $Proxy11 cannot be cast to test.ServiceBean at test.ScopedProxyConfig$$EnhancerByCGLIB$$d293ecc3.services() at test.ScopedProxyConfig.clientA(ScopedProxyConfig.java:26)
It's not clear to me exactly what this is telling me, but it appears to be a clash between the JDK proxy (which implements ServiceA
and ServiceB
) and the ServiceBean
object.
I've tried getting clever with generics:
@Bean @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES)
public <T extends ServiceA & ServiceB> T services() {
return (T)new ServiceBean();
}
But that doesn't even compile.
This isn't an especially exotic situation, I think, and I've run into it a few times before. In the past, the workaround has been to use TARGET_CLASS
proxying instead of interface proxying, but that's not an option for me here.
Can anyone figure out how to make this work?