by BehindJava

Smart Dependency Injection With Spring

Home » springboot » Smart Dependency Injection With Spring

This tutorial covers the basics of DI supported by the Spring Framework, including configuration types, injection variants, and how to inject different types.

Spring Framework is a very powerful framework and provides first-class support for dependency injection (DI). It contains a lot of features or ways to implement DI.

  • Basic usage of DI
  • DI with assignability
  • DI with generics

In This tutorial, You Will Learn:

  • What is dependency injection?
  • How to implement DI with Spring Framework
  • Which configuration types Spring Framework supports
  • Which variants of injection Spring Framework supports
  • What the injection rules are in Spring Framework
  • What bean types can be injected with Spring Framework
  • Several hints and gotchas for DI with Spring Framework

What Is Dependency Injection?

Dependency Injection (DI) is a well-known design pattern for a separation of concerns. It’s been used for some time. Wikipedia provides us with this definition:

Dependency injection separates the creation of a client’s dependencies from the client’s behavior, which promotes loosely coupled programs and the dependency inversion and single responsibility principles. Fundamentally, dependency injection is based on passing parameters to a method.

DI is an example of the more general concept known as Inversion of Control (IoC). The Hollywood principle is also often referred to as a synonym to DI.

Used Classes

Before we start with an explanation of DI, we should first define the used classes and their dependencies. We use EntityManager, UserRepository and UserService here. The EntityManager is defined by JPA, and the rest is defined by us. We can see their relationship depicted below.

ddd

Spring DI Example

We can use DI with Spring Framework in many ways (see the official documentation here). Before we get to that, I want to start with an easy example to demonstrate DI and shed some light on it.

Let’s say we have a UserRepository class for accessing user (mapped by a User class) data with JPA. The definition of the User class, EntityManager dependency, or the method implementation are not important here. They are out of the scope of this article.

The UserRepository class looks like this:

@Repository
public class UserRepository {
  
  @Autowired
  private EntityManager em;
	
  List<User> findAllUsers() { ... }

}

Besides a repository used by a persistent layer (in the UserRepository class), we also want a service layer for our business logic. This is realized in a UserService class which depends on an instance of the UserRepository class. The UserService class is defined as follows:

@Service
class UserService {

  @Autowired
  private UserRepository repository;
  
  public findAllUsers(){
    return repository.findUsers();
  } 
}

As you can see, we delegate injecting (also called wiring) of the UserRepository instance into the UserService instance to Spring Framework by using @Autowired annotation. That’s basically all that needs to be done. Once we have Spring Framework configured in our project, we need to define all dependencies (the injection points) and then let the Spring Framework do its job.

Configuration Types

The Spring Framework supports several ways to configure metadata. A very brief overview of every available configuration type is presented below.

Note: the configuration examples are not complete. Our concern is on the configuration of the beans and their dependencies. The configuration of Spring Framework or JPA (EntityManager) is out of the scope of this article (as already mentioned above).

XML-based Configuration

In the beginning, the Spring Framework supported just XML-based configuration. We can configure the userService bean with this approach as:

class UserService {

  private UserRepository repository;
  
  public setRepository(UserRepository userRepository){
    return this.repository = userRepository;
  }
  
  public findAllUsers(){
    return repository.findUsers();
  }
  
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.github.aha.sat.core.wiring.dummy.UserService">
        <property name="userRepository" ref="userRepository"></property>
    </bean>
  
    <bean id="userRepository" ... ></bean>
    <bean id="entityManager" ... ></bean>

</beans>

Note: we don’t need any configuration annotation in our classes as we see in the initial example. This is specified in the XML file.

Annotation-based Configuration

Very soon thereafter, the annotation-based configuration was introduced in order to provide a simpler way. This configuration type is used in the initial example above (see UserRepository or UserService classes). The wiring of the components is accomplished with the help of @Repository, @Service and @Autowired annotations provided by the Spring Framework.

Java-based Configuration

The newest type is the Java-based configuration. Here, we need the very same classes (without annotations) as for XML-based configuration. However, we configure our beans in Java itself instead of the XML file.

@Configuration
public class WiringConfig {

  @Bean
  public EntityManager entityManager() {
    return ...;
  }
  
  @Bean
  public UserRepository userRepository() {
    return new UserRepository(entityManager());
  }

  @Bean
  public UserService userService(UserRepository userRepository) {
    return new UserService(userRepository);
  }
}

Note: the configuration of EntityManager is not important here. Therefore, it’s skipped.

As you can see, we have two options to set the dependencies:

  1. To rely on the beans injected in a method argument (e.g. userService); or
  2. Call another method from the same configuration file to get the desired bean (e.g. the entityManager() method to get the entityManager bean).

Variants of Injection

Despite the configuration types (to define Spring beans and their dependencies), the Spring Framework also provides several variants of injection.

Field Injection

Probably the easiest is the field injection where we annotate directly a field of the class. This variant is used in the initial example above (see the repository field in the UserService class).

Constructor Injection

The next injection type is the Constructor Injection. This variant uses constructors instead of fields to inject the desired beans. We can see this approach demonstrated on UserService below. We just remove @Autowired annotation and add an appropriate constructor.

@Service
class UserService {

  private UserRepository repository;

  public UserService(UserRepository repository) {
    this.repository = repository;
  }
	
  ...
  
}

Note: the @Autowired annotation is not needed on the constructor when a single constructor is found. This feature is available since Spring Framework 4.3.

Setter Injection

The last injection variant is called the Setter Injection and is realized via setter methods. An example with this approach for UserService class looks like this:

@Service
class UserService {

  private UserRepository repository;

  @Autowired
  public void setRepository(UserRepository repository) {
    this.repository = repository;
  }
	
  ...
  
}

The Rules for Injection

The tricky part with DI is to define and configure our beans properly. The Spring Framework supports several ways to locate the correct bean (to be injected). Spring follows several rules to narrow the candidate list to match just a single bean. In precedence is from the last to the first, these rules are:

  • Type: Obviously, the first criteria to find any bean candidates to be injected is by their type. The injected bean instance should be assignable to the declared type. This rule is usually sufficient in many cases when we have specific beans.
  • Name: Sometimes we have several beans of the same type (e.g. RestTemplate). If the declared field is named as bean name (e.g. userRepository), then we don’t need to specify anything else (to define other @Primary or @Qualifier annotations).
  • @Primary: There are cases when we want to define the default bean when more beans are available (by their type). We can set the @Primary annotation on the bean class itself in order to make a bean the default one. Such beans are always preferred unless we override that with a qualifier.
  • @Qualifier: The last option with the highest precedence is the @Qualifier annotation. This annotation has to be defined on the bean class itself (to define the name of the bean), the injected type (to define the bean to be injected) or both.

Note: the examples for these rules will be demonstrated in the following article of this series in more detail (not a dummy code as here).

Injected Types

Except for the injection variants, we have several possibilities to inject dependencies (single bean, collection, or map of beans). See the official documentation about @Autowired annotation.

Single Bean

The easiest and probably most used option is to inject a single bean. We have seen this approach several times before (e.g. in UserRepository or UserService classes).

Collection of Beans

The next option is injecting several beans of the same type (as a collection or even an array). See the demonstration of both cases in the examples below:

lass RepositoryTest {
	
  @Autowired
  private Collection<Repository> allRepositories;
  
  ...
    
}
class RepositoryTest {
	
  @Autowired
  private Repository[] allRepositories;
  
  ...
    
}

Note: the usage of collection or array is equal. The only difference is the usage in our code.

Map of Beans

There are also situations when we need to know the names of the injected beans. In this case, we can inject the desired beans as a Map instead of the Collection.

class RepositoryTest {
	
  @Autowired
  private Map<String, Repository> repositoriesMap;
  
  ...
    
}

Note: the map entry key is defined as String and contains the bean name. The map entry value contains the bean instance.

Final Notes

The short theoretical overview of major DI features provided by Spring DI is complete. Of course, there are many details we have skipped. The next paragraphs present some of these details.

Required Attribute in @Autowired

By default, every injected bean is considered as mandatory, which means the injected bean instance must be found. However, there are cases when this is not the desired behavior (e.g. for plugin feature, usage of profiles, etc.).

In this case, we can set attribute required = false in @Autowired annotation. With this, the UserService bean is created without any UserRepository instance.

@Service
class UserService {

  @Autowired (required = false)
  private UserRepository repository;
  
  ...
  
}

JSR-330

The Spring Framework also supports the standard for Dependency Injection for Java specified in JSR-330. When we use this standard, then we can use @Named instead of @Component annotation and @Inject instead of @Autowired annotation. All we need to add is a dependency on javax.inject library (see the documentation here).

We can demonstrate the approach again on the UserService class.

@Named
class UserService {

  @Inject
  private UserRepository repository;
  
  ...
  
}

@Order Annotation

Sometimes when we need to prioritize our beans (e.g. injected into the collection) or processing (e.g. triggering listeners), we use @Order annotation. For these situations, see the documentation here.

Please, be aware that the lower values have higher priority. See the order usage here:

@Component
@Order(5)
class UserService {

  ...
  
}

Other Hints

Configuration
The configuration examples presented above are not complete or exhaustive (e.g. the setter injection via XML was skipped). We demonstrate here only the main approaches, but there are more possible combinations of configuration types and injection variants.

Bean Scope
The Spring Framework uses singleton as a default bean scope. However, we can use other scopes as well. In the case of injecting a bean with a different scope (e.g. prototype bean to be injected into the singleton bean), we need to use AOP proxy as explained here.

Note: JSR-330 has a different default scope. It’s similar to the prototype scope in the Spring Framework. Therefore, we need to be aware of such different approaches when injecting beans according to this specification.

Circular Dependencies
It’s quite easy to introduce circular dependency when creating small cohesive components/beans. The Spring Framework is capable of handling this in some injection variants, because the dependencies are injected in post-processing. The exception is constructor-based injection where a dependency resolution process fails due to the missing bean to be injected. This scenario can be mitigated by combining constructor and setter-based injection.

Note: The circular dependency issue indicates a poor design and should be avoided (and not just mitigated).

Preferred Approach

As we have seen, there are several approaches to use DI. Personally, I prefer constructor-based injection in the production code as it promotes:

  • Independence on the Spring Framework: there’s no Spring annotation.
  • Testability: all dependencies can be mocked easily.

However, I use field injection (with @Autowired) in tests, because it’s just the simplest option.

Note: as far as I know, the constructor-based injection is also recommended by the Spring community. The official recommendation (“the rule of thumb”) is to use constructor-based injection for mandatory dependencies and the rest for the optional.