Introduction
Do you think about compatibility of your public API when you modify classes from it? It is especially easy to miss out that something incompatibly changed when you are using
Lombok. If you use
AllArgsConstructor
annotation it will cause many problems.
What is the problem?
Let's define simple class with
AllArgsConstructor
:
@Data
@AllArgsConstructor
public class Person {
private final String firstName;
private final String lastName;
private Integer age;
}
Now we can use generated constructor in spock test:
def 'use generated allArgsConstructor'() {
when:
Person p = new Person('John', 'Smith', 30)
then:
with(p) {
firstName == 'John'
lastName == 'Smith'
age == 30
}
}
And the test is green.
Let's add new optional field to our Person class -
email
:
@Data
@AllArgsConstructor
public class Person {
private final String firstName;
private final String lastName;
private Integer age;
private String email;
}
Adding optional field is considered compatible change. But our test fails...
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.github.alien11689.allargsconstructor.Person(java.lang.String, java.lang.String, java.lang.Integer)
How to solve this problem?
After adding field add previous constructor
If you still want to use
AllArgsConstructor
you have to ensure compatibility by adding previous version of constructor on your own:
@Data
@AllArgsConstructor
public class Person {
private final String firstName;
private final String lastName;
private Integer age;
private String email;
public Person(String firstName, String lastName, Integer age) {
this(firstName, lastName, age, null);
}
}
And now our test again passes.
Annotation lombok.Data
is enough
If you use only
Data
annotation, then constructor, with only mandatory (
final
) fields, will be generated. It is because
Data
implies
RequiredArgsConstructor
:
@Data
public class Person {
private final String firstName;
private final String lastName;
private Integer age;
}
class PersonTest extends Specification {
def 'use generated requiredFieldConstructor'() {
when:
Person p = new Person('John', 'Smith')
p.age = 30
then:
with(p) {
firstName == 'John'
lastName == 'Smith'
age == 30
}
}
}
After adding new field
email
test still passes.
Use Builder
annotation
Annotation
Builder
generates for us
PersonBuilder
class which helps us create new
Person
:
@Data
@Builder
public class Person {
private final String firstName;
private final String lastName;
private Integer age;
}
class PersonTest extends Specification {
def 'use builder'() {
when:
Person p = Person.builder()
.firstName('John')
.lastName('Smith')
.age(30).build()
then:
with(p) {
firstName == 'John'
lastName == 'Smith'
age == 30
}
}
}
After adding email field test still passes.
Conclusion
If you use
AllArgsConstructor
you have to be sure what are you doing and know issues related to its compatibility. In my opinion the best option is not to use this annotation at all and instead stay with
Data
or
Builder
annotation.
Sources are available
here.