项目转换DTO使用总结,常用技巧
概要
mapstruct在当前轻量级框架开发中的重点使用,@Named注解使用示例,@AfterMapping与@BeforeMapping注解的详细常见用法,在转换DTO时,与过去常用的beanUtil转换有高性能的转换优势,编译期自动生成的mapper实现类能够更加优雅的来实现各种隐式类型转换,以实现快速而又敏捷的开发,告别臃肿的手动get、set与类型的强转
引入
当前core-service引入版本如下
<properties>
<mapstruct.version>1.2.0.Final</mapstruct.version>
</properties>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
基本使用
- 使用@Mapper注解,声明映射器,可以是接口,或者抽象类
- 使用@Mapping注解,实现灵活的字段映射,定制映射的规则
注入方式
- 工厂方式
@Mapper
public interface ItemMapper {
//使用工厂方法获取Mapper实例
ItemMapper INSTANCE = Mappers.getMapper(ItemMapper.class);
ItemDTO toDTO(Item item);
}
- 依赖注入方式
@Mapper(componentModel = "spring")
public interface ItemMapper {
ItemDTO toDTO(Item item);
}
简单映射
代码可以自动生成的Mapper接口,可以自动转换相同属性的字段,无需其他声明。
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface AwardMapper extends BaseMapper<AwardDTO, Award> {
}
需要注意的是:再不写任何转换代码的情况下,如果实体类中配置了其他关联的一对多或一对一的实体,DTO如果同样需要转换的话,就需要保证属性名一致才能正常转换。
自动类型转换
对于基础数据类型会进行自动隐式的转换如int、long、String,Integer、Long等;
//生成的实现类代码可以看出来
@Component
public class AssemblerImpl implements Assembler {
@Override
public ProductDTO toDTO(Product product) {
if ( product == null ) {
return null;
}
ProductDTO productDTO = new ProductDTO();
if ( product.getProductId() != null ) {
//String自动转int
productDTO.setProductId( Integer.parseInt( product.getProductId() ) );
}
if ( product.getPrice() != null ) {
//Long转String
productDTO.setPrice( String.valueOf( product.getPrice() ) );
}
return productDTO;
}
}
指定格式转换
查看mapping注解可以看到
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface Mapping {
String target();
String source() default "";
String dateFormat() default "";
String numberFormat() default "";
...
}
dateFormat与numberFormat即可用来进行转换操作
- dateFormat用于日期格式转换
@Mapper(componentModel = "spring")
public interface Demo4Assembler {
@Mapping(target = "saleTime", dateFormat = "yyyy-MM-dd HH:mm:ss") //Date转换成String
@Mapping(target = "validTime", dateFormat = "yyyy-MM-dd HH:mm") //String转换成Date
ProductDTO toDTO(Product product);
}
- 日期格式转换可以直接在DTO中注解形式转换,比mapper更为便捷
// 活动开始时间
@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss",timezone="GMT+8")
private Timestamp startTime;
// 活动结束时间
@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss",timezone="GMT+8")
private Timestamp endTime;
- numberFormat用于数字格式转换【不常用】
private String price;
private Integer stock;
@Mapper(componentModel = "spring")
public interface Demo3Assembler {
@Mapping(target = "price", numberFormat = "#.00元")
@Mapping(target = "stock", numberFormat = "#个")
ProductDTO toDTO(Product product);
}
不同参数映射
- 基本不同属性名转换,需要使用@Mappings注解进行指定转换,参数为数组,支持多个字段转换
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DrawRecordDetailMapper extends BaseMapper<DrawRecordDetailDTO, DrawRecord> {
@Override
@Mappings({
@Mapping(target = "chanceId", source = "participationChanceId")
})
DrawRecordDetailDTO toDto(DrawRecord entity);
}
- 对象属性提取和普通属性装载对象中
//将entity中activity实体中name属性装载至DrawRecordDetailDTO中activityName属性上
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DrawRecordDetailMapper extends BaseMapper<DrawRecordDetailDTO, DrawRecord> {
@Override
@Mappings({
@Mapping(target = "activityName", source = "activity.name")
})
DrawRecordDetailDTO toDto(DrawRecord entity);
}
//将entity中活动规则属性,转换至ActivityDetailDTO中activityRule对象中
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ActivityDetailMapper extends BaseMapper<ActivityDetailDTO, Activity> {
@Override
@Mappings({
@Mapping(target = "activityRule.voteOverlapAvailable", source = "voteOverlapAvailable"),
@Mapping(target = "activityRule.voteMultipleOnceAvailable", source = "voteMultipleOnceAvailable")
})
ActivityDetailDTO toDto(Activity entity);
}
自定义转换器
- 基础映射方式,mapper接口中直接引入即可(对数据接口要求高)
一个自定义映射器可以定义多个映射方法,匹配时,是以方法的入参和出参进行匹配的
如果绑定的映射中,存在多个相同的入参和出参方法,将会报错
使用样例:将list中多个code在映射时,转换为String用逗号隔开
定义映射器:
@Component
public class MediaServiceFormater {
public String format(List<String> serviceCodeList) {
return String.join(",", serviceCode);
}
}
mapper接口绑定映射器 【uses = {MediaServiceFormater.class}】
@Mapper(componentModel = "spring", uses = {MediaServiceFormater.class,unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MediaDetailMapper extends BaseMapper<MediaDetailDTO, Media> {
}
- 基于named注解实现【常用】
映射器中使用注解来定义转换方法,使用时,具体属性绑定方法;
定义映射器:
@Component
public class MapStructConverterUtil {
private static final Logger log = LoggerFactory.getLogger(MapStructConverterUtil.class);
public MapStructConverterUtil() {
}
@Named("getNormalImage")
public String getNormalImage(String images) {
if (StringUtils.isEmpty(images)) {
return null;
} else {
try {
if (images.contains("normal")) {
JSONObject imageJson = JSON.parseObject(images);
JSONObject mapJson = imageJson.getJSONObject("map");
JSONArray normalJson = mapJson.getJSONArray("normal");
Integer index = normalJson.getInteger(0);
JSONObject object = imageJson.getJSONArray("list").getJSONObject(index);
return object.getString("fileUrl");
}
} catch (Exception var7) {
log.error("normal图片解析出错,原images = [{}]", images);
}
return null;
}
}
}
使用时绑定映射器:【uses = {MapStructConverterUtil.class}】
@Mapper(componentModel = "spring", uses = {MapStructConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ActivityUserDetailMapper extends BaseMapper<ActivityUserDetailDTO, Activity> {
@Override
@Mappings({
@Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
@Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
@Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage")
})
ActivityUserDetailDTO toDto(Activity entity);
}
- Map映射
mapStruct还支持map集合的转换,可以对map进行隐式转换,支持对Key与value进行隐式转换
例如:日期转字符串,字符串转日期;同样支持@name注解转换形式,或使用qualifiedBy指定一个转换类(类中默认使用相同入参出参来匹配)
@Mapper(componentModel = "spring", uses = {ConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface SourceTargetMapper {
@MapMapping(keyQualifiedByName = "getValue", valueDateFormat = "dd.MM.yyyy")
Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
复杂映射实现
多个字段映射到一个字段,单个字段快速特殊处理
- 自定义字段表达式映射
- 单个字段特殊转换处理
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface GroupDetailOrderMapper extends BaseMapper<GroupDetailOrderDTO, GroupDetailOrder> {
@Override
@Mappings({
@Mapping(target = "userName", expression = "java(entity.getUserName() == null ? \"\" : new String(java.util.Base64.getDecoder().decode(entity.getUserName()), java.nio.charset.StandardCharsets.UTF_8))")
})
GroupDetailOrderDTO toDto(GroupDetailOrder entity);
}
- 多个字段处理为一个字段
@Mapper(componentModel = "spring", imports = DecimalUtils.class)
public interface Demo16Assembler {
@Mapping(target = "price", expression = "java(product.getPrice1() + product.getPrice2())")
@Mapping(target = "price2", expression = "java(DecimalUtils.add(product.getPrice1(), product.getPrice2()))")
ProductDTO toDTO(Product product);
}
- 前后置处理实现复杂映射
使用@BeforeMapping与@AfterMapping注解来实现,需要定义默认方法在mapper接口中【java8后支持】
使用前注意:
在调用上下文上的映射方法之前/之后,不执行空检查参数。调用方需要确保在这种情况下不传递null;
@Mapper(componentModel = “spring”, uses = {MapStructConverterUtil.class, MediaConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MediaDetailMapper extends BaseMapper<MediaDetailDTO, Media> {
//前置执行
@BeforeMapping
default MediaDetailDTO setMediaName(Media media) {
MediaDetailDTO mediaDetailDTO = new MediaDetailDTO();
mediaDetailDTO.setName("默认名称");
return mediaDetailDTO;
}
@Override
@Mappings({
@Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
@Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
@Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage"),
@Mapping(target = "cp", source = "contentProviderId", qualifiedByName = "getCP"),
@Mapping(target = "audioTrack", source = "audioDescription")
})
MediaDetailDTO toDto(Media media);
//后置执行
@AfterMapping
default void setMediaType(@MappingTarget MediaDetailDTO mediaDetailDTO, Media media) {
Integer type = media.getType();
Integer contentType = media.getContentType();
String contentTyp = String.format("%02d", contentType);
mediaDetailDTO.setMediaType(type + contentTyp);
}
}
隐式映射实现
利用继承关系,继承mapper自动生成的实现类,来重写增强
此转换方式,同样可以实现在dto转换后后置执行,实体真正实现类中代码简洁也较为优雅
需要注意的是,一定要先实现父类转换方法拿到结果后在进行后续增强转换
super.toDto(entity)
实现样例如下:
//实现类引入方式使用@Resource注解引入
@Autowired
@Resource(name = "activityDetailMapperImplPlus")
private ActivityDetailMapper activityDetailMapper;
@Component("activityDetailMapperImplPlus")
@RequiredArgsConstructor
public class ActivityDetailMapperImplPlus extends ActivityDetailMapperImpl {
private final VoteTargetRepository voteTargetRepository;
@Override
public ActivityDetailDTO toDto(Activity entity) {
ActivityDetailDTO activityDetailDTO = super.toDto(entity);
Long activityDetailDTOId = activityDetailDTO.getId();
Integer type = activityDetailDTO.getType();
boolean vote = LocalActConstant.ACT_TYPE_VOTE == type;
if (vote) {
Optional<List<VoteTarget>> voteOp = this.voteTargetRepository.findByActivityIdAndStatus(activityDetailDTOId, 1);
if (voteOp.isPresent()) {
List<VoteTarget> voteTargets = voteOp.get();
//累积投票
activityDetailDTO.setTotalVoteNum(voteTargets.stream()
.filter(voteTarget -> voteTarget.getCurrentNumber() != null)
.mapToInt(VoteTarget::getCurrentNumber).sum());
}
}
return activityDetailDTO;
}
}
隐式转换扩展
如果实体类中,关联了其他一对多或多对多的引用类型,JPA在mapper层自动转换后,又需要对引用对象增加出参或出参数据结构转换时,这时候利用转换生成是实现类继承强化后,再绑定到mapper中即可。这样就无需在接口实现层进行循环处理
使用样例如下:
MarketingActivityDetailDTO中有 private List registrationEvents;
需要在BaseRegistrationEventDTO对象中增加出参
//报名人数
private Long totalRegister;
接口侧转换使用的MarketingActivityDetailMapper接口如下:引用必须要包含RegistrationEventSimpleMapperImplPlus.class
uses = {MapStructConverterUtil.class, TemplateParamValueMapper1.class, RegistrationEventSimpleMapperImplPlus.class}
@Mapper(componentModel = "spring", uses = {MapStructConverterUtil.class, TemplateParamValueMapper1.class, RegistrationEventSimpleMapperImplPlus.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MarketingActivityDetailMapper extends BaseMapper<MarketingActivityDetailDTO, MarketingActivity> {
@Override
@Mappings({
@Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
@Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
@Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage")
})
MarketingActivityDetailDTO toDto(MarketingActivity entity);
}
RegistationEventSimpleMapper如下
@Component("registrationEventSimpleMapperImplPlus")
@RequiredArgsConstructor
public class RegistrationEventSimpleMapperImplPlus extends RegistrationEventSimpleMapperImpl {
private final UserRegistrationRepository userRegistrationRepository;
@Override
public BaseRegistrationEventDTO toDto(RegistrationEvent entity) {
BaseRegistrationEventDTO baseRegistrationEventDTO = super.toDto(entity);
Long registrationEventId = baseRegistrationEventDTO.getId();
baseRegistrationEventDTO.setTotalRegister(
this.userRegistrationRepository.countByRegistrationEventId(registrationEventId));
return baseRegistrationEventDTO;
}
}