欢迎你来读这篇博客,这篇博客主要是关于 Lombok、Java Record、MapStruct、MapStructPlus 的系统梳理。
这几个技术点看起来都在解决“Java 代码太啰嗦”的问题,但它们解决的问题其实并不一样:
Lombok:减少 JavaBean、构造器、Builder、日志对象等样板代码。
Record:Java 语言层面对“数据载体”的建模能力。
MapStruct:编译期生成对象转换代码,解决 DTO、VO、Entity、BO 之间的字段映射。
MapStructPlus:在 MapStruct 基础上进一步减少 Mapper 接口编写成本,提供统一转换入口。
如果把 Java 项目比作厨房,Lombok 是自动切菜机,Record 是标准餐盒,MapStruct 是配菜师傅,MapStructPlus 是自动配菜流水线。都好用,但别拿自动切菜机去炒菜,也别拿餐盒去当锅。
序言 在 Spring Boot 后端开发中,我们经常会遇到大量类似的对象:
Entity:数据库实体。
DTO:接口入参或远程调用对象。
VO:接口返回对象。
BO:业务内部对象。
Command:写操作命令对象。
Query:查询参数对象。
Event:事件消息对象。
Response:统一响应对象。
这些对象字段高度相似,却又不完全一样。于是项目中会出现大量重复代码:
1 2 3 4 5 6 UserVO vo = new UserVO (); vo.setId(entity.getId()); vo.setUsername(entity.getUsername()); vo.setNickname(entity.getNickname()); vo.setCreateTime(entity.getCreateTime());return vo;
一开始手写没什么问题,但项目大了以后,这种代码会变成“对象转换沼泽”:
字段多,容易漏。
字段改名,编译不一定报错。
复制粘贴多,维护成本高。
BeanUtils 反射转换虽然省事,但类型不安全,性能和可维护性都一般。
多层对象、枚举、时间格式、集合转换一多,代码就开始发麻。
所以我们需要一套比较清晰的组合方案:
Lombok 负责减少普通类的样板代码。
Record 负责表达不可变、纯数据的数据载体。
MapStruct 负责严谨、可控、类型安全的对象转换。
MapStructPlus 负责简单转换场景下的进一步自动化。
这篇文章会先把 Lombok 和 Record 讲透,再讲 MapStruct 和 MapStructPlus,最后给出它们在 Spring Boot 项目中的整合实践。
正文 第一章:Lombok 详解 1.1 Lombok 是什么 Lombok 是一个 Java 编译期增强工具,它通过注解处理器在编译阶段生成代码,从而减少 getter、setter、构造器、toString、equals、hashCode、builder 等样板代码。
普通 Java 写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class UserDTO { private Long id; private String username; private String nickname; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getNickname () { return nickname; } public void setNickname (String nickname) { this .nickname = nickname; } }
使用 Lombok 后:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserDTO { private Long id; private String username; private String nickname; }
Lombok 的价值非常直接:减少重复代码,让开发者把注意力放到业务语义上。
但它也有一个很现实的问题:Lombok 会让源码和编译后的真实代码不一致。源码里没有 getter、setter,但编译后确实存在这些方法。所以团队使用 Lombok 时,必须统一 IDE 插件、编译配置、注解处理器配置,否则就会出现“我这里能编译,你那里红一片”的经典名场面。
1.2 Lombok 的工作原理 Lombok 的核心工作发生在编译期。
简单理解:
Java 编译器读取源码。
Lombok 注解处理器发现类上、字段上、方法上的 Lombok 注解。
Lombok 修改抽象语法树。
编译器继续编译修改后的代码。
最终生成的 class 文件里包含 getter、setter、构造器等方法。
所以 Lombok 不是运行时反射工具,它不会在程序运行时动态生成 getter、setter。它的成本主要发生在编译期。
这也是为什么 Lombok 和 MapStruct 搭配时需要特别注意注解处理器顺序。MapStruct 需要看到 Lombok 生成的 getter、setter、builder 等方法,才能正确生成映射代码。
1.3 Lombok 常用注解总览 Lombok 常用注解可以分成几类:
1.3.1 Getter / Setter 相关 1 2 3 4 5 6 7 8 @Getter @Setter public class User { private Long id; private String username; }
也可以只作用在字段上:
1 2 3 4 5 6 7 8 public class User { @Getter private Long id; @Setter private String username; }
推荐写法:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserDTO { private Long id; private String username; private String nickname; }
不推荐为了偷懒直接所有地方都上 @Data。@Data 虽然方便,但它会同时生成 getter、setter、toString、equals、hashCode、RequiredArgsConstructor,在 Entity、复杂领域对象上很容易埋坑。
1.4 @Data:方便,但不要滥用 @Data 等价于组合使用:
@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
示例:
1 2 3 4 5 6 7 8 9 @Data public class UserDTO { private Long id; private String username; private String nickname; }
它适合简单 DTO、简单配置对象、临时数据对象。
但是不建议在 JPA Entity、MyBatis 复杂实体、领域聚合根上无脑使用。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 @Data @Entity public class UserEntity { @Id private Long id; private String username; @OneToMany(mappedBy = "user") private List<OrderEntity> orders; }
这类写法可能带来几个问题:
toString() 可能触发懒加载。
双向关联可能导致循环调用。
equals()、hashCode() 如果包含关联字段,可能出现性能问题或逻辑错误。
实体对象的相等性通常不应该简单用所有字段判断。
setter 全开放会削弱领域对象的封装性。
更推荐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Entity public class UserEntity { @Id private Long id; private String username; private String nickname; }
如果确实需要 equals/hashCode,也应该明确指定字段:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class UserEntity { @EqualsAndHashCode .Include private Long id; private String username; }
结论很简单:@Data 很爽,但不是银弹。爽文能看,系统不能全靠爽文。
1.5 @Builder:构建复杂对象 当对象字段较多时,构造器会变得非常难读:
1 User user = new User (1L , "mario" , "超级马里奥" , 18 , "ADMIN" , true );
使用 @Builder 后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Getter @Builder public class User { private Long id; private String username; private String nickname; private Integer age; private String role; private Boolean enabled; }
创建对象:
1 2 3 4 5 6 7 8 User user = User.builder() .id(1L ) .username("mario" ) .nickname("超级马里奥" ) .age(18 ) .role("ADMIN" ) .enabled(true ) .build();
这种写法可读性更好,也更适合测试代码、DTO 构建、命令对象构建。
@Builder.Default需要注意,如果字段有默认值:
1 2 3 4 5 6 @Getter @Builder public class User { private Boolean enabled = true ; }
使用 builder 构建时,这个默认值可能不会按你想象的方式生效。应该写成:
1 2 3 4 5 6 7 @Getter @Builder public class User { @Builder .Default private Boolean enabled = true ; }
toBuilder如果希望基于已有对象复制并修改:
1 2 3 4 5 6 7 8 9 10 @Getter @Builder(toBuilder = true) public class User { private Long id; private String username; private String nickname; }
使用:
1 2 3 User newUser = oldUser.toBuilder() .nickname("新昵称" ) .build();
这在不可变对象风格里非常实用。
1.6 构造器注解 Lombok 常用构造器注解:
1 2 3 @NoArgsConstructor @AllArgsConstructor @RequiredArgsConstructor
@NoArgsConstructor生成无参构造器:
1 2 3 4 5 6 7 @NoArgsConstructor public class User { private Long id; private String username; }
JPA Entity 经常需要无参构造器,但建议保护起来:
1 2 3 4 5 6 7 8 9 @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class UserEntity { @Id private Long id; private String username; }
这样既满足框架要求,又避免业务代码随便 new 一个半初始化对象。
@AllArgsConstructor生成全字段构造器:
1 2 3 4 5 6 7 @AllArgsConstructor public class User { private Long id; private String username; }
@RequiredArgsConstructor为 final 字段和 @NonNull 字段生成构造器。
非常适合 Spring 构造器注入:
1 2 3 4 5 6 7 8 @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; }
这比字段注入更清晰,也更方便单元测试。
1.7 @Value:Lombok 的不可变对象 @Value 可以理解为 Lombok 版的不可变数据类。
1 2 3 4 5 6 7 8 9 @Value public class UserSnapshot { Long id; String username; String nickname; }
它通常会让字段变成 private final,生成 getter、全参构造器、toString、equals、hashCode,并且不生成 setter。
适合:
值对象。
快照对象。
查询结果对象。
事件消息对象。
不希望被修改的数据对象。
不过 Java 已经有了 record,所以在 JDK 16+ 的项目中,如果只是表达纯数据载体,可以优先考虑 record。
1.8 @Accessors(chain = true):链式 setter 1 2 3 4 5 6 7 8 9 @Getter @Setter @Accessors(chain = true) public class UserDTO { private Long id; private String username; }
使用:
1 2 3 UserDTO dto = new UserDTO () .setId(1L ) .setUsername("mario" );
这种写法在 DTO 里还算方便,但在 Entity 或领域对象中要谨慎使用,因为链式 setter 会鼓励“随便改对象状态”。
1.9 @Slf4j 日志注解非常常用:
1 2 3 4 5 6 7 8 @Slf4j @Service public class UserService { public void createUser () { log.info("create user" ); } }
等价于手写:
1 private static final Logger log = LoggerFactory.getLogger(UserService.class);
推荐使用 @Slf4j,简单、清晰、没什么争议。
1.10 Lombok 在不同层的推荐用法 Entity 层 推荐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Entity @Table(name = "user") public class UserEntity { @Id private Long id; private String username; private String nickname; }
谨慎:
1 2 3 4 @Data @Entity public class UserEntity { }
不建议在复杂 Entity 上直接使用 @Data。
DTO / VO 层 简单 DTO 可以:
1 2 3 4 5 6 7 @Data public class UserDTO { private Long id; private String username; }
更稳一点:
1 2 3 4 5 6 7 8 @Getter @Setter public class UserDTO { private Long id; private String username; }
如果是只读返回对象,可以考虑 record:
1 2 3 4 5 6 public record UserVO ( Long id, String username, String nickname ) { }
Service 层 推荐:
1 2 3 4 5 6 7 8 9 @Slf4j @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; }
配置属性类 可以使用:
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter @ConfigurationProperties(prefix = "app.storage") public class StorageProperties { private String endpoint; private String bucket; private String accessKey; }
如果项目使用较新 Spring Boot,也可以结合 record 做配置属性绑定,但要注意版本兼容性。
第二章:Java Record 详解 2.1 Record 是什么 Record 是 Java 语言提供的一种特殊类,用来表达“透明的数据载体”。
普通类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class UserVO { private final Long id; private final String username; private final String nickname; public UserVO (Long id, String username, String nickname) { this .id = id; this .username = username; this .nickname = nickname; } public Long id () { return id; } public String username () { return username; } public String nickname () { return nickname; } @Override public String toString () { return "UserVO[id=" + id + ", username=" + username + ", nickname=" + nickname + "]" ; } @Override public boolean equals (Object o) { } @Override public int hashCode () { } }
Record 写法:
1 2 3 4 5 6 public record UserVO ( Long id, String username, String nickname ) { }
Java 会自动生成:
私有 final 字段。
全参构造器。
访问器方法。
equals。
hashCode。
toString。
注意,record 的访问器不是 getId(),而是 id()。
1 2 3 4 UserVO vo = new UserVO (1L , "mario" , "超级马里奥" );Long id = vo.id();String username = vo.username();
2.2 Record 的核心语义 Record 的重点不是“少写代码”,而是表达一种语义:
这个对象就是一组数据,它的状态由声明的组件完整决定。
例如:
1 2 3 4 5 public record Money ( BigDecimal amount, String currency ) { }
它表达的是:
Money 由 amount 和 currency 组成。
这两个字段就是它的完整状态。
它不应该有额外隐藏状态。
它天然适合作为值对象。
这和 Lombok 的 @Data 完全不一样。
@Data 是在普通类上生成方法,普通类仍然可以有复杂状态、继承、setter、额外字段、复杂生命周期。
Record 是语言层面的一种特殊类,它天生偏不可变、偏数据建模。
2.3 Record 的基本语法 1 2 3 4 5 6 public record UserRegisterRequest ( String username, String password, String email ) { }
创建对象:
1 2 3 4 5 UserRegisterRequest request = new UserRegisterRequest ( "mario" , "123456" , "mario@example.com" );
访问字段:
1 2 String username = request.username();String email = request.email();
打印:
1 System.out.println(request);
输出类似:
1 UserRegisterRequest[username=mario, password=123456 , email=mario@example .com]
2.4 Record 的构造器 Record 有规范构造器,也可以使用紧凑构造器。
规范构造器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public record UserRegisterRequest ( String username, String password, String email ) { public UserRegisterRequest (String username, String password, String email) { if (username == null || username.isBlank()) { throw new IllegalArgumentException ("username cannot be blank" ); } if (password == null || password.length() < 6 ) { throw new IllegalArgumentException ("password length must >= 6" ); } this .username = username; this .password = password; this .email = email; } }
紧凑构造器 更推荐这种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public record UserRegisterRequest ( String username, String password, String email ) { public UserRegisterRequest { if (username == null || username.isBlank()) { throw new IllegalArgumentException ("username cannot be blank" ); } if (password == null || password.length() < 6 ) { throw new IllegalArgumentException ("password length must >= 6" ); } } }
紧凑构造器里不需要写参数列表,也不需要手动 this.username = username。
2.5 Record 中的数据规范化 Record 构造器很适合做数据规范化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public record Money ( BigDecimal amount, String currency ) { public Money { if (amount == null ) { throw new IllegalArgumentException ("amount cannot be null" ); } if (currency == null || currency.isBlank()) { throw new IllegalArgumentException ("currency cannot be blank" ); } amount = amount.setScale(2 , RoundingMode.HALF_UP); currency = currency.toUpperCase(Locale.ROOT); } }
这样创建出来的对象天然就是规范状态:
1 2 3 4 Money money = new Money (new BigDecimal ("10.456" ), "cny" ); System.out.println(money.amount()); System.out.println(money.currency());
2.6 Record 的不可变性 Record 的字段是 final 的,不能重新赋值。
1 2 3 4 5 public record UserVO ( Long id, String username ) { }
下面这种代码不存在:
1 vo.setUsername("newName" );
但要注意:record 是浅不可变,不是深不可变。
例如:
1 2 3 4 5 public record UserRoles ( Long userId, List<String> roles ) { }
虽然 roles 字段引用不能换,但 List 本身可能还能被修改。
1 2 3 4 5 6 7 8 List<String> roles = new ArrayList <>(); roles.add("ADMIN" );UserRoles userRoles = new UserRoles (1L , roles); roles.add("USER" ); System.out.println(userRoles.roles());
更安全的写法:
1 2 3 4 5 6 7 8 9 public record UserRoles ( Long userId, List<String> roles ) { public UserRoles { roles = List.copyOf(roles); } }
这样可以避免外部继续修改集合内容。
2.7 Record 可以定义方法 Record 不只能放字段,也可以定义普通方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 public record Money ( BigDecimal amount, String currency ) { public boolean isPositive () { return amount.compareTo(BigDecimal.ZERO) > 0 ; } public String display () { return amount + " " + currency; } }
也可以定义静态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 public record Money ( BigDecimal amount, String currency ) { public static Money cny (BigDecimal amount) { return new Money (amount, "CNY" ); } public static Money usd (BigDecimal amount) { return new Money (amount, "USD" ); } }
2.8 Record 可以实现接口 1 2 3 4 public interface Identifiable { Long id () ; }
1 2 3 4 5 public record UserVO ( Long id, String username ) implements Identifiable { }
使用:
1 2 Identifiable identifiable = new UserVO (1L , "mario" ); System.out.println(identifiable.id());
2.9 Record 支持泛型 1 2 3 4 5 6 7 public record PageResult <T>( List<T> records, long total, int pageNo, int pageSize ) { }
使用:
1 2 3 4 5 6 PageResult<UserVO> page = new PageResult <>( List.of(new UserVO (1L , "mario" )), 1 , 1 , 10 );
这是接口返回值中非常好用的模式。
2.10 Record 可以作为局部类 在方法内部定义临时数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Map<String, Long> countByType (List<OrderEntity> orders) { record OrderTypeCount (String type, Long count) { } List<OrderTypeCount> list = orders.stream() .collect(Collectors.groupingBy(OrderEntity::getType, Collectors.counting())) .entrySet() .stream() .map(entry -> new OrderTypeCount (entry.getKey(), entry.getValue())) .toList(); return list.stream() .collect(Collectors.toMap(OrderTypeCount::type, OrderTypeCount::count)); }
这个特性很适合替代临时 Pair、Tuple、小 DTO。
2.11 Record 的限制 Record 有一些明确限制:
不能继承其他类 1 2 public record UserVO (Long id) extends BaseVO { }
这种写法不允许。
Record 隐式继承 java.lang.Record。
不能声明额外的实例字段 1 2 3 4 public record UserVO (Long id, String username) { private String nickname; }
这种不允许。
Record 的实例状态只能来自 record 组件。
字段天然 final 不能给组件生成 setter,也不能修改组件值。
不适合复杂可变对象 例如订单聚合根:
1 2 3 4 5 6 public record Order ( Long id, List<OrderItem> items, OrderStatus status ) { }
如果订单需要频繁变更状态、添加明细、取消、支付、发货,record 就不适合做领域聚合根。
2.12 Record 和 Lombok 的区别 很多人会问:有了 record,是不是 Lombok 就不用了?
答案是:不是。
它们解决的问题不同。
对比项
Lombok
Record
本质
编译期代码生成工具
Java 语言特性
目标
减少样板代码
表达透明数据载体
是否可变
可变、不可变都支持
默认偏不可变
getter 风格
getXxx()
xxx()
setter
可以生成
不支持
继承普通类
可以
不可以
额外实例字段
可以
不可以
适合 Entity
可以,但慎用 @Data
通常不适合作为传统 JPA Entity
适合 DTO/VO
可以
非常适合只读 DTO/VO
适合领域值对象
可以
很适合
依赖工具
需要 Lombok 依赖和 IDE 支持
JDK 原生支持
2.13 什么时候用 Lombok,什么时候用 Record 推荐规则:
使用 Lombok 的场景
JPA Entity。
MyBatis Entity。
需要 setter 的 DTO。
需要 builder 的复杂对象。
Service 构造器注入。
日志对象。
兼容旧框架的 JavaBean。
需要无参构造器的对象。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class UserCommand { private Long id; private String username; private String nickname; }
使用 Record 的场景
API 返回对象。
查询结果对象。
不可变请求对象。
领域值对象。
事件消息对象。
临时组合数据。
Stream 分组后的中间结果。
示例:
1 2 3 4 5 6 7 public record UserDetailVO ( Long id, String username, String nickname, String roleName ) { }
不建议使用 Record 的场景
传统 JPA Entity。
需要框架反复修改字段的对象。
生命周期复杂的领域聚合根。
需要继承父类的对象。
需要大量 setter 的对象。
字段特别多且经常变的入参对象。
2.14 Record 在接口入参中的使用 可以这样写:
1 2 3 4 5 6 7 8 9 10 11 public record CreateUserRequest ( @NotBlank(message = "用户名不能为空") String username, @NotBlank(message = "密码不能为空") String password, @Email(message = "邮箱格式不正确") String email ) { }
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequiredArgsConstructor @RequestMapping("/users") public class UserController { private final UserAppService userAppService; @PostMapping public UserVO create (@Valid @RequestBody CreateUserRequest request) { return userAppService.create(request); } }
返回对象:
1 2 3 4 5 6 7 public record UserVO ( Long id, String username, String nickname, LocalDateTime createdAt ) { }
这类用法非常清爽。
第三章:MapStruct 详解 3.1 为什么需要 MapStruct 在项目中,最常见的对象转换包括:
Entity 转 VO。
DTO 转 Entity。
Request 转 Command。
Entity 转 Event。
BO 转 Response。
List 转 List。
Page 转 Page。
手写转换最稳,但重复劳动太多。
使用 BeanUtils:
1 BeanUtils.copyProperties(entity, vo);
虽然省事,但问题也明显:
字段名错了,编译期不报错。
类型不匹配,运行时才可能发现。
复杂映射不好处理。
嵌套对象、枚举、时间格式处理麻烦。
反射性能不如直接方法调用。
出问题后排查不够直观。
MapStruct 的核心价值是:
在编译期生成类型安全、性能接近手写代码的对象转换代码。
3.2 MapStruct 的基本使用 Maven 依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <properties > <java.version > 17</java.version > <mapstruct.version > 1.6.3</mapstruct.version > <lombok.version > 1.18.46</lombok.version > <lombok.mapstruct.binding.version > 0.2.0</lombok.mapstruct.binding.version > </properties > <dependencies > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${mapstruct.version}</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > <scope > provided</scope > </dependency > </dependencies >
编译插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.13.0</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <annotationProcessorPaths > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${mapstruct.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > ${lombok.mapstruct.binding.version}</version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </build >
lombok-mapstruct-binding 很关键。它用于解决 Lombok 和 MapStruct 组合时的注解处理顺序问题。
3.3 基础映射 Entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class UserEntity { private Long id; private String username; private String nickname; private String email; }
VO:
1 2 3 4 5 6 public record UserVO ( Long id, String username, String nickname ) { }
Mapper:
1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface UserMapper { UserVO toVO (UserEntity entity) ; List<UserVO> toVOList (List<UserEntity> entities) ; }
Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service @RequiredArgsConstructor public class UserQueryService { private final UserMapper userMapper; public UserVO detail (Long id) { UserEntity entity = getById(id); return userMapper.toVO(entity); } private UserEntity getById (Long id) { return UserEntity.builder() .id(id) .username("mario" ) .nickname("超级马里奥" ) .email("mario@example.com" ) .build(); } }
MapStruct 会自动生成 UserMapperImpl,并交给 Spring 管理。
3.4 字段名不同的映射 Entity:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserEntity { private Long id; private String username; private String nickname; }
VO:
1 2 3 4 5 6 public record UserVO ( Long userId, String account, String nickname ) { }
Mapper:
1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "userId", source = "id") @Mapping(target = "account", source = "username") UserVO toVO (UserEntity entity) ; }
3.5 忽略字段 1 2 3 4 5 6 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "password", ignore = true) UserEntity toEntity (CreateUserRequest request) ; }
适合密码、状态、创建时间、ID 等不应该由外部请求直接控制的字段。
3.6 默认值和常量 1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "status", constant = "ENABLE") @Mapping(target = "deleted", constant = "false") UserEntity toEntity (CreateUserRequest request) ; }
默认值:
1 2 3 4 5 6 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "nickname", source = "nickname", defaultValue = "默认昵称") UserEntity toEntity (CreateUserRequest request) ; }
3.7 表达式映射 1 2 3 4 5 6 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())") UserEntity toEntity (CreateUserRequest request) ; }
表达式适合简单逻辑,不建议在表达式里写复杂业务。复杂逻辑应该放到 Service 或专门的转换方法中。
3.8 枚举映射 枚举:
1 2 3 4 5 6 7 8 9 @Getter @RequiredArgsConstructor public enum UserStatus { ENABLE("启用" ), DISABLE("禁用" ); private final String desc; }
Entity:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserEntity { private Long id; private String username; private UserStatus status; }
VO:
1 2 3 4 5 6 7 public record UserVO ( Long id, String username, String status, String statusDesc ) { }
Mapper:
1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "status", source = "status") @Mapping(target = "statusDesc", source = "status.desc") UserVO toVO (UserEntity entity) ; }
如果需要把枚举转成字符串:
1 2 3 default String map (UserStatus status) { return status == null ? null : status.name(); }
3.9 嵌套对象映射 Entity:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class OrderEntity { private Long id; private String orderNo; private UserEntity user; }
VO:
1 2 3 4 5 6 7 public record OrderVO ( Long id, String orderNo, Long userId, String username ) { }
Mapper:
1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface OrderMapper { @Mapping(target = "userId", source = "user.id") @Mapping(target = "username", source = "user.username") OrderVO toVO (OrderEntity entity) ; }
3.10 集合映射 1 2 3 4 5 6 7 8 9 @Mapper(componentModel = "spring") public interface UserMapper { UserVO toVO (UserEntity entity) ; List<UserVO> toVOList (List<UserEntity> entities) ; Set<UserVO> toVOSet (Set<UserEntity> entities) ; }
MapStruct 会自动循环转换集合元素。
3.11 更新已有对象:@MappingTarget 在修改场景中,不建议直接 new 一个新 Entity 覆盖旧对象,因为可能会丢失数据库已有字段。
请求对象:
1 2 3 4 5 public record UpdateUserRequest ( String nickname, String email ) { }
Mapper:
1 2 3 4 5 6 @Mapper(componentModel = "spring") public interface UserMapper { @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) void updateEntity (UpdateUserRequest request, @MappingTarget UserEntity entity) ; }
Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service @RequiredArgsConstructor public class UserCommandService { private final UserMapper userMapper; public void update (Long id, UpdateUserRequest request) { UserEntity entity = getById(id); userMapper.updateEntity(request, entity); save(entity); } private UserEntity getById (Long id) { return new UserEntity (); } private void save (UserEntity entity) { } }
NullValuePropertyMappingStrategy.IGNORE 的作用是:请求字段为 null 时,不覆盖原对象字段。
这在 PATCH 更新接口中特别有用。
3.12 严格检查未映射字段 建议在项目中开启严格策略:
1 2 3 4 5 6 7 8 @Mapper( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR ) public interface UserMapper { UserVO toVO (UserEntity entity) ; }
这样当目标对象新增字段但 Mapper 没有处理时,编译会失败。
别怕编译失败。编译期失败是队友,运行期炸锅才是敌人。
3.13 公共 Mapper 配置 可以统一配置:
1 2 3 4 5 6 7 @MapperConfig( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR, injectionStrategy = InjectionStrategy.CONSTRUCTOR ) public interface CentralMapperConfig { }
具体 Mapper:
1 2 3 4 5 @Mapper(config = CentralMapperConfig.class) public interface UserMapper { UserVO toVO (UserEntity entity) ; }
这样可以避免每个 Mapper 重复写配置。
3.14 MapStruct 和 Lombok Builder 整合 对象:
1 2 3 4 5 6 7 8 @Getter @Builder public class UserCommand { private final String username; private final String nickname; }
Mapper:
1 2 3 4 5 @Mapper(componentModel = "spring") public interface UserCommandMapper { UserCommand toCommand (CreateUserRequest request) ; }
如果 Lombok 和 MapStruct 配置正确,MapStruct 可以识别 Lombok Builder。
如果识别失败,优先检查:
是否配置了 mapstruct-processor。
是否配置了 lombok annotation processor。
是否配置了 lombok-mapstruct-binding。
IDE 是否开启 annotation processing。
Maven 和 IDE 使用的 JDK 是否一致。
3.15 MapStruct 和 Record 整合 Record:
1 2 3 4 5 6 public record UserVO ( Long id, String username, String nickname ) { }
Entity:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserEntity { private Long id; private String username; private String nickname; }
Mapper:
1 2 3 4 5 @Mapper(componentModel = "spring") public interface UserMapper { UserVO toVO (UserEntity entity) ; }
MapStruct 可以调用 record 的构造器完成对象创建。
如果字段名不一致:
1 2 3 4 5 public record UserVO ( Long userId, String accountName ) { }
Mapper:
1 2 3 4 5 6 7 @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "userId", source = "id") @Mapping(target = "accountName", source = "username") UserVO toVO (UserEntity entity) ; }
第四章:MapStructPlus 详解 4.1 MapStructPlus 是什么 MapStructPlus 是对 MapStruct 的增强。它的目标是继续减少 MapStruct 的样板代码。
MapStruct 的标准写法需要手动定义 Mapper 接口:
1 2 3 4 5 @Mapper(componentModel = "spring") public interface UserMapper { UserVO toVO (UserEntity entity) ; }
MapStructPlus 的思路是:在源对象或目标对象上加注解,让框架自动生成转换器,然后通过统一的 Converter 调用。
例如:
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter @AutoMapper(target = UserVO.class) public class UserEntity { private Long id; private String username; private String nickname; }
VO:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserVO { private Long id; private String username; private String nickname; }
使用:
1 2 3 4 5 6 7 8 9 10 @Service @RequiredArgsConstructor public class UserQueryService { private final Converter converter; public UserVO detail (UserEntity entity) { return converter.convert(entity, UserVO.class); } }
这就省掉了显式 Mapper 接口。
4.2 MapStructPlus Maven 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <properties > <java.version > 17</java.version > <mapstruct-plus.version > 1.5.0</mapstruct-plus.version > <lombok.version > 1.18.46</lombok.version > </properties > <dependencies > <dependency > <groupId > io.github.linpeilie</groupId > <artifactId > mapstruct-plus-spring-boot-starter</artifactId > <version > ${mapstruct-plus.version}</version > </dependency > <dependency > <groupId > io.github.linpeilie</groupId > <artifactId > mapstruct-plus-processor</artifactId > <version > ${mapstruct-plus.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > <scope > provided</scope > </dependency > </dependencies >
编译插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.13.0</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <annotationProcessorPaths > <path > <groupId > io.github.linpeilie</groupId > <artifactId > mapstruct-plus-processor</artifactId > <version > ${mapstruct-plus.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </build >
如果你同时直接使用 MapStruct 和 Lombok,也建议补充 lombok-mapstruct-binding。
4.3 基础转换示例 Entity:
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter @AutoMapper(target = UserVO.class) public class UserEntity { private Long id; private String username; private String nickname; }
VO:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserVO { private Long id; private String username; private String nickname; }
Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service @RequiredArgsConstructor public class UserQueryService { private final Converter converter; public UserVO detail (UserEntity entity) { return converter.convert(entity, UserVO.class); } public List<UserVO> list (List<UserEntity> entities) { return converter.convert(entities, UserVO.class); } }
4.4 字段名不一致 1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter @Setter @AutoMapper(target = UserVO.class) public class UserEntity { @AutoMapping(target = "userId") private Long id; @AutoMapping(target = "accountName") private String username; private String nickname; }
VO:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class UserVO { private Long userId; private String accountName; private String nickname; }
如果字段映射规则复杂,建议退回到原生 MapStruct 显式 Mapper。自动化很好,但别把自动化当玄学许愿池。
4.5 多目标对象转换 一个 Entity 可能要转多个对象:
UserListVO
UserDetailVO
UserExportVO
UserEvent
可以使用多个自动映射注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Getter @Setter @AutoMappers({ @AutoMapper(target = UserListVO.class), @AutoMapper(target = UserDetailVO.class), @AutoMapper(target = UserExportVO.class) }) public class UserEntity { private Long id; private String username; private String nickname; private String email; private LocalDateTime createdAt; }
使用:
1 2 3 4 5 UserListVO listVO = converter.convert(entity, UserListVO.class);UserDetailVO detailVO = converter.convert(entity, UserDetailVO.class);UserExportVO exportVO = converter.convert(entity, UserExportVO.class);
4.6 MapStructPlus 适合什么场景 适合:
字段名大部分一致。
简单 DTO、VO 转换。
列表转换。
项目中转换对象非常多,但逻辑比较简单。
想统一使用 Converter 的团队。
快速开发中后台 CRUD 系统。
不适合:
字段映射规则复杂。
涉及大量业务逻辑。
涉及多个聚合对象组装。
字段差异很大。
对编译速度非常敏感的大型项目。
需要非常明确 Mapper 边界的核心领域模块。
推荐策略:
简单转换:MapStructPlus。
复杂转换:MapStruct。
核心业务转换:显式 MapStruct 或手写。
不确定转换:宁愿手写,也不要让自动生成逻辑变成黑盒。
第五章:Lombok、Record、MapStruct、MapStructPlus 的组合实践 5.1 推荐分层模型 一个比较清晰的后端项目可以这样分层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 controller └── request / response application └── command / query / service domain └── entity / value object / aggregate infrastructure └── persistence entity / mapper / repository interfaces └── event / rpc dto
对象使用建议:
层级
对象类型
推荐技术
Controller 入参
Request
record 或 Lombok DTO
Controller 出参
Response / VO
record
Application 命令
Command
record 或 Lombok @Value
Domain 值对象
Value Object
record 或普通不可变类
Domain 聚合根
Aggregate
普通类 + 少量 Lombok
Persistence Entity
Entity / DO
Lombok
对象转换
Mapper
MapStruct
简单对象批量转换
Converter
MapStructPlus
5.2 一个完整示例:财务单据创建 5.2.1 Entity 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Getter @Setter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class FinanceBillEntity { private Long id; private String billNo; private Long customerId; private BigDecimal amount; private BillStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; }
枚举:
1 2 3 4 5 6 7 8 9 10 @Getter @RequiredArgsConstructor public enum BillStatus { WAIT_SETTLE("待结算" ), SETTLED("已结算" ), CANCELLED("已取消" ); private final String desc; }
5.2.2 Request 使用 record 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public record FinanceBillCreateRequest ( @NotNull(message = "客户ID不能为空") Long customerId, @NotNull(message = "金额不能为空") @DecimalMin(value = "0.01", message = "金额必须大于0") BigDecimal amount ) { public FinanceBillCreateRequest { if (amount != null ) { amount = amount.setScale(2 , RoundingMode.HALF_UP); } } }
5.2.3 Response 使用 record 1 2 3 4 5 6 7 8 9 10 public record FinanceBillResponse ( Long id, String billNo, Long customerId, BigDecimal amount, String status, String statusDesc, LocalDateTime createdAt ) { }
5.2.4 Mapper 使用 MapStruct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Mapper( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR, injectionStrategy = InjectionStrategy.CONSTRUCTOR ) public interface FinanceBillMapper { @Mapping(target = "id", ignore = true) @Mapping(target = "billNo", ignore = true) @Mapping(target = "status", constant = "WAIT_SETTLE") @Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())") @Mapping(target = "updatedAt", expression = "java(java.time.LocalDateTime.now())") FinanceBillEntity toEntity (FinanceBillCreateRequest request) ; @Mapping(target = "status", expression = "java(entity.getStatus() == null ? null : entity.getStatus().name())") @Mapping(target = "statusDesc", source = "status.desc") FinanceBillResponse toResponse (FinanceBillEntity entity) ; List<FinanceBillResponse> toResponseList (List<FinanceBillEntity> entities) ; }
5.2.5 Service 使用 Lombok 构造器注入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service @RequiredArgsConstructor public class FinanceBillService { private final FinanceBillMapper financeBillMapper; public FinanceBillResponse create (FinanceBillCreateRequest request) { FinanceBillEntity entity = financeBillMapper.toEntity(request); entity.setBillNo(generateBillNo()); save(entity); return financeBillMapper.toResponse(entity); } private String generateBillNo () { return "BILL" + System.currentTimeMillis(); } private void save (FinanceBillEntity entity) { } }
5.2.6 Controller 1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequiredArgsConstructor @RequestMapping("/finance-bills") public class FinanceBillController { private final FinanceBillService financeBillService; @PostMapping public FinanceBillResponse create (@Valid @RequestBody FinanceBillCreateRequest request) { return financeBillService.create(request); } }
这个组合里:
Entity 用 Lombok,因为它需要被持久化框架操作。
Request 用 record,因为它是请求数据载体。
Response 用 record,因为它是只读返回数据。
Mapper 用 MapStruct,因为转换规则比较明确。
Service 用 Lombok 的 @RequiredArgsConstructor,减少构造器样板代码。
这就是比较舒服的现代 Java 后端写法。
5.3 修改接口:MapStruct 更新已有 Entity Update Request:
1 2 3 4 public record FinanceBillUpdateRequest ( BigDecimal amount ) { }
Mapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Mapper( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR ) public interface FinanceBillMapper { @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) @Mapping(target = "id", ignore = true) @Mapping(target = "billNo", ignore = true) @Mapping(target = "customerId", ignore = true) @Mapping(target = "status", ignore = true) @Mapping(target = "createdAt", ignore = true) @Mapping(target = "updatedAt", expression = "java(java.time.LocalDateTime.now())") void updateEntity (FinanceBillUpdateRequest request, @MappingTarget FinanceBillEntity entity) ; }
Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service @RequiredArgsConstructor public class FinanceBillService { private final FinanceBillMapper financeBillMapper; public FinanceBillResponse update (Long id, FinanceBillUpdateRequest request) { FinanceBillEntity entity = getById(id); financeBillMapper.updateEntity(request, entity); save(entity); return financeBillMapper.toResponse(entity); } private FinanceBillEntity getById (Long id) { return new FinanceBillEntity (); } private void save (FinanceBillEntity entity) { } }
这比直接 BeanUtils.copyProperties 稳得多。
5.4 查询列表:MapStructPlus 简化简单转换 如果只是简单列表查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Getter @Setter @AutoMapper(target = FinanceBillListVO.class) public class FinanceBillEntity { private Long id; private String billNo; private Long customerId; private BigDecimal amount; private BillStatus status; private LocalDateTime createdAt; }
VO:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Getter @Setter public class FinanceBillListVO { private Long id; private String billNo; private Long customerId; private BigDecimal amount; private BillStatus status; private LocalDateTime createdAt; }
Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service @RequiredArgsConstructor public class FinanceBillQueryService { private final Converter converter; public List<FinanceBillListVO> list () { List<FinanceBillEntity> entities = queryList(); return converter.convert(entities, FinanceBillListVO.class); } private List<FinanceBillEntity> queryList () { return List.of(); } }
这种简单转换就可以交给 MapStructPlus。
5.5 更推荐的组合边界 写操作:优先 MapStruct 写操作涉及状态变更、字段忽略、默认值、安全字段保护,因此推荐显式 MapStruct。
1 2 3 4 CreateRequest -> Entity UpdateRequest -> @MappingTarget Entity Command -> Entity Entity -> Event
读操作:简单场景可用 MapStructPlus 读操作很多时候只是 Entity 转 VO:
1 2 3 Entity -> ListVO Entity -> DetailVO Entity -> ExportVO
如果字段基本一致,可以用 MapStructPlus 提效。
核心领域:少用自动转换 核心领域逻辑不要过度依赖自动映射。
例如:
1 OrderEntity -> OrderAggregate
这种转换可能涉及:
订单主表。
订单明细。
支付信息。
优惠信息。
状态机。
金额计算。
领域规则。
这类场景建议手写装配逻辑,或者使用非常明确的 MapStruct Mapper,不建议用过度自动化的转换。
第六章:项目级配置建议 6.1 Maven 推荐配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <properties > <java.version > 17</java.version > <lombok.version > 1.18.46</lombok.version > <mapstruct.version > 1.6.3</mapstruct.version > <lombok.mapstruct.binding.version > 0.2.0</lombok.mapstruct.binding.version > <mapstruct-plus.version > 1.5.0</mapstruct-plus.version > </properties > <dependencies > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${mapstruct.version}</version > </dependency > <dependency > <groupId > io.github.linpeilie</groupId > <artifactId > mapstruct-plus-spring-boot-starter</artifactId > <version > ${mapstruct-plus.version}</version > </dependency > </dependencies >
编译插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.13.0</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <annotationProcessorPaths > <path > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${mapstruct.version}</version > </path > <path > <groupId > io.github.linpeilie</groupId > <artifactId > mapstruct-plus-processor</artifactId > <version > ${mapstruct-plus.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > </path > <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > ${lombok.mapstruct.binding.version}</version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </build >
6.2 IDE 配置 IntelliJ IDEA 中需要开启:
1 2 3 4 5 Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors -> Enable annotation processing
否则 IDE 可能会报红,但 Maven 编译正常。这个问题特别烦,就像代码没错但 IDE 在旁边阴阳怪气。
6.3 MapStruct 统一配置 1 2 3 4 5 6 7 @MapperConfig( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR, injectionStrategy = InjectionStrategy.CONSTRUCTOR ) public interface GlobalMapperConfig { }
使用:
1 2 3 4 5 @Mapper(config = GlobalMapperConfig.class) public interface UserMapper { UserVO toVO (UserEntity entity) ; }
6.4 Lombok 配置文件 项目根目录可以增加 lombok.config:
1 2 3 4 5 config.stopBubbling = true lombok.addLombokGeneratedAnnotation = true lombok.anyConstructor.addConstructorProperties = true
如果团队希望限制某些注解,也可以配置:
1 2 lombok.data.flagUsage = warning lombok.val.flagUsage = warning
这样可以提醒团队不要滥用 @Data。
第七章:常见踩坑 7.1 Lombok 和 MapStruct 编译报错 典型问题:
1 No property named "xxx" exists in source parameter
可能原因:
Lombok 没有生效。
IDE 没有开启 annotation processing。
Maven compiler plugin 没有配置 Lombok processor。
缺少 lombok-mapstruct-binding。
getter/setter 生成规则和 MapStruct 识别规则不一致。
解决:
1 2 3 4 5 6 <path > <groupId > org.projectlombok</groupId > <artifactId > lombok-mapstruct-binding</artifactId > <version > 0.2.0</version > </path >
7.2 Entity 上乱用 @Data 不推荐:
1 2 3 4 5 6 7 @Data @Entity public class OrderEntity { @OneToMany(mappedBy = "order") private List<OrderItemEntity> items; }
推荐:
1 2 3 4 5 6 7 8 9 10 11 12 @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class OrderEntity { @Id private Long id; @OneToMany(mappedBy = "order") private List<OrderItemEntity> items; }
如果需要 equals/hashCode,明确指定 ID:
1 2 3 4 5 6 7 8 9 10 @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity public class OrderEntity { @EqualsAndHashCode .Include @Id private Long id; }
7.3 Record 不是 JavaBean Record 的访问器是:
1 2 user.id(); user.username();
不是:
1 2 user.getId(); user.getUsername();
大多数现代框架已经支持 record,但如果你用的是老框架、老版本 JSON 库、老版本 ORM,就要先确认兼容性。
7.4 Record 不适合做传统 JPA Entity 传统 JPA Entity 通常需要:
无参构造器。
可变字段。
代理增强。
生命周期回调。
关联关系。
延迟加载。
Record 天然 final、字段 final、没有 setter,也不能继承普通类,所以不适合做传统 JPA Entity。
但在较新的 Hibernate 中,record 可以在部分值对象、Embeddable、查询投影场景中使用。实际项目里建议谨慎使用,先验证框架版本和运行行为。
7.5 MapStruct 表达式不要塞业务逻辑 不推荐:
1 2 3 4 5 @Mapping( target = "amount", expression = "java(request.getPrice().multiply(request.getQuantity()).subtract(request.getDiscount()).setScale(2, java.math.RoundingMode.HALF_UP))" ) OrderEntity toEntity (CreateOrderRequest request) ;
推荐把复杂逻辑放到业务服务中:
1 2 3 4 5 6 7 8 9 10 @Service public class AmountCalculator { public BigDecimal calculate (CreateOrderRequest request) { return request.price() .multiply(BigDecimal.valueOf(request.quantity())) .subtract(request.discount()) .setScale(2 , RoundingMode.HALF_UP); } }
Mapper 只做映射,不要变成业务垃圾桶。
7.6 MapStructPlus 不要过度自动化 MapStructPlus 很适合简单转换,但如果项目里几百个对象都自动生成转换,有可能导致生成代码膨胀、编译速度变慢、转换关系不够清晰。
建议:
简单列表 VO:用 MapStructPlus。
复杂详情 VO:用 MapStruct。
写操作转换:用 MapStruct。
领域模型转换:手写或显式 MapStruct。
跨微服务 DTO:显式 MapStruct。
7.7 字段新增后没有处理 建议统一使用:
1 unmappedTargetPolicy = ReportingPolicy.ERROR
这样目标类新增字段后,Mapper 没处理就会编译失败。
这不是麻烦,这是保护。编译器像一个严厉的老员工:嘴毒,但能救命。
第八章:最佳实践总结 8.1 总体原则 一句话总结:
Lombok 减少普通类样板代码,Record 表达不可变数据载体,MapStruct 负责严肃对象转换,MapStructPlus 负责简单转换自动化。
8.2 推荐用法清单 Entity 1 2 3 4 5 6 7 @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class XxxEntity { }
不要默认 @Data。
DTO 1 2 3 4 @Getter @Setter public class XxxDTO { }
或者:
1 2 3 4 5 public record XxxDTO ( Long id, String name ) { }
VO / Response 优先 record:
1 2 3 4 5 public record XxxResponse ( Long id, String name ) { }
Service 1 2 3 4 5 6 7 @Slf4j @Service @RequiredArgsConstructor public class XxxService { private final XxxMapper xxxMapper; }
Mapper 复杂转换用 MapStruct:
1 2 3 @Mapper(config = GlobalMapperConfig.class) public interface XxxMapper { }
简单转换用 MapStructPlus:
1 2 3 @AutoMapper(target = XxxVO.class) public class XxxEntity { }
8.3 技术选型建议
场景
推荐
普通 JavaBean 减少 getter/setter
Lombok
构造器注入
Lombok @RequiredArgsConstructor
日志对象
Lombok @Slf4j
不可变 DTO / VO
Record
领域值对象
Record 或普通不可变类
JPA Entity
Lombok,但慎用 @Data
Entity 转 VO
MapStruct
简单 Entity 转 VO
MapStructPlus
更新已有 Entity
MapStruct @MappingTarget
复杂聚合转换
手写或 MapStruct
高性能类型安全转换
MapStruct
快速 CRUD 转换
MapStructPlus
第九章:一个推荐的项目落地方案 如果是一个 Spring Boot 3 + JDK 17 的中后台项目,我会这样定规范:
9.1 Controller 层 入参优先 record:
1 2 3 4 5 public record CreateUserRequest ( @NotBlank String username, @NotBlank String password ) { }
出参优先 record:
1 2 3 4 5 6 public record UserResponse ( Long id, String username, String nickname ) { }
9.2 Application 层 Command 可以用 record:
1 2 3 4 5 public record CreateUserCommand ( String username, String password ) { }
如果字段多且构建复杂,可以用 Lombok:
1 2 3 4 5 6 7 8 9 10 @Getter @Builder public class CreateUserCommand { private final String username; private final String password; private final String source; }
9.3 Domain 层 值对象可以用 record:
1 2 3 4 5 6 7 8 9 10 public record PhoneNumber ( String value ) { public PhoneNumber { if (value == null || !value.matches("^1\\d{10}$" )) { throw new IllegalArgumentException ("手机号格式错误" ); } } }
聚合根使用普通类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Getter public class User { private Long id; private String username; private String password; private UserStatus status; public void disable () { if (status == UserStatus.DISABLED) { return ; } this .status = UserStatus.DISABLED; } }
不要为了省 setter 把领域对象强行写成 record。领域对象有行为,record 更适合数据。
9.4 Infrastructure 层 持久化实体使用 Lombok:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class UserDO { private Long id; private String username; private String password; private String status; }
9.5 转换层 写操作:
1 2 3 4 5 6 7 @Mapper(config = GlobalMapperConfig.class) public interface UserCommandMapper { User toDomain (CreateUserCommand command) ; UserDO toDO (User user) ; }
读操作:
1 2 3 4 5 6 7 @Mapper(config = GlobalMapperConfig.class) public interface UserQueryMapper { UserResponse toResponse (UserDO userDO) ; List<UserResponse> toResponseList (List<UserDO> list) ; }
简单 CRUD 可以补充 MapStructPlus:
1 2 3 @AutoMapper(target = UserResponse.class) public class UserDO { }
结论 Lombok、Record、MapStruct、MapStructPlus 不是互相替代的关系,而是一套可以互补的工具链。
比较成熟的使用方式是:
Lombok 用在普通类、实体类、服务类中,减少样板代码。
Record 用在不可变 DTO、VO、值对象、查询结果中,表达清晰的数据语义。
MapStruct 用在复杂、关键、需要编译期检查的对象转换中。
MapStructPlus 用在简单、大量、重复的对象转换中,提高开发效率。
核心领域逻辑不要过度自动化,必要时手写更清晰。
最终目标不是“代码越少越好”,而是:
简单对象少写代码。
核心逻辑表达清楚。
对象转换类型安全。
字段变化编译期发现。
项目成员一看就知道该在哪里写什么。
工具只是工具,架构边界才是重点。代码不是越短越高级,而是越不容易误解、越不容易出错、越容易维护,才是真的高级。
参考资料
Project Lombok 官方文档。
Java Record / JEP 395 官方说明。
MapStruct 官方参考文档。
MapStructPlus 官方文档与 GitHub 示例。
Lombok 与 Record 相关实践文章。
MapStruct 与 MapStructPlus 项目实践经验。
启示录 富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。