44. Dependency Injection in Spring#

In software engineering, dependency injection (DI) is a technique in which an object receives other objects that it depends on. Together with its elaborate collection of annotations, dependency injection is the heart of the Spring framework.

We will be using the debugger to explore the concept of DI. The same project of the previous tutorial will be used to go through this tutorial. The previously used Bird class will also be used, with minor modifications (an ID field was added).

As example, a simple Bird “database” will be used as data layer to make available to the application.

This is the Bird class:

package nl.bioinf.model;

import java.util.Objects;

public class Bird {
    private Long id;
    private String name;
    private String status;

    public Bird(Long id, String name, String status) {
        this.id = Objects.requireNonNull(id);
        this.name = Objects.requireNonNull(name);
        this.status = status;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getStatus() {
        return status;
    }

    @Override
    public String toString() {
        return "Bird{" +
                "name='" + name + '\'' +
                ", status='" + status + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Bird bird = (Bird) o;

        return getId() != null ? getId().equals(bird.getId()) : bird.getId() == null;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }
}

Create a package named dao. In it, create an interface named BirdsRepository:

package nl.bioinf.dao;

import nl.bioinf.model.Bird;

public interface BirdsRepository {
    void add(Bird bird);
    Bird findByName(String name);
    Bird findById(Long id);
}

Create a sub-package of dao named dummy. In this package, create a class named BirdsRepositoryDummyImpl that implements the interface. It uses the Java Streams api that may be new to you. Try to figure out what is going on:

package nl.bioinf.dao.dummy;

//imports omitted

public class BirdsRepositoryDummyImpl implements BirdsRepository {
    private Map<Long, Bird> birds = new HashMap<>();

    @Override
    public void add(Bird bird) {
        Objects.requireNonNull(bird);
        this.birds.put(bird.getId(), bird);
    }

    @Override
    public Bird findByName(String name) {
        final List<Bird> found = this.birds
                .values()
                .stream()
                .filter(b -> b.getName().equals(name))
                .collect(Collectors.toList());
        assert(found.size() <= 1);
        if (found.isEmpty()) {
            return null;
        } else {
            return found.get(0);
        }
    }

    @Override
    public Bird findById(Long id) {
        if (this.birds.containsKey(id)) {
            return birds.get(id);
        } else {
            return null;
        }
    }
}

Note that returning null from a method is generally considered bad practice. You should use the Optional class for such cases. However, for simplicity’s sake I keep things as they are. Also note there are more sophisticated ways of “mocking” your data layer.

Let’s first create some tests to see whether it works.

package nl.bioinf.dao.dummy;

//imports omitted

class BirdsRepositoryDummyImplTest {
    private BirdsRepository birdsRepository;
    private Bird bird;

    @BeforeEach
    void setupDatabase() {
        this.birdsRepository = new BirdsRepositoryDummyImpl();
        this.bird = new Bird(1234L, "Steppe eagle", "extremely rare");
    }

    @Test
    void findByName_OK() {
        birdsRepository.add(this.bird);
        Bird found = birdsRepository.findByName("Steppe eagle");
        assertEquals(this.bird, found);
    }

    @Test
    void findByName_Null() {
        birdsRepository.add(this.bird);
        Bird found = birdsRepository.findByName("Tawny Eagle");
        assertNull(found);
    }

    @Test
    void findById() {
        birdsRepository.add(this.bird);
        Bird found = birdsRepository.findById(this.bird.getId());
        assertEquals(this.bird, found);
    }
}

It passes. Note that, when we want to test the application, we will want to run these tests no matter what the service layer implementation looks like. This is called an integration test, and it also uses Spring DI. We’ll return to good testing practices in the Spring framework later.

Now let’s consume the service. Here is a REST controller that we want to use to serve birds from the data layer:

package nl.bioinf.webcontrol;

//imports omitted

@RestController
@RequestMapping(value="/birds")
public class BirdsController {

    @GetMapping(value = "/{id}")
    public Bird getBirdById(@PathVariable(value="id") Long id) {
        //I need to get hold of a BirdsRepository implementation here

        throw new UnsupportedOperationException("not implemented yet");
    }
}

As you can see, in order to serve birds, a reference to a BirdsRepository implementation needs to be present. The BirdsController class has a dependency on a BirdsRepository implementation. There are several ways to satisfy this dependency. Whichever technique you choose, never make a class dependent on an implementation; always use abstractions.

The dependency could be passed into the constructor or into a setter. But which object will be responsible for passing that dependency? Alternatively, you may have learned patterns like these:

BirdsRepository repo = BirdsRepositoryDummyImpl.getInstance()
//OR, more loosely coupled and thus better
BirdsRepository repo = BirdsRepositoryFactory.getInstance();

Which makes the BirdsController class dependent on a factory class (or worse, a specific implementation). A general guideline in OO design is that you want to have as few dependencies as possible: loose coupling is the term for that (see wikipedia).

Supporting loose coupling is exactly what the Spring framework is build for: Wiring the objects that make up the backbone of your application with the objects they depend upon.

Let’s have a look at the Spring way of doing this.

First, we’ll add a @Component annotation to the BirdsRepositoryDummyImpl class.

@Component
public class BirdsRepositoryDummyImpl implements BirdsRepository {
    public BirdsRepositoryDummyImpl() {
        //add a single bird to be able to serve
        this.birds.put(1111L, new Bird(1111L, "Long-eared owl", "fairly common"));
    }
    //rest of code omitted

What this @Component annotation does is telling the Spring container that this class should be instantiated as a component of the application. Once you’ve done that, you should never take that responsibility back: never use new BirdsRepositoryDummyImpl() in your code. Spring is now responsible for that. Now, whenever your request an object of type BirdsRepository you will get the single instance of this class that Spring instantiates.

For testing purposes, I already add a single bird within the constructor. I know, this is not the official way to do this. But I want to stay focussed on the topic here, which is dependency injection.

Next, we’ll tell the Spring container we’ll be needing a component of type BirdsRepository, using the @Autowired annotation.

@RestController
@RequestMapping(value="/birds")
public class BirdsController {

    @Autowired
    private BirdsRepository birdsRepository;

    @GetMapping(value = "/{id}")
    public Bird getBirdById(@PathVariable(value="id") Long id) {
        return birdsRepository.findById(id);
    }
}

Go to the endpoint http://localhost:8080/birds/1111 and you should receive this:

{
    "id": 1111,
    "name": "Long-eared owl",
    "status": "fairly common"
}

I used the interface type, not the implementation, to get hold of a BirdsRepositoryDummyImpl instance. Spring will look up all implementations and if there is only one, it will instantiate that class. If there are more implementations (or none), it will throw an error. More on how to solve that later.

This is the simplest form of Autowiring; IntelliJ suggests it is not the best way. Alternatively, I could have used Autowiring of the constructor, like below. In some cases, autowiring a field gives null whereas autowiring the constructor works. See this blog post for a discussion on pros and cons of different types of DI.

@RestController
@RequestMapping(value="/birds")
public class BirdsController {

    private final BirdsRepository birdsRepository;

    @Autowired
    public BirdsController(BirdsRepository birdsRepository) {
        this.birdsRepository = birdsRepository;
    }

    @GetMapping(value = "/{id}")
    public Bird getBirdById(@PathVariable(value="id") Long id) {
        return birdsRepository.findById(id);
    }
}

Finally, I could also have used setter-based injection:

@RestController
@RequestMapping(value="/birds")
public class BirdsController {

    private BirdsRepository birdsRepository;

    @Autowired
    private void setBirdsRepository(BirdsRepository repo) {
        this.birdsRepository = repo;
    }

    @GetMapping(value = "/{id}")
    public Bird getBirdById(@PathVariable(value="id") Long id) {
        return birdsRepository.findById(id);
    }
}

Perhaps surprisingly, the setter can be made private.

This concludes the three ways of doing dependency injection using the spring framework.

Use this technique to build the backbone of your application. Once you’ve annotated a class as a @Component or other related ones such as @Service, @DataSource, never ever construct them yourself.

Note that this is primarily used for classes of which the application needs only a single instance; you are not likely going to annotate your data objects in this way.