Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › Spring

Dependency injection

What does dependency injection do for you?

Dependency injection means that a class does not create the objects it needs to do its work, but receives them from outside. Here OrderService needs a PayService object to make a payment, and this is called "OrderService depends on PayService". In Spring, all you do is write PayService as a parameter of the OrderService constructor, and the @Component above a class is a mark that tells Spring to create an object (a bean) of that class and keep it. When Spring creates the OrderService object, it puts the PayService bean it was keeping into that constructor. In this lab you change the constructor to new PayService() or remove it, remove @Component above PayService, and run the same main, to see where the PayService that OrderService uses came from and how many PayService objects are created.

Checked against real execution · Compiled 6 combinations with real component scanning on Spring Framework 7.0.9 and ran them, measured even the objects in the fields by reflection, and checked against the results

Concepts in this lab

Dependency injection (DI)
This is an approach in which an object does not create the other objects it uses (its dependencies) but receives them from outside. In Spring, the container supplies the dependencies through a constructor, a setter, or a field as it creates a bean. It becomes easier to change the implementing class or to put in a fake object in a test, and the coupling between classes becomes lower.
Constructor injection, setter injection, and field injection
These are three ways to supply a dependency. Constructor injection receives the dependencies when the object is created, so the fields can be final and an object is never created with a dependency missing; Spring recommends it. Setter injection is used for optional dependencies. Field injection puts @Autowired on a field; the code is short, but it is hard to create the object without Spring in order to test it.
Dependency
A dependency is an object of another class that a class needs to do its work. For example, if OrderService calls a method of a PayService object to make a payment, we say that OrderService depends on PayService, and the PayService object is a dependency of OrderService. The Spring documentation describes a dependency as the other objects that an object works with. You can create the object you need yourself with new, or receive it as a constructor parameter, and when Spring creates it and puts it in for you, this is called dependency injection.
@Component
@Component is an annotation that you put above a class so that Spring knows the class and treats it as something it creates objects of and manages (a bean). With component scanning, Spring scans the classes under a specified package, finds the classes with @Component, and creates objects of those classes and keeps them in the container. If you do not give the bean a name, the class name with its first letter in lowercase is used (greeter for Greeter). @Repository, @Service, and @Controller are specializations of @Component, so they are found in the same way. A class without this annotation is not found even if it is in the same package, so if you ask Spring for an object of that class, an exception saying that there is no such bean is thrown.

Try these

Running with the default settings

  1. The first line of the main method runs. new AnnotationConfigApplicationContext("app") starts Spring. The "app" in the parentheses is the name of the package where Spring looks for classes, and Spring prepares a place to keep the objects it creates. An object that Spring creates and keeps is called a bean.
  2. Spring scans the classes in the app package and looks for classes with @Component. It is on PayService and OrderService, so Spring notes down that it will create and keep objects of these two classes.
  3. Spring is about to create the OrderService object. It looks at the constructor OrderService(PayService payService) and sees that it receives PayService as a parameter. So Spring finds out that OrderService needs a PayService object to do its work. This is called "OrderService depends on PayService".
  4. To create OrderService, a PayService object has to exist first, so Spring creates a PayService object first and keeps it. This object is the PayService bean. The number of PayService objects created is 1.
  5. Spring creates the OrderService object. It puts the PayService bean it was keeping into the payService parameter of the OrderService constructor, and the line this.payService = payService; in the constructor makes the field point to that object. The code we wrote has neither new OrderService(...) nor new PayService(). Spring putting in the object that is needed like this is dependency injection.
  6. The line that creates the context ends without an error, and the next line of main, context.getBean(OrderService.class), runs. Spring gives main the OrderService bean it was keeping. The object was already created in the earlier line, and no new object was created in this line.
  7. The orderService.order() method runs. order() calls payService.pay(), and this payService is the PayService bean that Spring injected. The pay() method returns "Payment complete".
  8. System.out.println prints the text it received, "Payment complete".

Result of this run

Code

@Component
public class PayService {
    public String pay() {
        return "Payment complete";
    }
}

@Component
public class OrderService {
    private final PayService payService;

    // Injection: if you write the object you need as a constructor parameter, Spring puts it in
    public OrderService(PayService payService) {
        this.payService = payService;
    }

    public String order() {
        return payService.pay();
    }
}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext("app");
        OrderService orderService = context.getBean(OrderService.class);
        System.out.println(orderService.order());
    }
}

Assumptions of this model

These are the results of running Spring Framework 7.0.9 on JDK 21.0.12.1. Spring Boot was not used, and only the component scanning of AnnotationConfigApplicationContext("app"), which the main on the screen uses as it is, was used. All 6 combinations were checked by compiling a source with the same code as on the screen (PayService, OrderService, and Main in the package app) with javac (-parameters) and running it as a child Java process. Components were not registered by hand, and Spring was made to scan the classes and find them. Only in the source used for checking, a Probe class and four lines were added. One line each was added to the two classes, an instance initializer block that reads from the stack which side called its constructor (Spring, or which constructor of OrderService) when an object is created and records it, and to main, one line each that measures after the context is created and after getBean; the checking script removes these lines when it compares with the code on the screen. What was checked is the result of the run (the output, the kind and cause of the exception when startup failed, and the exception that stopped it and where), the PayService that OrderService uses, and the number of PayService objects created. "The PayService that OrderService uses" was measured, not guessed. During the run, the payService field of the OrderService object was read by reflection, and it was compared with the PayService bean in the context to see whether it is the same object (==) and whether it is empty. The numbers of objects were also counted by the number of times the constructors were called. The facts that the step sentences rely on were also measured separately: the classes that component scanning found; the order in which the objects were created (with constructor injection PayService first, and with direct creation PayService is created while OrderService is being created); that all objects are created at the line that creates the context and no new object is created at the getBean line; the dependencies of OrderService that Spring recorded (payService only when it is received in the constructor); that when there is no PayService bean, OrderService is not created and main stops at its first line; that the place where the program stops using an empty field is the order() method of OrderService; and that our code has no line new OrderService(...). The actual exception in the combination where startup failed is UnsatisfiedDependencyException with the cause NoSuchBeanDefinitionException (no bean of type PayService), and the original text was kept in the Probe output. What the documentation specifies is the following. The Dependency Injection section of the Core part of the Spring Framework documentation (dependency injection is a process in which objects define the other objects they depend on only through constructor arguments and so on, and the container injects them when it creates the bean; and constructor-based injection is the container calling a constructor with arguments that each represent a dependency), the Using @Autowired section (if the target bean has only one constructor, @Autowired does not have to be put on that constructor), the Classpath Scanning and Managed Components section, and the API documentation of @Component and AnnotationConfigApplicationContext (a class with @Component is a candidate for auto-detection, and the specified package is scanned and bean definitions are registered). The following behaviors seen in the same run are not defined by the documentation but were observed on this version. In the order in which the class files are scanned, OrderService came before PayService, and because of that, with constructor injection, PayService was created first while OrderService was being created; with direct creation by new, Spring creates OrderService first and the PayService bean after it (this depends on the scan order, so it could change if the class names change); and Spring does not report an empty field as an error. The code on the screen leaves out the package and import lines. This lab covers only constructor injection. Setter injection, field injection (@Autowired), the case where there are several beans of the same type, circular dependencies, the scope and lifecycle of beans, and designs that receive an interface are not covered, and they are covered in the in-depth lab "Bean creation and injection".

Previous lab: Beans · Next lab: AOP and proxies

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these