@Service
@Primary
public class MainProductServiceImpl implements ProductService
{
// . . .
}
Introduction to dependency injection
29-06-2024 - Antonio Archilla
Table of Contents
Spring Dependency Injection capabilities allow us to manage bean bindings in different ways. Here is a basic guide on how to:
-
Having a single bean candidate and a single injection point, that is the base case when we define
a bean and use @Autowired
annotation to inject it
-
Having multiple bean candidates of the same type and a single injection point. Here we can define
a default bean or use identifiers to select which one will be injected
-
Having zero bean candidates for a type and a single injection point, which means that we can have
optional beans
-
Having multiple bean candidates of the same type and a multiple injection point, allowing accessing
all beans for the specified type in the same injection point
-
Having conditional bean candidates that are only available when a condition is met
-
Create and inject prototype scoped beans
Optional and multiple bindings
Primary bean
We can use Spring's @Primary
annotation to mark a specific bean to be the default candidate
to be injected in single injection points.
Definition:
Injection (Field):
@Service
public class Store
{
@Autowired
private ProductService defaultProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService productService;
public Store(ProductService productService) {
this.productService = productService;
}
// . . .
}
Qualifiers
Individual beans for a same type are still available if we use the @Qualifier
annotation.
In that case, we need to specify an identifier for the individual bean and use it at definition
and injection points.
Definition:
@Service
@Qualifier("ADDITIONAL_PRODUCT")
public class AdditionalProductServiceImpl implements ProductService
{
// . . .
}
Injection (Field):
@Service
public class Store
{
@Autowired
@Qualifier("ADDITIONAL_PRODUCT")
private ProductService additionalProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService additionalProductService;
public Store(@Qualifier("ADDITIONAL_PRODUCT") ProductService additionalProductService) {
this.additionalProductService = additionalProductService;
}
// . . .
}
Injecting Optional
, List
and ObjectProvider
In case we have no bean candidates for a type, it is required to use the required = false
flag in @Autowired
annotation
Injection (Field):
@Service
public class Store
{
@Autowired(required = false)
private ProductService defaultProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService productService;
public Store(@Autowired(required = false) ProductService productService) {
this.productService = productService;
}
// . . .
}
In that case the bean instance will be null
at runtime. We have to handle this every time that we want to use it in the code.
Alternatively, we can wrap the bean instance with an Optional
or ObjectProvider
so we can handle easily the null
case:
Injection of an Optional:
@Service
public class Store
{
@Autowired
private Optional<ProductService> productService;
// . . .
public Optional<String> getProductName() {
return productService
.map(ProductService::getProductName);
}
}
Injection of an ObjectProvider:
@Service
public class Store
{
@Autowired
private ObjectProvider<ProductService> productService;
// . . .
public Optional<String> getProductName() {
return productService.stream()
.findAny()
.map(ProductService::getProductName);
}
}
We can also use ObjectProvider
and a Collection
when we have multiple beans of the same type:
Injection of an ObjectProvider:
@Service
public class Store
{
@Autowired
private ObjectProvider<ProductService> productService;
// . . .
public List<String> getProductNames() {
return productService.stream()
.map(ProductService::getProductName)
.toList();
}
}
Injection of a List:
@Service
public class Store
{
@Autowired
private List<ProductService> productService;
// . . .
public List<String> getProductNames() {
return productService.stream()
.map(ProductService::getProductName)
.toList();
}
}
Conditional binding
Using conditional binding allow us to create a bean instance only if a condition is met. We can add a condition to any bean defined with a stereotype annotation (@Component
, @Service
, @Repository
, @Controller
), a @Configuration
or a @Bean
creation.
Spring-boot provides many built-in conditions such as:
-
Conditional on profile
Beans annotated with @Profile("<PROFILE_NAME>")
will only be available if the specified Spring profile is active.
@Service
@Profile("profile1")
public class StoreService {
// . . .
}
@Profile
annotation accepts also an expression involving multiple profiles and logical operations AND (&
), OR (|
) and NOT (!
)
@Service
@Profile("!profile1 & (profile2 | profile3)")
public class StoreService {
// . . .
}
Or an array of multiple profiles, resolved as a logical AND between the specified values
@Service
@Profile({ "profile1", "profile2" })
public class StoreService {
// . . .
}
-
Conditional on property
@ConditionalOnProperty
annotation allows to load beans conditionally depending on a certain environment property
@Service
@ConditionalOnProperty(
value = "store.service.enabled"
)
public class StoreService {
// . . .
}
-
Conditional on expression
If we have a logical expression involving more than one property, we can use @ConditionalOnExpression
annotation instead of @ConditionalOnProperty` and use Spring Expression Language (SpEL) to write the condition
@Service
@ConditionalOnExpression(
"${store.module.enabled:true} and ${store.service.enabled:true}"
)
public class StoreService {
// . . .
}
In that example, condition will evaluate to true if both some.module.enabled
and some.service.enabled
properties are true
, taking true
as default value if the property is not present.
-
Conditional on bean or on a missing bean
@ConditionalOnBean
annotation allows to load beans conditionally depending on the presence of a specific bean in the context
@Service
@ConditionalOnBean(ProductService.class)
public class ProductStoreService {
// . . .
}
Alternatively we can use @ConditionalOnMissingBean
to load when the required bean is missing
@Service
@ConditionalOnMissingBean(ProductService.class)
public class EmptyStoreService {
// . . .
}
Both annotations accept more than 1 bean. All specified beans must (not)exists so that the condition resolves to true
-
Conditional on class or on a missing class
@ConditionalOnClass
annotation allows to load beans conditionally depending on the presence of a specific class in classpath
@Service
@ConditionalOnClass("com.bitsmi.store.ProductService")
public class ProductStoreService {
// . . .
}
Alternatively we can use @ConditionalOnMissingClass
to load when the required class is missing
@Service
@ConditionalOnMissingClass("com.bitsmi.store.ProductService")
public class EmptyStoreService {
// . . .
}
Both annotations accept more than 1 class. All specified classes must (not)exists so that the condition resolves to true
-
Conditional on resource
@ConditionalOnResource
annotation allows to load beans conditionally depending on the presence of a specific resource on classpath
@Service
@ConditionalOnResource(
"logback.xml"
)
public class LogbackService {
// . . .
}
-
Conditional on Java version
@ConditionalOnJava
annotation allows to load beans conditionally only if running a certain version of Java
import org.springframework.boot.system.JavaVersion;
@Service
@ConditionalOnJava(JavaVersion.SEVENTEEN)
public class StoreServiceJava17Impl {
// . . .
}
Custom conditions
In addition to Spring's built-in conditions, we can create our own ones implementing Condition
interface:
import org.apache.commons.lang3.SystemUtils;
class OnUnixCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return SystemUtils.IS_OS_LINUX;
}
}
// . . .
@Bean
@Conditional(OnUnixCondition.class)
UnixService unixService() {
return new UnixService();
}
// . . .
In case of be necessary, we can create an annotation to provide additional data that will take part in the resolution of condition. We will know these annotations as @ConditionalOn…
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnSystemNameCondition.class)
public @interface ConditionalOnSystemName {
String[] value();
}
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;
class OnSystemNameCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean matches = false;
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ConditionalOnSystemName.class.getName());
if (attrs != null) {
// Annotation attribute values
String[] osNames = (String[])attrs.get("value");
matches = isAcceptedOs(osNames);
}
return matches;
}
// . . .
}
@Service
@ConditionalOnSystemName({ "Windows", "Linux" })
public class SystemDependantService {
// . . .
}
Combining conditions
We can specify multiple conditions that will be evaluated in order to resolve if the bean will be available.
If we want to achieve an OR
logic, we will have to create a new condition that extends AnyNestedCondition
and wraps individual conditions as nested classes
class OnTestOrDevProfileCondition extends AnyNestedCondition {
OnTestOrDevCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@Profile("DEV")
static class OnDev {}
@Profile("Test")
static class OnTest {}
}
@Service
@Conditional(OnTestOrDevProfileCondition.class)
public class DevOrTestService {
// . . .
}
The same approach can be followed to create a custom annotation combining multiple conditions in a single @ConditionalOn…
annotation using AND
logic, extending AllNestedConditions
class, or a NONE
logic extending NoneNestedCondition
class.
AND combined conditions
class OnTestingJava17Condition extends AllNestedCondition {
OnTestingJava8Condition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@Profile("TEST")
static class OnTest {}
@ConditionalOnJava(JavaVersion.SEVENTEEN)
static class OnJava17 {}
}
@Service
@Conditional(OnTestingJava17Condition.class)
public class Java17TestService {
// . . .
}
NONE combined conditions
class OnUnsupportedJavaVersionCondition extends NoneNestedCondition {
OnUnsupportedJavaVersionCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnJava(JavaVersion.SEVENTEEN)
static class OnJava17 {}
@ConditionalOnJava(JavaVersion.TWENTY_ONE)
static class OnJava21 {}
}
@Service
@Conditional(OnUnsupportedJavaVersionCondition.class)
public class UnsupportedJavaVersionTestService {
// . . .
}
By default, if we add multiple @ConditionalOn…
annotations to a bean, they will be combined using AND
logic
Note
As @Conditional
annotation cannot be specified multiple types in a class / method, we only use custom @ConditionalOn…
annotations
to achieve this (or one @Conditional
plus other @ConditionalOn…
for each additional condition)
Prototype scoped beans
Spring context allows us to get unique instances of the same bean every time we ask for it
when they are scoped as prototype beans.
This scope is specified using @Scope(BeanDefinition.SCOPE_PROTOTYPE)
in the bean definition along with
@Service
, @Component
, @Bean
, @Repository
annotations in bean definition:
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class PrototypeServiceImpl implements PrototypeService {
// . . .
}
@Configuration
public class ServiceConfig
{
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public PrototypeService simplePrototypeService()
{
return new PrototypeServiceImpl();
}
}
We can obtain an instance of them in multiple ways:
-
Using @Autowired
annotation. This will inject a unique instance that will not be shared in other injection points.
This example code injects instances of the same type for a prototype bean. As prototype beans are not shared
across multiple injections points, the injected services are different instances.
@Service
class ExampleService {
@Autowired
private PrototypeService serviceInstance1;
@Autowired
private PrototypeService serviceInstance2;
}
-
Using ObjectProvider
. We will obtain a new instance every time we call getObject
method. E.G:
import org.springframework.beans.factory.ObjectProvider;
@Service
class ExampleService {
@Autowired
private ObjectProvider<PrototypeService> prototypeServices;
public void doSomething() {
final PrototypeService instance = prototypeServices.getObject();
// . . .
}
}
-
Directly from Spring's ApplicationContext
, every time we call getBean()
method. E.G:
import org.springframework.context.ApplicationContext;
@Service
class ExampleService {
@Autowired
private ObjectProvider<PrototypeService> prototypeServices;
public void doSomething() {
final PrototypeService instance = applicationContext.getBean(PrototypeService.class);
// . . .
}
}
-
Using a custom factory. This also allows to parameterize bean creation:
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
@Configuration
public class ParameterizedServicePrototypeFactory
{
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public ParameterizedService get(String parameter)
{
return new ParameterizedServiceImpl(parameter);
}
}
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
@Service
class ExampleService {
@Autowired
private ParameterizedServicePrototypeFactory serviceFactory;
public void doSomething() {
final ParameterizedService actualInstance1 = parameterizedServicePrototypeFactory.get("A_PARAMETER");
// . . .
}
}
In this case we will not mark bean implementation with any stereotype like @Service
, @Component
, etc.
as the factory is in charge of create the bean:
public class ParameterizedServiceImpl implements ParameterizedService {
private final String name;
public ParameterizedServiceImpl(String name) {
this.name = name;
}
// . . .
}
FactoryBean
When the creation of a specific bean type requires a complex logic, we can encapsulate this in a FactoryBean
class
and delegate the creation of the bean to it. For that we must define a class implementing the FactoryBean<T>
interface of
extending the class AbstractFactoryBean<T>
, being T
the type of the bean to create:
import org.springframework.beans.factory.config.AbstractFactoryBean;
public class ComplexServiceFactoryBean extends AbstractFactoryBean<ComplexService> {
private final DependencyService dependencyService;
private String configurationValue;
public ComplexServiceFactoryBean(DependencyService dependencyService) {
this.dependencyService = dependencyService;
// Singleton by default. Set `false` to create prototype instances
// setSingleton(false);
}
public ComplexServiceFactoryBean setConfigurationValue(String configurationValue) {
this.configurationValue = configurationValue;
return this;
}
@Override
public Class<?> getObjectType() {
return ComplexService.class;
}
@Override
protected ComplexService createInstance() throws Exception {
return new ComplexServiceImpl(dependencyService)
.setConfigurationValue(configurationValue);
}
}
And then configure the FactoryBean
as a bean using the name that will we assigned to the final bean, complexService
in the example:
@Configuration
public class FactoryBeanConfig {
@Bean("complexService")
public ComplexServiceFactoryBean complexServiceFactory(DependencyService dependencyService) {
return new ComplexServiceFactoryBean(dependencyService)
.setConfigurationValue("configuration_value");
}
}
After that, we will be capable of injecting bean instances that will be singletons
of prototypes
depending on the configuration
made in the FactoryBean
(see setSingleton
method):
@Autowired
private ComplexService complexService;
@Autowired
private ComplexServiceFactoryBean complexServiceFactoryBean;
// . . .
ComplexService serviceFromFactory = complexServiceFactoryBean.getObject();
Using the bean name configured in the @Bean
annotation of the interface, we will also be able of retrieving
bean instance and its related FactoryBean
through ApplicationContext
@Autowired
private ApplicationContext context;
// . . .
// It is needed to add a '&' to the bean name to retrieve factory instance and then the bean instance through the getObjectMethod
ComplexService localService1 = ((ComplexServiceFactoryBean) context.getBean("&complexService")).getObject();
// And use the bean name directly to retrieve the bean instance
ComplexService localService2 = (ComplexService) context.getBean("complexService");
Spring Dependency Injection capabilities allow us to manage bean bindings in different ways. Here is a basic guide on how to:
-
Having a single bean candidate and a single injection point, that is the base case when we define a bean and use
@Autowired
annotation to inject it -
Having multiple bean candidates of the same type and a single injection point. Here we can define a default bean or use identifiers to select which one will be injected
-
Having zero bean candidates for a type and a single injection point, which means that we can have optional beans
-
Having multiple bean candidates of the same type and a multiple injection point, allowing accessing all beans for the specified type in the same injection point
-
Having conditional bean candidates that are only available when a condition is met
-
Create and inject prototype scoped beans
Optional and multiple bindings
Primary bean
We can use Spring's @Primary
annotation to mark a specific bean to be the default candidate
to be injected in single injection points.
Definition:
Injection (Field):
@Service
public class Store
{
@Autowired
private ProductService defaultProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService productService;
public Store(ProductService productService) {
this.productService = productService;
}
// . . .
}
Qualifiers
Individual beans for a same type are still available if we use the @Qualifier
annotation.
In that case, we need to specify an identifier for the individual bean and use it at definition
and injection points.
Definition:
@Service
@Qualifier("ADDITIONAL_PRODUCT")
public class AdditionalProductServiceImpl implements ProductService
{
// . . .
}
Injection (Field):
@Service
public class Store
{
@Autowired
@Qualifier("ADDITIONAL_PRODUCT")
private ProductService additionalProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService additionalProductService;
public Store(@Qualifier("ADDITIONAL_PRODUCT") ProductService additionalProductService) {
this.additionalProductService = additionalProductService;
}
// . . .
}
Injecting Optional
, List
and ObjectProvider
In case we have no bean candidates for a type, it is required to use the required = false
flag in @Autowired
annotation
Injection (Field):
@Service
public class Store
{
@Autowired(required = false)
private ProductService defaultProductService;
// . . .
}
Injection (Constructor):
@Service
public class Store
{
private final ProductService productService;
public Store(@Autowired(required = false) ProductService productService) {
this.productService = productService;
}
// . . .
}
In that case the bean instance will be null
at runtime. We have to handle this every time that we want to use it in the code.
Alternatively, we can wrap the bean instance with an Optional
or ObjectProvider
so we can handle easily the null
case:
Injection of an Optional:
@Service
public class Store
{
@Autowired
private Optional<ProductService> productService;
// . . .
public Optional<String> getProductName() {
return productService
.map(ProductService::getProductName);
}
}
Injection of an ObjectProvider:
@Service
public class Store
{
@Autowired
private ObjectProvider<ProductService> productService;
// . . .
public Optional<String> getProductName() {
return productService.stream()
.findAny()
.map(ProductService::getProductName);
}
}
We can also use ObjectProvider
and a Collection
when we have multiple beans of the same type:
Injection of an ObjectProvider:
@Service
public class Store
{
@Autowired
private ObjectProvider<ProductService> productService;
// . . .
public List<String> getProductNames() {
return productService.stream()
.map(ProductService::getProductName)
.toList();
}
}
Injection of a List:
@Service
public class Store
{
@Autowired
private List<ProductService> productService;
// . . .
public List<String> getProductNames() {
return productService.stream()
.map(ProductService::getProductName)
.toList();
}
}
Conditional binding
Using conditional binding allow us to create a bean instance only if a condition is met. We can add a condition to any bean defined with a stereotype annotation (@Component
, @Service
, @Repository
, @Controller
), a @Configuration
or a @Bean
creation.
Spring-boot provides many built-in conditions such as:
-
Conditional on profile
Beans annotated with
@Profile("<PROFILE_NAME>")
will only be available if the specified Spring profile is active.@Service @Profile("profile1") public class StoreService { // . . . }
@Profile
annotation accepts also an expression involving multiple profiles and logical operations AND (&
), OR (|
) and NOT (!
)@Service @Profile("!profile1 & (profile2 | profile3)") public class StoreService { // . . . }
Or an array of multiple profiles, resolved as a logical AND between the specified values
@Service @Profile({ "profile1", "profile2" }) public class StoreService { // . . . }
-
Conditional on property
@ConditionalOnProperty
annotation allows to load beans conditionally depending on a certain environment property@Service @ConditionalOnProperty( value = "store.service.enabled" ) public class StoreService { // . . . }
-
Conditional on expression
If we have a logical expression involving more than one property, we can use
@ConditionalOnExpression
annotation instead of @ConditionalOnProperty` and use Spring Expression Language (SpEL) to write the condition@Service @ConditionalOnExpression( "${store.module.enabled:true} and ${store.service.enabled:true}" ) public class StoreService { // . . . }
In that example, condition will evaluate to true if both
some.module.enabled
andsome.service.enabled
properties aretrue
, takingtrue
as default value if the property is not present. -
Conditional on bean or on a missing bean
@ConditionalOnBean
annotation allows to load beans conditionally depending on the presence of a specific bean in the context@Service @ConditionalOnBean(ProductService.class) public class ProductStoreService { // . . . }
Alternatively we can use
@ConditionalOnMissingBean
to load when the required bean is missing@Service @ConditionalOnMissingBean(ProductService.class) public class EmptyStoreService { // . . . }
Both annotations accept more than 1 bean. All specified beans must (not)exists so that the condition resolves to
true
-
Conditional on class or on a missing class
@ConditionalOnClass
annotation allows to load beans conditionally depending on the presence of a specific class in classpath@Service @ConditionalOnClass("com.bitsmi.store.ProductService") public class ProductStoreService { // . . . }
Alternatively we can use
@ConditionalOnMissingClass
to load when the required class is missing@Service @ConditionalOnMissingClass("com.bitsmi.store.ProductService") public class EmptyStoreService { // . . . }
Both annotations accept more than 1 class. All specified classes must (not)exists so that the condition resolves to
true
-
Conditional on resource
@ConditionalOnResource
annotation allows to load beans conditionally depending on the presence of a specific resource on classpath@Service @ConditionalOnResource( "logback.xml" ) public class LogbackService { // . . . }
-
Conditional on Java version
@ConditionalOnJava
annotation allows to load beans conditionally only if running a certain version of Javaimport org.springframework.boot.system.JavaVersion; @Service @ConditionalOnJava(JavaVersion.SEVENTEEN) public class StoreServiceJava17Impl { // . . . }
Custom conditions
In addition to Spring's built-in conditions, we can create our own ones implementing Condition
interface:
import org.apache.commons.lang3.SystemUtils;
class OnUnixCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return SystemUtils.IS_OS_LINUX;
}
}
// . . .
@Bean
@Conditional(OnUnixCondition.class)
UnixService unixService() {
return new UnixService();
}
// . . .
In case of be necessary, we can create an annotation to provide additional data that will take part in the resolution of condition. We will know these annotations as @ConditionalOn…
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnSystemNameCondition.class)
public @interface ConditionalOnSystemName {
String[] value();
}
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;
class OnSystemNameCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean matches = false;
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ConditionalOnSystemName.class.getName());
if (attrs != null) {
// Annotation attribute values
String[] osNames = (String[])attrs.get("value");
matches = isAcceptedOs(osNames);
}
return matches;
}
// . . .
}
@Service
@ConditionalOnSystemName({ "Windows", "Linux" })
public class SystemDependantService {
// . . .
}
Combining conditions
We can specify multiple conditions that will be evaluated in order to resolve if the bean will be available.
If we want to achieve an OR
logic, we will have to create a new condition that extends AnyNestedCondition
and wraps individual conditions as nested classes
class OnTestOrDevProfileCondition extends AnyNestedCondition {
OnTestOrDevCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@Profile("DEV")
static class OnDev {}
@Profile("Test")
static class OnTest {}
}
@Service
@Conditional(OnTestOrDevProfileCondition.class)
public class DevOrTestService {
// . . .
}
The same approach can be followed to create a custom annotation combining multiple conditions in a single @ConditionalOn…
annotation using AND
logic, extending AllNestedConditions
class, or a NONE
logic extending NoneNestedCondition
class.
AND combined conditions
class OnTestingJava17Condition extends AllNestedCondition {
OnTestingJava8Condition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@Profile("TEST")
static class OnTest {}
@ConditionalOnJava(JavaVersion.SEVENTEEN)
static class OnJava17 {}
}
@Service
@Conditional(OnTestingJava17Condition.class)
public class Java17TestService {
// . . .
}
NONE combined conditions
class OnUnsupportedJavaVersionCondition extends NoneNestedCondition {
OnUnsupportedJavaVersionCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnJava(JavaVersion.SEVENTEEN)
static class OnJava17 {}
@ConditionalOnJava(JavaVersion.TWENTY_ONE)
static class OnJava21 {}
}
@Service
@Conditional(OnUnsupportedJavaVersionCondition.class)
public class UnsupportedJavaVersionTestService {
// . . .
}
By default, if we add multiple @ConditionalOn…
annotations to a bean, they will be combined using AND
logic
Note
|
As |
Prototype scoped beans
Spring context allows us to get unique instances of the same bean every time we ask for it when they are scoped as prototype beans.
This scope is specified using @Scope(BeanDefinition.SCOPE_PROTOTYPE)
in the bean definition along with
@Service
, @Component
, @Bean
, @Repository
annotations in bean definition:
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class PrototypeServiceImpl implements PrototypeService {
// . . .
}
@Configuration
public class ServiceConfig
{
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public PrototypeService simplePrototypeService()
{
return new PrototypeServiceImpl();
}
}
We can obtain an instance of them in multiple ways:
-
Using
@Autowired
annotation. This will inject a unique instance that will not be shared in other injection points.This example code injects instances of the same type for a prototype bean. As prototype beans are not shared across multiple injections points, the injected services are different instances.
@Service class ExampleService { @Autowired private PrototypeService serviceInstance1; @Autowired private PrototypeService serviceInstance2; }
-
Using
ObjectProvider
. We will obtain a new instance every time we callgetObject
method. E.G:import org.springframework.beans.factory.ObjectProvider; @Service class ExampleService { @Autowired private ObjectProvider<PrototypeService> prototypeServices; public void doSomething() { final PrototypeService instance = prototypeServices.getObject(); // . . . } }
-
Directly from Spring's
ApplicationContext
, every time we callgetBean()
method. E.G:import org.springframework.context.ApplicationContext; @Service class ExampleService { @Autowired private ObjectProvider<PrototypeService> prototypeServices; public void doSomething() { final PrototypeService instance = applicationContext.getBean(PrototypeService.class); // . . . } }
-
Using a custom factory. This also allows to parameterize bean creation:
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Scope; @Configuration public class ParameterizedServicePrototypeFactory { @Bean @Scope(BeanDefinition.SCOPE_PROTOTYPE) public ParameterizedService get(String parameter) { return new ParameterizedServiceImpl(parameter); } }
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Scope; @Service class ExampleService { @Autowired private ParameterizedServicePrototypeFactory serviceFactory; public void doSomething() { final ParameterizedService actualInstance1 = parameterizedServicePrototypeFactory.get("A_PARAMETER"); // . . . } }
In this case we will not mark bean implementation with any stereotype like
@Service
,@Component
, etc. as the factory is in charge of create the bean:public class ParameterizedServiceImpl implements ParameterizedService { private final String name; public ParameterizedServiceImpl(String name) { this.name = name; } // . . . }
FactoryBean
When the creation of a specific bean type requires a complex logic, we can encapsulate this in a FactoryBean
class
and delegate the creation of the bean to it. For that we must define a class implementing the FactoryBean<T>
interface of
extending the class AbstractFactoryBean<T>
, being T
the type of the bean to create:
import org.springframework.beans.factory.config.AbstractFactoryBean;
public class ComplexServiceFactoryBean extends AbstractFactoryBean<ComplexService> {
private final DependencyService dependencyService;
private String configurationValue;
public ComplexServiceFactoryBean(DependencyService dependencyService) {
this.dependencyService = dependencyService;
// Singleton by default. Set `false` to create prototype instances
// setSingleton(false);
}
public ComplexServiceFactoryBean setConfigurationValue(String configurationValue) {
this.configurationValue = configurationValue;
return this;
}
@Override
public Class<?> getObjectType() {
return ComplexService.class;
}
@Override
protected ComplexService createInstance() throws Exception {
return new ComplexServiceImpl(dependencyService)
.setConfigurationValue(configurationValue);
}
}
And then configure the FactoryBean
as a bean using the name that will we assigned to the final bean, complexService
in the example:
@Configuration
public class FactoryBeanConfig {
@Bean("complexService")
public ComplexServiceFactoryBean complexServiceFactory(DependencyService dependencyService) {
return new ComplexServiceFactoryBean(dependencyService)
.setConfigurationValue("configuration_value");
}
}
After that, we will be capable of injecting bean instances that will be singletons
of prototypes
depending on the configuration
made in the FactoryBean
(see setSingleton
method):
@Autowired
private ComplexService complexService;
@Autowired
private ComplexServiceFactoryBean complexServiceFactoryBean;
// . . .
ComplexService serviceFromFactory = complexServiceFactoryBean.getObject();
Using the bean name configured in the @Bean
annotation of the interface, we will also be able of retrieving
bean instance and its related FactoryBean
through ApplicationContext
@Autowired
private ApplicationContext context;
// . . .
// It is needed to add a '&' to the bean name to retrieve factory instance and then the bean instance through the getObjectMethod
ComplexService localService1 = ((ComplexServiceFactoryBean) context.getBean("&complexService")).getObject();
// And use the bean name directly to retrieve the bean instance
ComplexService localService2 = (ComplexService) context.getBean("complexService");