Technetium
Technetium

Reputation: 6158

Respect @Lazy annotation on non-@Primary @Bean

I'm having problems getting Spring to respect the @Lazy annotation on @Bean methods when it is configured to use a different @Bean method that returns an implementation of the same interface that is flagged as @Primary.

Specifically, I have a @Configuration-annotated class with several @Bean methods that all return the same interface. Many of these @Bean methods are @Lazy, as they contact external services for which the application may not currently be using. The @Primary bean is not @Lazy, as it looks at runtime configuration to determine which implementation to return.

Here is a contrived example of that configuration class, revolving around a fictitious ThingService interface:

@Configuration
@ComponentScan(basePackages = { "com.things" })
public class ThingConfiguration {

    @Bean
    public ThingOptions thingOptions() {
        ThingOptions options = new ThingOptions();
        options.sharing = true;
        return options;
    }

    @Primary
    @Bean
    public ThingService primaryThing(ThingOptions options, ApplicationContext context) {
        System.out.println("PrimaryThing -- Initialized");

        if (options.sharing) {
            return context.getBean("OurThing", ThingService.class);
        } else {
            return context.getBean("YourThing", ThingService.class);
        }
    }

    @Lazy
    @Bean(name = "YourThing")
    public ThingService yourThing() {
        System.out.println("YourThingService -- Initialized");
        return new YourThingService();
    }

    @Lazy
    @Bean(name = "OurThing")
    public ThingService ourThing() {
        System.out.println("OurThingService -- Initialized");
        return new OurThingService();
    }

}

I then have a @Component that depends on this interface which that the @Primary annotation will ensure that the correct implementation will be injected into the object. Here is an example of that downstream @Component:

@Component
public class ThingComponent {

    private final ThingService thingService;

    @Inject
    public ThingComponent(ThingService thingService) {
        this.thingService = thingService;
    }

}

I then built a small test to ensure that @Lazy and @Primary are all being respected.

public class ThingTest {

    @Test
    public void TestLazyAndPrimary() {

        // Arrange

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(ThingConfiguration.class);
        context.refresh();

        // Act

        ThingComponent component = context.getBean(ThingComponent.class);

        // Assert

        Assert.assertNotNull(component);
    }

}

However, when I run this test, I found that @Lazy was being ignored. The following text is emitted to the console:

PrimaryThing -- Initialized
OurThingService -- Initialized
YourThingService -- Initialized

The "YourThing" @Bean should not have been initialized, as it was @Lazy and not loaded at runtime via the ApplicationContext.getBean() method. Yet when the ThingComponent is resolved, it causes the @Bean methods with that return an implementation of ThingService to be hydrated before the @Primary mean is chosen.

How do I get the @Primary annotated implementation of an interface to be respected without causing all of the non-@Primary implementations annotated with @Lazy to be hydrated?

Upvotes: 0

Views: 786

Answers (1)

Technetium
Technetium

Reputation: 6158

I have been unable to stop the @Primary annotation from forcing eager hydration of all @Bean methods that return that interface, even though this information seems available without forcing hydration from the annotations in exclusivity. I got around this by using a naming convention on @Bean methods instead.

Specifically, I changed my @Primary annotated @Bean method to include a name like so:

@Configuration
@ComponentScan(basePackages = { "com.things" })
public class ThingConfiguration {

    // @Primary -- I don't want someone to accidentally use this without a @Qualifier!
    @Bean(name = "PrimaryThingService")
    public ThingService primaryThing(ThingOptions options, ApplicationContext context) {
        System.out.println("PrimaryThing -- Initialized");

        if (options.sharing) {
            return context.getBean("OurThing", ThingService.class);
        } else {
            return context.getBean("YourThing", ThingService.class);
        }
    }

    // ... the rest of the methods removed for clarity ...

}

Then I placed a @Qualifier on the ThingService being injected into the @Component like so:

@Component
public class ThingComponent {

    private final ThingService thingService;

    @Inject
    public ThingComponent(@Qualifier("PrimaryThingService") ThingService thingService) {
        this.thingService = thingService;
    }

}

Now when I rerun the test, I get the following output:

PrimaryThing -- Initialized
OurThingService -- Initialized

So this removes the @Primary annotation in place of using a named @Bean following a convention of "Primary{Interface}", stepping around the Spring's overeager hydration of non-@Primary annotated @Bean methods.

Upvotes: 1

Related Questions