MapStruct
多层应用通常需要在不同的对象模型(例如实体和DTOs)之间进行映射。
MapStruct是一个代码生成器,便捷的配置能很方便地解决两个Java bean内部字段之间的映射,在编译时生成setter/getter代码实现的字节码,因此在真正run的时候,速度快,类型安全。
官方文档:Reference Guide – MapStruct
DTO模式
DTO即数据传输对象(Data Transfer Objects)。该模式是一种简单的设计模式,主要目的是通过将一次单一调用的多个参数分批来减少到服务器的往返次数,在远程操作中降低网络开销。
该实践的其它好处是序列化的逻辑(转换对象结构和数据为一种能被存储和传输的指定格式的机制)的封装。它提供了在序列化细微差别中一个单一改变点。它也解耦了表示层的领域模型,允许它们独自改变。
DTOs是平整的数据结构,不包含业务逻辑,仅仅有存储、访问和最终关联序列化或解析的方法。数据从领域模型映射为 DTO,一般通过在表示层或门面层的 mapper 组件。下方的图片说明了组件间的交互:
依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.4.2.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </dependency>
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data public class CarDto { private String make; private int seatCount; private String type; }
@Data public class Car { private String make; private int numberOfSeats;
}
|
为了方便我们进行对象间字段的映射,我们制定一个基类接口,进行统一化的定制,如果有特殊的需要,再自己去复写基类接口中的相应方法:
1 2 3 4 5 6 7 8 9
| public interface EntityConverter<D, E> { E toEntity(D dto);
D toDto(E entity);
List<E> toEntity(List<D> dtoList);
List<D> toDto(List<E> entityList); }
|
具体的转换类:
1 2 3 4 5 6 7
|
@Mapper(componentModel = "spring") public interface CarMapper extends EntityConverter<CarDto, Car> { @Mapping(source = "numberOfSeats", target = "seatCount") CarDto toDto(Car car); }
|
@Mapper
注解中指定了componentModel
为"spring"
,即将该接口的实现类放入了IOC容器
@Mapping
中指定了两个字段,表示car
中的字段numberOfSeats
中的值映射到CarDto
中的seatCount
,这是因为mapstruct的映射是根据字段名来进行的,所以字段名不能对应的字段,他们之间的映射必须要 显式指定。
如果存在多个映射,可以使用@Mappings
1 2 3 4 5
| @Mappings({ @Mapping(source = "type.name",target = "typeName"), @Mapping(source = ..,target = ..), ... })
|
在使用MapStruct,idea2020.3版本在build项目的时候出现错误:java: Internal error in the mapping processor: java.lang.NullPointerException
解决: Setting -->Build,Execution,Deployment -->Compiler -->User-local build
加上参数: -Djps.track.ap.dependencies=false
进阶属性
在映射器中忽略未映射目标属性的警告:
1
| @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
@Mappering注解的特殊属性:
1
| @Mappering(target="",ignore=true)
|
1 2
| @Mappering(target="",expression="java(Integer.parseInt(user.getId()))") UserDto userToDto(userVo user);
|
- numberFormat:格式化该元素的值
- defaultValue:设置元素默认值,如果原对象的该属性为空,则使用默认值
1 2
| @Mappering(source = "salary",target="salary",numberFormat="¥#.oo",defaultValue="¥0.00") UserDto userToDto(userVo user);
|
反转注解:@InheritInverseConfiguration
1 2 3
| @InheritInverseConfiguration(name="userToDto") UserVo userToVo(UserDto user);
|
插件推荐
MapStruct Suppot
使用示例
实体类:
Courses |
CoursesVO |
private Long id; |
private Long id; |
private String name; |
private String name; |
private String code; |
private String code; |
private Integer type; |
private String type; |
private Boolean open; |
private String open; |
private Long category; |
private Long category; |
private String describe; |
private String describe; |
private Boolean excellent; |
private String excellent; |
private Boolean expire; |
private String expire; |
private LocalDateTime createDate; |
private String createDate; |
private LocalDateTime updateDate; |
private String updateDate; |
private String createrId; |
private String createrId; |
private String createrName; |
private String createrName; |
private String managerId; |
private String managerId; |
private String managerName; |
private String managerName; |
private Boolean deleteFlag; |
private String deleteFlag; |
private Integer status; |
private Integer status; |
private Boolean beProtect; |
private String beProtect; |
MapMapper接口:
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
| @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface CoursesMapMapper { default String booleanToString(Boolean value) { return value ? "是" : "否"; }
default Boolean stringToboolean(String value) { if ("是".equals(value)) { return true; } else if ("否".equals(value)) { return false; } return null; }
default String localDateTimeToString(LocalDateTime dateTime) { Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()); return DateFormatUtils.format(date, "yyyy.MM.dd HH.mm.ss"); }
@Mappings({ @Mapping(target = "beProtect", expression = "java(courses.getBeProtect() ? \"是\" : \"否\")"), @Mapping(target = "type", expression = "java(courses.getType() == 1 ? \"面授\" : (courses.getType() == 2 ? \"在线\" : \"混合\"))") }) CoursesVO toVO(Courses courses);
List<CoursesVO> toVO(List<Courses> courses);
@InheritInverseConfiguration(name = "toVO") Courses toEntity(CoursesVO coursesVO);
List<Courses> toEntity(List<CoursesVO> coursesVO);
}
|
MapStruct可以自动检测到我们接口的默认方法并进行使用,可以省去我们重复的mapping注解。但是容易出bug,这就是为什么只有LocalDateTime 转换为String,不过MapStruct自动实现了String转LocalDateTime。
注意:source、defaultValue 不能 和 expression 一起出现
常见报错
1
| Unknown property "xxx" in result type xxx. Did you mean "null"?
|
原因:项目中同时使用了Lombok,两者生成代码顺序出现冲突
解决方案:
- 代码生成器annotationProcessor标签部分,将lombok放在mapstruct之前。
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
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
|
- 官网解释:常见问题 (FAQ) – MapStruct
1 2 3 4 5
| <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </dependency>
|
常用的封装名称
含义 |
命名规范示例 |
存储请求体信息 |
XxRequest |
展示层对象命名(存储返回给前端的信息) |
XxVo |
数据传输对象(用于存储从数据库中查询的数据) |
XxDto |
es实体类命名 |
XxindexDO |
db实体命名 |
与表名相同 |
service接口命名 |
Xxservice |
service实现命名 |
XxserviceImpl |
manager,service引入多个manager进行复杂的组合业务处理 |
Xxmanager |
dao层命名 |
XxManager |
封装持久化组合服务(一个实体需要从db,es,redis多种存储获取) |
XxRepository |