skaffman
skaffman

Reputation: 403451

Spring @Bean configs and java polymorphism

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?

Upvotes: 2

Views: 3614

Answers (2)

axtavt
axtavt

Reputation: 242686

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()); 
}

Upvotes: 1

Sean Patrick Floyd
Sean Patrick Floyd

Reputation: 298838

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.

Upvotes: 1

Related Questions