Reputation: 6158
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
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