数据类型转换
自动类型转换
在平常的对象映射中,总会有很多类型不完全一致的情况,例如Integer
转Long
,String
转Integer
或者两个自定义对象之间的转换等。在MapStruct
中定义了一些类型的自动类型转换或者说是隐形类型转换。
例如在上节中提到的基本数据类型和其包装类型之间的转换,String
和数字类型的转换,String
和时间类型的转换等。在@Mapping
注解中还可以自定义转换格式。
例如数字类型转String
类型,使用numberFormat
属性。
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
| @Data @AllArgsConstructor @NoArgsConstructor public class Goods {
private BigDecimal price; }
@Data public class GoodsDto {
private String price; }
@Mapper public interface GoodsMapper {
GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);
@Mapping(source = "price",target = "price",numberFormat = "¥#.00") GoodsDto toGoodsDto(Goods goods); }
@Test void testNumberFormat(){ Goods goods = new Goods(BigDecimal.valueOf(19.9)); GoodsDto goodsDto = GoodsMapper.INSTANCE.toGoodsDto(goods); log.info("goodsDto:{}",goodsDto); }
|
使用numberFormat
属性本质上就是使用java.text.DecimalFormat
来实现转换的。
下面是MapStruct
生成的toGoodsDto
方法代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class GoodsMapperImpl implements GoodsMapper {
@Override public GoodsDto toGoodsDto(Goods goods) { if ( goods == null ) { return null; }
GoodsDto goodsDto = new GoodsDto();
if ( goods.getPrice() != null ) { goodsDto.setPrice( createDecimalFormat( "¥#.00" ).format( goods.getPrice() ) ); }
return goodsDto; }
private DecimalFormat createDecimalFormat( String numberFormat ) {
DecimalFormat df = new DecimalFormat( numberFormat ); df.setParseBigDecimal( true ); return df; } }
|
在@IterableMapping
注解中也有numberFormat
属性,效果跟上面是一样的。
1 2
| @IterableMapping(numberFormat = "¥#.00") List<String> toPricesStr(List<Integer> price);
|
时间类型字段例如Date
,LocalDate
,LocalDateTime
等转String
类型,可以使用dateFormat
指定转换格式。
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 42
| @Data @AllArgsConstructor @NoArgsConstructor public class Goods {
private BigDecimal price; private Date createTime; }
@Data public class GoodsDto {
private String price; private String createTime;
}
@Mapper public interface GoodsMapper {
GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);
@Mapping(source = "price",target = "price",numberFormat = "¥#.00") @Mapping(source = "createTime",target = "createTime",dateFormat = "YYYY-MM-dd") GoodsDto toGoodsDto(Goods goods); }
@Test void testFormat(){ Goods goods = new Goods(BigDecimal.valueOf(19.9),new Date()); GoodsDto goodsDto = GoodsMapper.INSTANCE.toGoodsDto(goods); log.info("goodsDto:{}",goodsDto); }
|
- 如果是
Date
类型和String
互相转换就是使用SimpleDateFormat
类来实现的。
- 如果是
LocalDate
和LocalDateTime
互相转换就是使用DateTimeFormatter
来实现的
其他类型转换
在MapStruct
中还支持其他类的自动类型相互转换,常用的如下:
java.sql.Date
和 java.util.Date
java.sql.Time
和java.util.Date
java.sql.Timestamp
和 java.util.Date
String
和 StringBuilder
java.net.URL
和 String
.
java.util.UUID
and String
具体的可以参考[官方文档](MapStruct 1.5.1.Final Reference Guide)
映射引用对象
在平常的开发中,一个对象除了引用基本属性类型或者包装类型之外,还会引用其他对象,那么不同引用类型之间应该如何去转换?
例如下面的情况
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Data @AllArgsConstructor @NoArgsConstructor public class Car {
private Integer id;
private String carName;
private LocalDateTime createTime;
private List<String> carInfos;
private Person person; }
@Data @AllArgsConstructor @NoArgsConstructor public class Person {
private Integer id;
private Integer age;
private String name;
private LocalDateTime createTime; }
@Data public class CarDto {
private String carName;
private String publishTime;
private List<String> carInfos;
private PersonDto personDto; }
@Data public class PersonDto {
private Integer age;
private String name;
private String birthday; }
@Mapping(source = "createTime",target = "publishTime",dateFormat = "YYYY-MM-dd hh:mm:ss") @Mapping(source = "person",target = "personDto") CarDto carToCarDto(Car car);
|
在上面的例子中,要将Car
转换为CarDto
类,两边分别引用着Person
类型和PersonDto
类型。我们编译一下代码,就会神奇的发现多了一个personToPersonDto
的方法,这是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 28 29 30 31 32 33 34 35 36 37 38 39
| @Component public class CarMapperImpl implements CarMapper {
private final DateTimeFormatter dateTimeFormatter_YYYY_MM_dd_hh_mm_ss_11499725216 = DateTimeFormatter.ofPattern( "YYYY-MM-dd hh:mm:ss" );
@Override public CarDto carToCarDto(Car car) { if ( car == null ) { return null; }
CarDto carDto = new CarDto();
if ( car.getCreateTime() != null ) { carDto.setPublishTime( dateTimeFormatter_YYYY_MM_dd_hh_mm_ss_11499725216.format( car.getCreateTime() ) ); } carDto.setPersonDto( personToPersonDto( car.getPerson() ) ); carDto.setCarName( car.getCarName() ); List<String> list = car.getCarInfos(); if ( list != null ) { carDto.setCarInfos( new ArrayList<String>( list ) ); }
return carDto; }
protected PersonDto personToPersonDto(Person person) { if ( person == null ) { return null; }
PersonDto personDto = new PersonDto();
personDto.setAge( person.getAge() ); personDto.setName( person.getName() );
return personDto; } }
|
MapStruct在生成源对象和目标对象的映射方法时会遵循下列特点:
如果源属性和目标属性具有相同的类型,值将被简单地从源属性直接复制到目标属性。如果属性是一个集合(例如:List),那么集合的副本将被设置到目标属性中。
例如上面List<String>
类型字段之间的映射,直接new了一个ArrayList
出来并把源对象的属性作为构造参数传了进去。
如果源属性和目标属性的类型不同,那么就会检查是否存在另一个映射方法,方法参数是源属性的类型,返回类型是目标的属性类型。如果存在这样的方法,那么他会在映射方法中被调用。
例如在CarMapper
接口中这样定义一个方法,那么在实现类中就会调用它。
1 2
| @Mapping(source = "createTime",target = "birthday") PersonDto toPersonDto(Person person);
|
如果不存在这样的方法,MapStruct
将会检查目标类型的属性和源对象的属性是否可以自动转换。如果可以的话,那么他会在映射方法中被调用。
如果没有找到这样的方法,MapStruct
将尝试生成一个自动的映射方法,在源和目标属性之间进行映射。像上面的personToPersonDto
方法就是自动生成的映射方法。
如果MapStruct
无法创建一个该映射方法,那么在编译时将会报出错误信息。
自定义映射方法
一般情况下MapStruct
自动生成的代码就能够使用了,但在大部分情况下还是需要自定义映射方法,例如目标属性名和源属性名不匹配等情况。例如上面Person
和PersonDto
中的createTime
和birthday
字段一样。子映射方法和普通的映射方法使用是一样的,都是定义@Mapping
注解。
1 2
| @Mapping(source = "createTime",target = "birthday") PersonDto toPersonDto(Person person);
|
当然你也可以在源方法上定义@Mapping方法,不过官方还是建议在子映射方法上来定义@Mapping
注解
1 2 3 4
| @Mapping(source = "createTime",target = "publishTime",dateFormat = "YYYY-MM-dd hh:mm:ss") @Mapping(source = "person",target = "personDto") @Mapping(source = "person.createTime",target = "personDto.birthday") CarDto carToCarDto(Car car);
|
在一些情况下我们并不是要单纯的映射复制属性,而是需要通过一些自定义逻辑来生成一些属性值,这个时候就可以自定义方法逻辑体。例如下面我们给CarDto
加上一个CarInfoDto
类型的字段,我们需要通过车辆的发布时间来动态判断他是新品还是热销的车型。
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
| @Data @AllArgsConstructor @NoArgsConstructor public class CarInfoDto {
private String desc; }
@Data public class CarDto {
private String carName;
private String publishTime;
private List<String> carInfos;
private CarInfoDto carInfoDto;
private PersonDto personDto; }
|
我们在原来的CarMapper
上新增方法mapToCarInfoDto
,MapStruct
生成的实现类将会调用此方法来生成CarInfoDto
的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) public interface CarMapper {
@Mapping(source = "createTime",target = "publishTime",dateFormat = "YYYY-MM-dd hh:mm:ss") @Mapping(source = "person",target = "personDto") @Mapping(source = "person.createTime",target = "personDto.birthday") @Mapping(source = "car",target = "carInfoDto") CarDto carToCarDto(Car car);
@Mapping(source = "createTime",target = "birthday") PersonDto toPersonDto(Person person);
default CarInfoDto mapToCarInfoDto(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getYear()==createTime.getYear()?"新品":"热销"; return new CarInfoDto(desc); }
}
|
调用其他类的映射方法
在上面的dateFormat
的例子中,我们使用该属性定义时间类型和字符串类型之间的转换格式。如果在每个类中都定义一遍那就有点繁琐了,在MapStruct
中还支持调用其他类的映射方法。如下我们定义了一个DateMapper
类,定义了LocalDateTime
和String
类之间的转换方法,需要注意一点,如果调用该类的Mapper被CI所管理的话,那么该类也需要被CI管理,例如加个@Component
注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component public class DateMapper {
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern( "yyyy年MM月dd日 hh:mm:ss" );
public String toString(LocalDateTime localDateTime){ String dateTimeStr = dateTimeFormatter.format(localDateTime); return dateTimeStr; } public LocalDateTime toLocalDateTime(String dateTimeStr){ LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, dateTimeFormatter); return localDateTime; } }
|
定义完转换类之后,修改CarMapper方法
,在@Mapper
注解添加uses
属性,效果跟在@Mapping
中定义dateFormat
效果是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Mapper(componentModel = MappingConstants.ComponentModel.SPRING,uses = DateMapper.class) public interface CarMapper {
@Mapping(source = "createTime",target = "publishTime") @Mapping(source = "person",target = "personDto") @Mapping(source = "person.createTime",target = "personDto.birthday") @Mapping(source = "car",target = "carInfoDto") CarDto carToCarDto(Car car);
@Mapping(source = "createTime",target = "birthday") PersonDto toPersonDto(Person person);
default CarInfoDto mapToCarInfoDto(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getYear()==createTime.getYear()?"新品":"热销"; return new CarInfoDto(desc); }
}
|
方法冲突
在有些时候会出现多个映射方法签名相同的情况,例如下面的例子,双方的入参和返回参数都相同的时候。根据MapStruct
定义的映射规则,这时候会编译出错,因为不知道具体选择那个方法来作为映射。这个时候MapStruct
提供了@Named
注解来标识方法在通过@Mapping
注解的qualifiedByName
属性来解决。
1 2 3 4 5 6 7 8 9 10 11 12
| default CarInfoDto mapToCarInfoDtoByYear(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getYear()==createTime.getYear()?"新品":"热销"; return new CarInfoDto(desc); }
default CarInfoDto toCarInfoDtoByMonth(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getMonthValue()==createTime.getMonthValue()?"新品":"热销"; return new CarInfoDto(desc); }
|
具体使用代码如下
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
| @Mapper(componentModel = MappingConstants.ComponentModel.SPRING,uses = DateMapper.class) public interface CarMapper {
@Mapping(source = "createTime",target = "publishTime") @Mapping(source = "person",target = "personDto") @Mapping(source = "person.createTime",target = "personDto.birthday") @Mapping(source = "car",target = "carInfoDto",qualifiedByName = "toCarInfoDtoByMonth") CarDto carToCarDto(Car car);
@Mapping(source = "createTime",target = "birthday") PersonDto toPersonDto(Person person);
@Named("mapToCarInfoDtoByYear") default CarInfoDto mapToCarInfoDtoByYear(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getYear()==createTime.getYear()?"新品":"热销"; return new CarInfoDto(desc); }
@Named("toCarInfoDtoByMonth") default CarInfoDto toCarInfoDtoByMonth(Car car){ LocalDateTime createTime = car.getCreateTime(); String desc = LocalDateTime.now().getMonthValue()==createTime.getMonthValue()?"新品":"热销"; return new CarInfoDto(desc); }
}
|
小憩一下
在本章中介绍了MapStruct
中的数据类型的自动转换,numberFormat
和dateFormat
两个格式属性,以及相关的映射规则等。在下一节将会继续介绍MapStruct
的相关知识~。