携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
首先,为什么要有这个?
这个是因为在项目中,经常要使用类型转换。
类型转换一般用BeanUtils,但是里面有个问题,比如是浅拷贝还是深拷贝,什么事前拷贝什么是深拷贝?如果有不一样的类型名了怎么办?
这些问题都是要思考的。
而且,BeanUtils有两个,用哪个?有个apache的,有个spring的。 两者有什么区别。为什么用这个而不用那个?
这个自己去查啊。我这里就不错记录了。
MapStruct就是用于这个类型转换的。
首先是引入的问题,这个新版本的引入和老版本的还不一样,废了我老长时间,最后还是直接上官网去查,才引进来。
他里面有和lombok版本兼容的问题,需要设置好版本。
详细的如下
maven配置
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.16</org.projectlombok.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- lombok dependency should not end up on classpath -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<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>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<!-- additional annotation processor required as of Lombok 1.18.16 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
这个是最新版的,支持jdk9.
虽然jdk都已经19了~~
然后就是实体类的编写。
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Source {
private String id;
private Integer num;
private String sourceName;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Target {
private String id;
private Integer num;
private String targetName;
}
这个是转换接口,在接口上标注上@Mapper,有什么字段不一样的话,就使用@Mapping注解, 里面的source就是被转换的类型的原本名字,target就是转换后的名字。
然后这个方法返回类型,就是你要的类型,传入的类型是需要转换的类型,
@Mapper
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
@Mapping(source = "sourceName",target = "targetName")
Target source2target(Source source);
}
字段忽略
如果我们想不拷贝一些东西呢?比如一些公共的抽取出来的字段,id啊createTime,updateTime,这些怎么办呢?
有两种方法
第一种,直接写一个mapping
写上忽略的字段,然后就可以了。
这个是测试结果,可以发现,id为null,没有赋上值。
但是这样一个还行,如果有多个mapping的话,难道要一个一个写吗?
答案是no,我们可以抽象出一个注解,这个注解里面有我们想要忽略的字段。
注意这个注解里面可不单单可以写忽略的字段,还可以进行其他操作。 具体的可以看官网MapStruct 1.5.2.Final Reference Guide 下面是我的演示
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
//@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }
写好这个类之后,添加上方法上就可以
@Mapper
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
@Mapping(source = "sourceName",target = "targetName")
// @Mapping(target = "id",ignore = true)
@ToEntity
Target source2target(Source source);
}
再次测试,发现也是完全没有问题的。id还是null。
向映射器添加自定义方法
在某些情况下,可能需要手动实现一个从一种类型到另一种类型的特定映射,而这种映射是 MapStruct 无法生成的。处理这个问题的一种方法是在另一个类上实现自定义方法,然后由 MapStruct 生成的映射器使用该方法。
@Mapper
public abstract class CarMapper {
@Mapping(...)
...
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
怎么使用呢?
例
这是两个实体类,标红的就是要玩的。
这个需要在定义一个接口
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
//这个里面就是自定义的
default UserDTO userToUserDTO(User user) {
//hand-written mapping logic
return new UserDTO(user.getId(), user.getName(),user.getPassword(),null);
}
}
这个是实验结果,可以看出,跟我们自己定义的一样。
具有多个源参数的映射方法
这个是什么意思呢,就是把两个类聚合成1个,因为有时候视图上需要的信息很多,我们需要从两个类中取数据,然后再聚合成一个类,最后返回。
废话不多说,上代码
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "address.houseNo")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
上面这个是官网的例子
那我们来实现一下。
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Address {
private String houseNo;
private String street;
private String city;
private String house;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class DeliveryAddressDto {
private String description;
private String houseNumber;
private String name;
private Integer age;
private Integer gender;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
private String name;
private Integer age;
private Integer gender;
private String description;
}
重点来了
测试
Person person = new Person("John",20,1,"handsome");
Address address = new Address("1103","chenghuadadao","beijing","bieshu");
DeliveryAddressDto deliveryAddressDto = AddressMapper.INSTANCE.personAndAddressToDeliveryAddressDto(person, address);
System.out.println(deliveryAddressDto);
结果:
只有mapping上的字段,他才有显示,其他的都为null。也就是说我们要都赋值的话还得去一个一个点,他不能自动识别那个类的那个字段是这个dto里面的。
map映射到bean
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
@Mapper
public interface CustomerMapper {
@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {
@Override
public Customer toCustomer(Map<String, String> map) {
// ...
if ( map.containsKey( "id" ) ) {
customer.setId( Integer.parseInt( map.get( "id" ) ) );
}
if ( map.containsKey( "customerName" ) ) {
customer.setName( map.get( "customerName" ) );
}
// ...
}
}
这个里面就是在实现类里面写的具体逻辑。
类型转换
涉及到实体类中的不同类型的转换他是怎么做的呢?
官网上是这么说的:
MapStruct takes care of type conversions automatically in many cases. If for instance an attribute is of type
intin the source bean but of typeStringin the target bean, the generated code will transparently perform a conversion by callingString#valueOf(int)andInteger#parseInt(String), respectively.
大概意思就是如果是int转string,会用Integer#parseInt(String),如果是string转int会用String # valueOf (int)
映射集合
生成的代码将包含一个循环,循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或它使用的映射器中找到集合元素类型的映射方法,则调用此方法来执行元素转换。或者,如果存在源和目标元素类型的隐式转换,则将调用此转换例程。下面是一个例子:
@Mapper
public interface CarMapper {
Set<String> integerSetToStringSet(Set<Integer> integers);
List<CarDto> carsToCarDtos(List<Car> cars);
CarDto carToCarDto(Car car);
}
具体怎么变得
//GENERATED CODE
@Override
public Set<String> integerSetToStringSet(Set<Integer> integers) {
if ( integers == null ) {
return null;
}
Set<String> set = new LinkedHashSet<String>();
for ( Integer integer : integers ) {
set.add( String.valueOf( integer ) );
}
return set;
}
@Override
public List<CarDto> carsToCarDtos(List<Car> cars) {
if ( cars == null ) {
return null;
}
List<CarDto> list = new ArrayList<CarDto>();
for ( Car car : cars ) {
list.add( carToCarDto( car ) );
}
return list;
}
支持默认值插入
例子
imports java.util.UUID;
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}
大概用的就这么多,至于后面的定制啊,综合配置啊,可以去翻文档,在后面都有写,这里感觉目前用不到,就不做研究了。