Spring’s Constructor-Based Dependency Injection with Lombok

When the picture of the puzzle appears
  FR, EO
 4 min. to read
 chop
 No comment yet
 

In the past, we advised using the constructor to inject dependencies with Spring, and we introduced Lombok. Guess what? Those go quite well along together, except maybe for a little trick to known when you wish to use Spring’s @Value. We’ll cover all that in this post.

Generate the Injecting Constructor with Lombok #

Let’s make an immutable service that injects its dependencies through a constructor.

 1import org.springframework.stereotype.Service;
 2
 3@Service
 4public class Library {
 5    private final BooksDatabase booksDb;
 6    private final BorrowersDatabase borrowersDb;
 7
 8    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
 9        this.booksDb = booksDb;
10        this.borrowersDb = borrowersDb;
11    }
12}

If we apply what we showed with Lombok last time, a more concise way to do this would be:

1import lombok.RequiredArgsConstructor;
2import org.springframework.stereotype.Service;
3
4@Service
5@RequiredArgsConstructor
6public class Library {
7    private final BooksDatabase booksDb;
8    private final BorrowersDatabase borrowersDb;
9}

And we’re done! Plus, no maintenance needed of the constructor if we add or remove fields. Ain’t that a match made in heaven?

The Annoying Case of @Value #

Don’t Mix Up! #

Lombok has lombok.Value, Spring has org.springframework.beans.factory.annotation.Value. They don’t have the same purpose at all. In this post, we’ll focus on Spring’s @Value.

The Problem #

To use Spring’s constructor-based injection, everything needs to go through the constructor. If you need to inject configuration, you must be able to set the @Value on the parameters.

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5    private final int maxLendingDays;
 6
 7    public Library(
 8      BooksDatabase booksDb,
 9      BorrowersDatabase borrowersDb,
10      @Value("${library.lending.days.max}") maxLendingDays
11    ) {
12        this.booksDb = booksDb;
13        this.borrowersDb = borrowersDb;
14        this.maxLendingDays = maxLendingDays;
15    }
16}

However, since Lombok writes your constructor in your stead, how can you do that?

The Naive Solution #

When first confronted to this issue, the instinct of my team was to do that:

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private int maxLendingDays;
9}

But what happens in this case?

First, Lombok will generate a constructor taking an argument for both final fields, but not for the integer. The generated code would look like this:

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private int maxLendingDays;
 8
 9    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
10        this.booksDb = booksDb;
11        this.borrowersDb = borrowersDb;
12    }
13}

When Spring encounters this code, it creates the bean in two steps:

  1. Since a constructor is present, it uses it to inject the two concerned fields.
  2. It then uses reflection to set the non-final, @Value-annotaetd field. All of the gain of constructor-based injection is then lost.

The Real Solution #

We said Lombok’s behaviour can be customized with a lombok.config file. This is one of the times it gets very interesting.

You can for instance tell Lombok to copy certain annotations on the generated constructors’ parameters. Add the following line to your configuration file:

lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value

Then update your code thus:

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private final int maxLendingDays;
9}

The generated code will look like the following:

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private final int maxLendingDays;
 8
 9    public Library(
10      BooksDatabase booksDb,
11      BorrowersDatabase borrowersDb,
12      @Value("${library.lending.days.max}") maxLendingDays
13    ) {
14        this.booksDb = booksDb;
15        this.borrowersDb = borrowersDb;
16        this.maxLendingDays = maxLendingDays;
17    }
18}

My Other Tips #

@RequiredArgsConstructor vs @AllArgsConstructor #

I like @RequiredArgsConstructor because it lets me decide which fields should be passed to the constructor. Yet, in the case of a Spring bean, I think all fields should be initialized this way in 99% of cases, so using @AllArgsConstructor is a valid solution, and it saves some thinking time (“will this field be included in the constructor’s arguments?” Yes, it will!).

Adding @Autowired to a Generated Constructor #

You may wish your constructor to carry the @Autowired annotation. Through optional1, it may help devs unfamiliar with Spring to understand how the class is initialized during runtime, for instance. This can be set through your Lombok annotation:

1@Service
2@RequiredArgsConstructor(onConstructor = @__(@Autowired))
3public class Library {
4    // ...
5}

Conclusion #

I hope this short post will help you get rid of some of the boilerplate code that remained for the instanciation of your Spring beans.

Don’t hesitate to share tips that could help in such contexts.


  1. @Autowired on constructor is only required if the class has several constructors. In such cases, exactly one constructor should be thus annotated. ↩︎