Backend Development 17 min read

MapStruct and Lombok Integration: Maven Dependency Order and Advanced Usage

The article explains that when using MapStruct with Lombok, Maven must list Lombok before MapStruct (or define annotationProcessorPaths) so Lombok’s annotation processor runs first, and then demonstrates basic and advanced mapping techniques, Spring integration, and singleton caching strategies.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
MapStruct and Lombok Integration: Maven Dependency Order and Advanced Usage

MapStruct is a compile‑time code generator that simplifies Java bean mapping. Compared with BeanUtils, it generates type‑safe mapping code during compilation, which brings higher performance and early error detection.

The article explains why the order of Maven dependencies matters when MapStruct is used together with Lombok. Because both libraries rely on annotation processors, the processor that generates Lombok getters/setters must run before MapStruct’s processor; otherwise MapStruct cannot see the generated fields.

Correct Maven dependency order

When Lombok is declared before MapStruct (or when using <annotationProcessorPaths> ), MapStruct works correctly. Example:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.2.Final</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>

Alternatively, declare the processor explicitly inside <annotationProcessorPaths> of the Maven Compiler Plugin; the order defined there is guaranteed.

Basic conversion example

Source and target classes:

@Data
@Builder
public class Source {
    private Long id;
    private Long age;
    private String userNick;
}

@Data
public class Target {
    private Long id;
    private Long age;
    private String userNick;
}

Mapper interface:

@Mapper
public abstract class Converter {
    public static final Converter INSTANT = Mappers.getMapper(Converter.class);
    public abstract Target convert(Source source);
}

Usage:

Source src = Source.builder().id(1L).age(18L).userNick("Nick").build();
Target tgt = Converter.INSTANT.convert(src);
System.out.println(tgt); // Target(id=1, age=18, userNick=Nick)

Advanced mappings

Field name or type differences can be handled with @Mapping annotations:

@Mapper
public abstract class Converter {
    public static final Converter INSTANT = Mappers.getMapper(Converter.class);

    @Mapping(source = "age", target = "age", resultType = Integer.class)
    @Mapping(source = "userNick", target = "nick")
    public abstract Target convert(Source source);
}

One‑to‑many JSON field conversion using a custom method:

@Mapper
public abstract class Converter {
    @Mapping(target = "extra", source = "vo", qualifiedByName = "convertToExtra")
    public abstract DTO convert(VO vo);

    @Named("convertToExtra")
    public String convertToExtra(VO vo) {
        return String.format("%s,%s", vo.getAge(), vo.getUserNick());
    }
}

Sub‑class field mapping (flattening):

@Mapper
public abstract class Converter {
    @Mapping(target = "config.age", source = "age")
    @Mapping(target = "config.userNick", source = "userNick")
    public abstract DTO convertToDTO(VO source);

    @Mapping(target = "age", source = "config.age")
    @Mapping(target = "userNick", source = "config.userNick")
    public abstract VO convertToVO(DTO dto);
}

Spring integration

By setting componentModel = MappingConstants.ComponentModel.SPRING , the mapper becomes a Spring bean and can be injected:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class Converter {
    public abstract Target convert(Source source);
}

@RestController
public class MainController {
    @Resource
    private Converter converter;

    @GetMapping("/")
    public boolean test() {
        Source src = Source.builder().id(1L).age(18L).userNick("nick").build();
        Target tgt = converter.convert(src);
        System.out.println(tgt);
        return true;
    }
}

Caching in a singleton mapper

When a mapper is a singleton, a ThreadLocal buffer can avoid repeated parsing of complex fields:

@Mapper
public abstract class Converter {
    public static final Converter INSTANT = Mappers.getMapper(Converter.class);
    private final ThreadLocal
extraFieldBufferLocal = new ThreadLocal<>();

    @Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
    @Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
    public abstract VO convertToVO(DTO dto);

    @Named("extractAge")
    public Long extractAge(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extra.split(","));
        }
        return Long.valueOf(extraFieldBufferLocal.get()[0]);
    }

    @Named("extractUserNick")
    public String extractUserNick(String extra) {
        if (extraFieldBufferLocal.get() == null) {
            extraFieldBufferLocal.set(extra.split(","));
        }
        return extraFieldBufferLocal.get()[1];
    }
}

Why Maven respects the order

If <annotationProcessorPaths> is not used, Maven builds the classpath in the same order as the <dependencies> section, and the annotation‑processor discovery follows that order. When the path is defined, Maven uses the explicit order, guaranteeing that Lombok runs before MapStruct.

The article concludes with three practical rules:

Without <annotationProcessorPaths> , the classpath order equals the dependency order, so place Lombok before MapStruct.

If MapStruct appears before Lombok, its processor cannot see Lombok‑generated members, leading to missing mappings.

Using <annotationProcessorPaths> removes the need to remember the order, because Maven will always execute Lombok first.

JavaSpringmavenMapStructannotation-processingLombok
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.