tags:

views:

47

answers:

2

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?

+1  A: 

I think you'll have to go for a more interface-based solution:

create an interface ServiceC:

public interface ServiceC extends ServiceA, ServiceB {}

and let ServiceBean implement that interface

public class ServiceBean implements ServiceC{}

And in your ScopedProxyConfig:

@Bean @Scope(value=WebApplicationContext.SCOPE_SESSION,
             proxyMode=ScopedProxyMode.INTERFACES)
public ServiceC services() {
    return new ServiceBean();
}

Consistent use of interfaces should let Spring work with JDK proxies.

seanizer
Yeah, I'm considering that, but the two interfaces aren't really related, so introducing an artificial umbrella type doesn't feel right. It may be the least annoying solution, though.
skaffman
+1  A: 

This one at least compiles, perhaps it will work:

@Bean @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES) 
public <T extends ServiceA & ServiceB> T services() { 
    return (T)new ServiceBean(); 
}

@Bean 
public ClientA clientA() { 
    return new ClientA(this.<ServiceBean>services()); 
} 

@Bean 
public ClientB clientB() { 
    return new ClientB(this.<ServiceBean>services()); 
}
axtavt
skaffman