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
- Case 1: the most essential “properties” bean class
- Case 2: a “properties” bean class with only getters
- Case 3: an immutable “properties” bean class
- Conclusions
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
, andfinal
- 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.