MapStruct Debug in Microservice

1. Configuration in Microservice

MapStruct is configured in the parent pom.xml to standardize object mapping across all backend services.

  • dependencyManagement — Defines the version centrally. Sub-modules can choose to use MapStruct by adding the dependency (without specifying version).
  • annotationProcessorPaths in build plugins — Runs annotation processors during compilation to generate mapper implementation classes (e.g., MemberMapperImpl.java). This applies to all sub-modules automatically.

Parent pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.5.5.Final</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.10.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <!-- Lombok MUST come FIRST -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>1.18.30</version>
                    </path>
                    <!-- MapStruct SECOND -->
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                    <!-- Binding (recommended if using @Builder) -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Sub-Module pom.xml

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

Why Parent vs Sub-Module Split?

Section Purpose Where
<dependencyManagement> Defines version only, does NOT add dependency Parent only
<dependencies> Actually adds to classpath Sub-module
<annotationProcessorPaths> Runs processors during compilation Parent only

Benefits:

  • Change version in one place → all modules updated
  • Consistent versions across all microservices
  • Centralized annotation processor configuration

2. Understanding annotationProcessorPaths

What Each Part Does

Configuration What It Provides
<dependency>mapstruct</dependency> @Mapper, @Mapping annotations (compile-time API)
<annotationProcessorPaths>mapstruct-processor Generates MemberMapperImpl.java
<annotationProcessorPaths>lombok Generates getters/setters from @Data
<annotationProcessorPaths>lombok-mapstruct-binding Helps MapStruct understand @Builder

Without mapstruct-processor in Build Plugin

If you only have mapstruct in dependencies but NOT in annotationProcessorPaths:

target/generated-sources/annotations/
└── (empty)   ✗ No MemberMapperImpl.java generated!

Result at runtime:

No bean of type 'MemberMapper' found

Why Lombok is in annotationProcessorPaths

Lombok is not required by MapStruct itself. It’s there because your DTOs use @Data:

@Data  // Lombok generates getEmail(), setEmail()
public class MemberDTO {
    private String email;  // No manual getter/setter
}

Both run at compile time. If Lombok doesn’t run, MapStruct can’t find getEmail().

Why Order Matters

Compile Time Order:
1. Lombok runs FIRST  → generates getEmail(), setEmail()
2. MapStruct runs SECOND → sees getEmail(), generates mapper code

Wrong order:
1. MapStruct runs FIRST → "Where is getEmail()?" → FAILS
2. Lombok runs SECOND → Too late!

3. lombok-mapstruct-binding Explained

When You Need It

Your DTO Uses Need Binding?
@Data only No
@Data + @Builder Yes
@Data + @SuperBuilder Yes
@Data + @AllArgsConstructor + @NoArgsConstructor No

What It Does

Without binding, MapStruct doesn’t know how to use Lombok’s @Builder:

@Data
@Builder
public class MemberDTO {
    private String email;
}

With binding, MapStruct generates:

// Generated MemberMapperImpl.java
public MemberDTO memberToMemberDTO(Member member) {
    return MemberDTO.builder()
        .email(member.getEmail())
        .build();
}

Why Your Code Works Without It

If your DTO has both @Builder AND @NoArgsConstructor:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor  // ← MapStruct uses this instead
public class MemberDTO { }

MapStruct uses the no-arg constructor + setters, so binding isn’t strictly required. But adding it is safer for future changes.


4. Private Variable Declaration on DTO and Entity

All fields in DTOs and entities should be declared private to enforce encapsulation and maintain JavaBean standards.

Use Lombok to generate getters/setters:

@Data
public class MemberDTO {
    private UUID id;
    private String email;
    // ... other fields
}

@Entity
@Data
public class Member {
    @Id
    private UUID id;
    private String email;
    // ... other fields
}

5. MapStruct Potential Bug to Access Private Member Value

Problem Description

MapStruct may silently fail or generate incomplete mapping code if:

  • Fields are private
  • No getters/setters are found
  • Annotation processing is disabled or misconfigured
  • Lombok runs AFTER MapStruct (wrong order)

How to Debug Mapping Issues

Step 1: Open the Autogenerated Mapper Implementation

Navigate to:

/target/generated-sources/annotations/...

Visually confirm whether the generated class uses:

member.setEmail(memberDTO.getEmail());  // Correct

Or fails with direct field access like:

member.email = memberDTO.email;  // WRONG - will fail for private fields

Step 2: Manually Add Getters/Setters if Needed

If @Data or @Getter/@Setter does not work due to annotation processing issues, define them manually in both MemberDTO and Member.

Step 3: Rebuild Project

./mvnw clean compile

Or IntelliJ’s Build > Rebuild Project with annotation processing enabled.

Step 4: Verify Generated Code Exists

ls target/generated-sources/annotations/com/your/package/mapper/
# Should see: MemberMapperImpl.java

Step 5: Write Proper Test Code

@Test
void testMemberDTOtoMember() {
    MemberDTO dto = MemberDTO.builder().email("test@example.com").build();
    Member member = memberMapper.memberDTOtoMember(dto);
    assertEquals("test@example.com", member.getEmail());
}

This ensures that future source code changes do not silently break mapping logic.


6. Recommended Final Configuration

<annotationProcessorPaths>
    <!-- 1. Lombok FIRST - generates getters/setters -->
    <path>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </path>
    <!-- 2. MapStruct SECOND - generates mapper implementations -->
    <path>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>${org.mapstruct.version}</version>
    </path>
    <!-- 3. Binding LAST - bridges Lombok @Builder with MapStruct -->
    <path>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok-mapstruct-binding</artifactId>
        <version>0.2.0</version>
    </path>
</annotationProcessorPaths>

Leave a Comment

Your email address will not be published. Required fields are marked *