Using Spring’s @Value annotation with Lombok

I worked several times on Spring Boot applications that use Lombok, a famous code-generation tool based on annotation processing. In this article, I will discuss using Spring’s @Value annotation in a “component” class annotated with Lombok.

Premises

First of all, I want to clarify that this is not an introductory article on Lombok. So, I won’t explain how to install Lombok in your IDE or how to use the essential Lombok annotations. I expect the reader already has some, even minor, experience with Lombok.

Instead, this article focuses on how to use Spring’s @Value annotation in classes annotated with Lombok annotations. @Value is a Spring annotation you can apply to fields or method/constructor parameters to inject a value using two different expression types, Spring Expression Language (#{……}) or property placeholders (${……}).

In real applications, property placeholders are typically used to inject values from configuration files (like the application.properties and variants in Spring Boot). This latter is precisely the case covered by this article.

Case 1: the most essential “properties” bean class

Consider, for example, the following introductory “properties” bean class:

// ✔️ This works correctly
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.Data;

@Data
@Component
public class AppProperties {
    @Value("${app.config1}")
    private String config1;

    @Value("${app.config2}")
    private int config2;
}

AppProperties becomes a Spring “bean” since it is annotated with @Component. And it is also a “data” class, as Lombok’s @Data annotation expresses. Spring can inject configuration values through reflection, and then you can access these values using accessor methods generated by Lombok (getConfig1(), etc.).

This version of AppProperties works correctly and is reasonably valid and appropriate in many basic scenarios. However, it has a significant drawback. Indeed, AppProperties is a mutable class since the @Data annotation means that Lombok generates both getter and setter methods. If you inject the AppProperties bean into another component, that component can change the configured values, which may not be good.

In the following sections, we will see what we can do to solve this issue.

Case 2: a “properties” bean class with only getters

As a first step, we may consider replacing the @Data annotation with the combination of @Getter, @ToString, and @EqualsAndHashCode annotations to solve the mutability issue. Note that @ToString and @EqualsAndHashCode are not strictly required if you don’t need to use these standard methods.

// ✔️ This works correctly
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
@EqualsAndHashCode
@Component
public class AppProperties {
    @Value("${app.config1}")
    private String config1;

    @Value("${app.config2}")
    private int config2;
}

This class also works perfectly since Spring can still inject values through reflection on fields. However, in this case, there are only getter methods since Lombok doesn’t generate any setter method. This version of AppProperties is better than the previous one because you cannot easily change the configured values (unless you “play” with the reflection API 😉).

Someone may argue that this version is not truly “immutable”. Indeed, the developer can add setter methods later, explicitly or using Lombok. So the question now is, can we make an immutable properties class? Yes, however, we have to use constructor injection.

Case 3: an immutable “properties” bean class

Firstly, it’s essential to understand that simply replacing @Data with Lombok’s @Value is insufficient. The Lombok’s @Value annotation is the “immutable” counterpart of @Data, and it modifies the class in the following ways:

  • marks all fields as private, and final
  • does not generate setter methods
  • marks the class as final
  • generates an all-args constructor

First attempt (not working)

// ❌ This will NOT work
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@lombok.Value
@Component
public class AppProperties {
    @Value("${app.config1}")
    private String config1;     // made final by @lombok.Value

    @Value("${app.config2}")
    private int config2;        // made final by @lombok.Value
}

The above version of AppProperties will not work. I have only shown this code to clarify that this is not the right solution. If you try to run a Spring Boot application with such a class, the Spring context will fail to start. You should see a failure message that is similar to this:

Parameter 0 of constructor in somepackage.AppProperties required a bean of type 'java.lang.String' that could not be found.

The problem is that Spring’s @Value annotation is only present on fields, not on constructor parameters. One way to solve this issue is by defining an explicit constructor with parameters annotated by Spring’s @Value annotation, like in the following second attempt.

Second attempt (good)

// ✔️ This works correctly
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@lombok.Value
@Component
public class AppProperties {
    private String config1;     // made final by @lombok.Value
    private int config2;        // made final by @lombok.Value

    public AppProperties(
            @Value("${app.config1}") String config1,
            @Value("${app.config2}") int config2) {
        this.config1 = config1;
        this.config2 = config2;
    }
}

This latter version works perfectly, and the class is truly immutable. However, while my example is simple and fictitious with only two fields, you may have a more realistic class with 10, 20, or more property fields. Thus, writing and maintaining an explicit constructor with many parameters is awkward and boring. And it also defeats the usefulness of Lombok. But don’t worry, Lombok can even solve this!

Lombok provides an exciting and valuable feature. It can “copy” annotations from fields to constructor parameters, setter parameters, and getter methods. Lombok already knows about some standard annotations like javax.annotation.Nonnull and others. However, it doesn’t know anything about Spring’s @Value annotation. Simply stated, it is sufficient to configure Lombok appropriately to copy the @Value annotation.

You can configure Lombok with a file named lombok.config that you can place in the same package of the AppProperties source file, in a parent package/folder, or in your project’s main folder (where you have pom.xml or build.gradle).

Final attempt (optimum)

So, let’s create the file lombok.config with the following content:

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

With this configuration, the Spring’s @Value annotation is added to the set of “copyable” annotations (note the += sign, which is essential). The AppProperties class can remain like the following, with no explicit constructor:

// ✔️ This now works correctly using the lombok.config
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@lombok.Value
@Component
public class AppProperties {
    @Value("${app.config1}")
    private String config1;

    @Value("${app.config2}")
    private int config2;
}

This is the final solution to have a “properties” bean class that is easy to write, concise, and (most importantly) immutable. I have personally used this solution in a couple of work projects without any issues.

If you try to “delombok” this latter AppProperties class (check the Delombok page for details), you should see something like this:

// Result of Delombok on AppProperties.java

@Component
public final class AppProperties {
    @Value("${app.config1}")
    private final String config1;
    @Value("${app.config2}")
    private final int config2;

    @java.lang.SuppressWarnings("all")
    public AppProperties(@Value("${app.config1}") final String config1, @Value("${app.config2}") final int config2) {
        this.config1 = config1;
        this.config2 = config2;
    }

    @Value("${app.config1}")
    @java.lang.SuppressWarnings("all")
    public String getConfig1() {
        return this.config1;
    }

    @Value("${app.config2}")
    @java.lang.SuppressWarnings("all")
    public int getConfig2() {
        return this.config2;
    }

    // ......other methods generated by Lombok
}

You can notice that Lombok generated the all-args constructor with parameters annotated by the Spring’s @Value annotation copied from fields. Lombok also placed the @Value annotation on getter methods, which is generally harmless because Spring does not care about a @Value on getters. Furthermore, the developer typically only calls those getter methods programmatically to get the values.

Conclusions

In this article, I have shown various ways to use Spring’s @Value annotation in a bean class annotated with one or more Lombok annotations.

If you have a simple application or don’t care too much about immutability, you can create a simple mutable bean, as in case 1. If you have more interest in immutability, you can create a bean with only getter methods, as in case 2, or a truly immutable bean, as shown in the last attempt of case 3.

It all depends on your specific requirements.

Similar Posts