Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring overrides bean configuration setting its "Primary" one

Using Spring 3.X.X I have 2 services annotated with @Primary and I created another configuration class where I want to use a "customised" version of one of the services.

For some reason I ignore while debugging the configuration class I see that it gets the correct dependencies but when I want to use it it has setup the wrong ones.

I find it easier to explain with code, here is an example:

Interface Foo:

public interface Foo {
    String getMessage();
}

Primary implementation with a default message hardcoded:

@Primary
@Service("FooImpl")
public class FooImpl implements Foo {

    private String message = "fooDefaultMessage";

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Bar interface:

public interface Bar {

    void doBar();
}

Bar default implementation:

@Primary
@Service("BarImpl")
public class BarImpl implements Bar {

    private Foo foo;

    @Override
    public void doBar() {
        System.out.println(foo.getMessage());
    }

    public Foo getFoo() {
        return foo;
    }

    @Autowired
    public void setFoo(Foo foo) {
        this.foo = foo;
    }
}

So far so good, if I want to Inject a Bar I get everything as expected. Thing is I have a Configuration with the following:

@Configuration
public class BeanConfiguration {

    @Bean
    @Qualifier("readOnlyFoo")
    Foo readOnlyFoo() {
        FooImpl foo = new FooImpl();
        foo.setMessage("a read only message");
        return foo;
    }

    @Bean
    @Qualifier("readOnlyBar")
    Bar readOnlyBar() {
        BarImpl bar = new BarImpl();
        bar.setFoo(readOnlyFoo());
        return bar;
    }
}

When I want to inject a readOnlyBar I am getting the default Foo instead of the one set up here.

private Bar readOnlyBar;

@Autowired
@Qualifier("readOnlyBar")
public void setReadOnlyBar(Bar readOnlyBar) {
    this.readOnlyBar = readOnlyBar;
}

...
readOnlyBar.doBar();

Has the Foo bean with "fooDefaultMessage". Why is that? How can I make sure Spring uses the Bean I set on the @Configuration?

Thanks in advance,

EDIT: Note that the same happens if instead of calling the readOnly() method in the @Configuration class you pass a @Qualified argument. i.e.:

@Configuration
public class BeanConfiguration {

    @Bean
    @Qualifier("readOnlyFoo")
    Foo readOnlyFoo() {
        FooImpl foo = new FooImpl();
        foo.setMessage("a read only message");
        return foo;
    }

    @Bean
    @Qualifier("readOnlyBar")
    Bar readOnlyBar(@Qualifier("readOnlyFoo") Foo readOnlyFoo) {
        BarImpl bar = new BarImpl();
        bar.setFoo(readOnlyFoo);
        return bar;
    }
}

If instead I try using Constructor dependency injection everything works as I want/expect but unfortunately I can't use that.

like image 475
void Avatar asked Nov 03 '17 15:11

void


2 Answers

As @yaswanth pointed out in his answer, the problem was that the foo property was overwritten by property injection happening after the creation of the bean.

One way to get around that is to use constructor injection for the BarImpl instead of property injection. That would make your code look like...

@Primary @Service("BarImpl")
class BarImpl implements Bar
{
    private Foo foo;

    @Autowired
    public BarImpl(Foo foo) {
        this.foo = foo;
    }
}

and your configuration would be...

@Configuration
class BeanConfiguration 
{
    @Bean @Qualifier("readOnlyFoo")
    Foo readOnlyFoo() {
        FooImpl foo = new FooImpl();
        foo.setMessage("a read only message");
        return foo;
    }

    @Bean @Qualifier("readOnlyBar")
    Bar readOnlyBar(@Qualifier("readOnlyFoo") Foo readOnlyFoo) {
        return new BarImpl(readOnlyFoo);
    }
}

As a side track; you should also make sure to use dependency injection in you factory methods instead of calling the factory method explicitly, or you will end up with multiple instances of your beans...

Good luck with your future endeavours!

like image 64
Per Huss Avatar answered Nov 10 '22 00:11

Per Huss


You cannot use @Autowired on an instance variable and set that in an @Bean method like you did. Part of spring life cycle goes this way

Bean definitions scanned => Bean instances created => Post Processors called => .....

In your example,

@Bean
@Qualifier("readOnlyBar")
Bar readOnlyBar() {
    BarImpl bar = new BarImpl();
    bar.setFoo(readOnlyFoo());
    return bar;
}

readOnlyBar bean gets created and because readOnlyFoo() is called from within this method, readOnlyFoo bean also gets instantiated. Till this point, readOnlyBar has readOnlyFoo in its instance variable. Once the bean is instantiated, AutowiredAnnotationBeanPostProcessor is called which scans the class of bean (BarImpl in this case) for any @Autowired annotations on instance variables or methods. It finds them and tries to inject the beans into the corresponding variables using field injection or setter injection (wherever the @Autowired annotation is present). In this case since we have @Autowired setter and did not specify the @Qualifier annotation, spring injects the @Primary bean which is the defaultFooMessage bean.

AFAIK, there is no straightforward way to configure in spring for @Bean methods to take priority over Autowiring.

Instantiating all beans with @Bean methods will fix the issue.

like image 35
yaswanth Avatar answered Nov 10 '22 00:11

yaswanth