Gradle dependencies order matters!
Table of Content
Why Lombok and MapStruct Dependency Order is Important
Gradle helps manage your project’s dependencies. However, the order in which you declare these dependencies can impact how they work together, especially for tools like Lombok and MapStruct.
During the build process, Gradle runs special tools called annotation processors that help generate extra code based on your annotations.
Compilation Stages
- Source Code Scanning:
The compiler scans your project’s source files to understand the structure of your code, preparing it for further processing. - Annotation Processing:
Gradle runs annotation processors, which analyze your annotations (like@Getter
from Lombok or@Mapper
from MapStruct) and generate additional code. - Compiling Generated Code:
The compiler combines your original code and the newly generated code, converting them into bytecode, which can run on the Java Virtual Machine (JVM).
Why the Order of annotationProcessor Matters
Gradle runs annotation processors in the same order they appear in your build.gradle
file.
Consider this example:
dependencies {
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final")
annotationProcessor("org.projectlombok:lombok:1.18.34")
}
What Happens When MapStruct Runs First?
- MapStruct runs first: It tries to generate mappers by accessing methods like
getId()
andgetUserName()
, but these methods are not yet available because Lombok hasn’t generated them. - Compilation Error: When MapStruct tries to create the mapper, it looks for methods like
getId()
andgetUserName()
. But since Lombok hasn’t generated them yet, MapStruct gets confused and can’t do its job.
When MapStruct runs before Lombok, you might encounter an error like this during compilation:
warning: Unmapped target properties: "id, userName".
UserDTO mapToUserDto(User user);
and generated class as:
public class UserMapperImpl implements UserMapper {
@Override
public UserDTO mapToUserDto(User user) {
if ( user == null ) {
return null;
}
// only initialization with null values
Long id = null;
String userName = null;
UserDTO userDTO = new UserDTO( id, userName );
return userDTO;
}
}
Lombok must finish building its parts (like
getters
andsetters
) before MapStruct can build the final product. If MapStruct starts too early, it won't have all the necessary pieces.
The Correct Order
To avoid this issue, you should declare your dependencies in the following order:
dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.34")
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final")
}
In this case:
- Lombok runs first, generating the necessary methods.
- MapStruct then uses the generated code to create mappers successfully.
And generated implementation is correct:
public class UserMapperImpl implements UserMapper {
@Override
public UserDTO mapToUserDto(User user) {
if ( user == null ) {
return null;
}
Long id = null;
String userName = null;
// Lombok-generated getters will be used here
id = user.getId();
userName = user.getUserName();
UserDTO userDTO = new UserDTO( id, userName );
return userDTO;
}
}
How to Verify annotationProcessor Order
You can check the order of annotation processors using the following command:
gradle dependencies --configuration annotationProcessor
This command will list all annotation processors in the order they are passed to the compiler:
annotationProcessor - Annotation processors and their dependencies for source set 'main'.
+--- org.projectlombok:lombok:1.18.34
\--- org.mapstruct:mapstruct-processor:1.5.5.Final
This shows that Lombok runs before MapStruct, ensuring everything works correctly.
Conclusion
Remember, the right dependency order isn’t just a best practice — it’s the secret sauce that keeps your build process running smoothly, especially when using libraries like Lombok and MapStruct together.
Declare Lombok first, then MapStruct and your code will work as expected.
Have you faced similar issues with annotation processors? Share your experience in the comments!