Skip to content

Advanced dependency injection

Cyclical dependencies

Using the Dependency Injection mechanism, we can accidentally introduce the so-called cyclical relationship, i.e. one that causes some components which usually cannot be created to be established as a component that cannot be created in any other way. This problem can be introduced with the help of two classes (eg class A requires class B, and class B requires class A).

However, the following example is a bit more complicated as it uses three classes:

  • A, which requires class C to create
  • B, which requires class A to create
  • C, which requires class B to create
import org.springframework.stereotype.Component;

@Component
public class ClassAThatRequiresC {

  private final ClassCThatRequiresB classCThatRequiresB;

  public ClassAThatRequiresC(final ClassCThatRequiresB classCThatRequiresB) {
    this.classCThatRequiresB = classCThatRequiresB;
  }
}
import org.springframework.stereotype.Component;

@Component
public class ClassBThatRequiresA {

  private final ClassAThatRequiresC classAThatRequiresC;

  public ClassBThatRequiresA(final ClassAThatRequiresC classAThatRequiresC) {
    this.classAThatRequiresC = classAThatRequiresC;
  }
}
import org.springframework.stereotype.Component;

@Component
public class ClassCThatRequiresB {

  private final ClassBThatRequiresA classBThatRequiresA;

  public ClassCThatRequiresB(final ClassBThatRequiresA classBThatRequiresA) {
    this.classBThatRequiresA = classBThatRequiresA;
  }
}

In the event that such a problem occurs, the application will not start with the appropriate message. In this case:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  classAThatRequiresC defined in file [...ClassAThatRequiresC.class]
↑     ↓
|  classCThatRequiresB defined in file [...ClassCThatRequiresB.class]
↑     ↓
|  classBThatRequiresA defined in file [...ClassBThatRequiresA.class]
└─────┘

Most often, the solution to such a problem is to create an additional component or components for which we extract the logic needed to "break" the cycle.

Interfaces

In this section, the methods of dependency injection have been presented. In the examples shown there, simple classes were defined that were directly injected into the components that required them. In real applications, the injected classes may implement some interface. Spring also allows you to inject an interface implementation if that implementation is in context.

The following example shows how dependency injection is done:

public interface SimpleLogger {

  void printMessage(String message);
}
import org.springframework.stereotype.Component;

// a context-registered component implementing the interface that will be injected
@Component 
public class SimpleConsoleLogger implements SimpleLogger {

  @Override
  public void printMessage(final String message) {
    System.out.println("Hello from component that implements interface");
  }
}
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class HelloWorldLogger implements CommandLineRunner {

  private final SimpleLogger simpleLogger;

  // injecting through a constructor using an interface
  public HelloWorldLogger(final SimpleLogger simpleLogger) {
    this.simpleLogger = simpleLogger;
  }

  @Override
  public void run(final String... args) throws Exception {
    simpleLogger.printMessage("Hello from command line runner");
  }
}

Component names

The dependency injection mechanism in Spring is based on the names of components of their injection. By default, when using stereotypes, the class name becomes the component name, but its first letter is always lowercase (ie the camelCase convention is used). It is also possible to give the component a different name, using the value field in the annotation.

E.g:

  • the class ExampleSpringComponent, marked with the annotation@Service, will have the name exampleSpringComponent
  • class AnotherSpringComponent marked with the annotation @Component("customName")will have the name given in the annotation, i.e.customName

When creating beans using the @Bean annotation, the bean name is the name of the method that was used to create it. Hence, these names most often do not meet all good rules method names. As in the case of stereotypes, also when using the @Bean annotation, it is possible to indicate the bean name (using thename field). The following example shows a name change like this:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SimpleWebConfiguration {

  @Bean(name = "sdaObjectMapper") // the name used is "sdaObjectMapper". Overwrite the default name - "objectMapper"
  public ObjectMapper objectMapper() {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return objectMapper;
  }

}

Injecting by name

Since Spring allows dependency injection using the interface it implements, it may happen that injection of a specific implementation may not be possible because there are several beans in the context that implement such an interface. In this case, we will get an error and the application will not start. Another example shows this problem:

public interface SimpleLogger {
  void printMessage(String message);
}
import org.springframework.stereotype.Component;

@Component
public class SimpleConsoleLogger implements SimpleLogger {

  @Override
  public void printMessage(final String message) {
    System.out.println("Hello from component that implements interface");
  }
}
@Slf4j
@Component
public class SimpleLombokLogger implements SimpleLogger {

  @Override
  public void printMessage(final String message) {
    log.info("Hello from Lombok Logger: " + message);
  }
}
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class HelloWorldLogger implements CommandLineRunner {

  private final SimpleLogger simpleLogger;

  // injecting through a constructor using an interface
  public HelloWorldLogger(final SimpleLogger simpleLogger) {
    this.simpleLogger = simpleLogger;
  }

  @Override
  public void run(final String... args) throws Exception {
    simpleLogger.printMessage("Hello from command line runner");
  }
}

If you try to run such an application, we will get the following error:

Description:

Parameter 0 of constructor in pl.sdacademy.sb.HelloWorldLogger required a single bean, but 2 were found:
    - simpleConsoleLogger: defined in file [...classes\pl\sdacademy\sb\SimpleConsoleLogger.class]
    - simpleLombokLogger: defined in file [...\classes\pl\sdacademy\sb\SimpleLombokLogger.class]

We can solve the above problem using the annotation:

  • @Qualifier
  • @Primary

Using @Qualifier

The @Qualifier annotation is used to indicate the [name] (# component-name) of the injected component. We use it with the injected object, i.e .:

  • when we inject the object using a constructor or setter
  • above the injected field i.e. in case of injecting directly into the field

The problem shown in the previous [example] (#name-injection) can be solved by modifying the HelloWorldLogger class:

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class HelloWorldLogger implements CommandLineRunner {

  private final SimpleLogger simpleLogger;

  // injecting through a constructor using an interface and @Qualifier annotation
  public HelloWorldLogger(@Qualifier("simpleLombokLogger") final SimpleLogger simpleLogger) {
    this.simpleLogger = simpleLogger;
  }

  @Override
  public void run(final String... args) throws Exception {
    simpleLogger.printMessage("Hello from command line runner");
  }
}

NOTE: IntelliJ Ultimate will underline the name used inside the Qualifier annotation when using the name of a non-existent bean. There are however projects that create beans in a very specific way. In this case, the application will function properly, although some IDEs may indicate a problem.

Primary beans

If you have many beans of the same type in the context, you can add one of them with the annotation @Primary. In a situation where there are many beans of a given type (i.e. implementing a specific interface) and the @Qualifier annotation is not used, the one marked as the main will be selected. The following example shows this behavior:

public interface SimpleLogger {
  void printMessage(String message);
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@Slf4j
public class SimpleLoggersConfiguration {

  @Bean
  @Primary
  public SimpleLogger verySimpleLogger() {
    return System.out::println;
  }

  @Bean
  public SimpleLogger lombokLogger() {
    return log::info;
  }
}
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class HelloWorldLogger implements CommandLineRunner {

  private final SimpleLogger simpleLogger;

  // verySimpleLogger will be injected
  public HelloWorldLogger(final SimpleLogger simpleLogger) {
    this.simpleLogger = simpleLogger;
  }

  @Override
  public void run(final String... args) throws Exception {
    simpleLogger.printMessage("Hello from command line runner");
  }
}

Injecting into a method

There are times when in order to create one bean with the @Bean annotation, we need another. In this case, we can inject such a bean, taking it as an argument of the method (we do not need to use any annotation), e.g .:

public interface SimpleLogger {
  void printMessage(String message);
}
import org.springframework.stereotype.Component;

import java.text.DateFormat;

@Component
public class DateFormatProvider {

  public DateFormat get() {
    return DateFormat.getDateInstance();
  }
}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SimpleLoggerConfiguration {

  @Bean // this method injects a dependent bean necessary to create an ObjectMapper object
  public ObjectMapper objectMapper(final DateFormatProvider dateFormatProvider) {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setDateFormat(dateFormatProvider.get());
    return objectMapper;
  }
}

_NOTE: _ Spring provides the ability to inject (many) objects into many specific methods. These methods are most often described in the documentation for a specific project.