1-1 mpg生成Dao接口和PO对象
1-2 MapStruct完成po/dto/vo的互相转换
Maven 项目引入 MapStruct
注意:
由于 MapStruct 依赖于 JavaBean 中有 getter/setter 方法,所以,如果使用了 lombok 来生成 getter/setter 方法的话,那么需要 lombok 先与 MapStruct “起作用”。为了确保 lombok “先与 MapStruct 起作用”这需要在 maven 项目的 pom.xml 中的 plugins > plugin 下的 maven-compiler-plugin 插件下加上好大一坨 configuration 配置。
好在,虽然很繁琐,但是都是直接复制粘贴的事,不需要我们改动什么。
第 1 步:引入 pom 依赖
<properties>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.16</org.projectlombok.version>
… 其它版本声明
</properties>
<dependencies>
<!-- lombok dependencies should not end up on classpath -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
… 其它依赖声明
</dependencies>
第 2 步:配置 maven 插件
<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.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>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<!-- 复制的结束 -->
</configuration>
</plugin>
… 其它插件声明
</plugins>
</build>
案例:最简单情况:PO 和 DTO 一模一样
第一步:准备好“源”
首先我们准备好 PO 类 Department:
@Data
@TableName("department")
public class Department {
private Long id;
private String name;
private String location;
}
第二步:准备好“目标”
然后我们再准备好 DTO 类 DepartmentDto,两者的属性数量、类型甚至名称都是一样的:
@Data
public class DepartmentDto {
private Long id;
private String name;
private String location;
}
第三步:准备好转换器
最后我们准备好转换工具类 Mapper/Converter:
import org.mapstruct.Mapper;
/**
* 转换器,就MapStruct的格式完成
* 1.转换器上面添加@Mapper注解,不要和ibatis下面的@Mapper注解混淆
* 2.提供转换方法
*/
@Mapper(componentModel = "spring") //componentModel设置转换器对象获取的方式:spring
public interface DepartmentConverter {
//单个po-->单个dto
// 目标类型 自定义方法名(源类型 姓名自定会议);
DepartmentDto from(Department po);
//多个po-->多个dto List<po>--->List<dto>
// List<目标类型> 自定义方法名(List<源类型> 姓名自定会议);
List<DepartmentDto> from(List<Department> pos);
}
以上代码的编写要求
- 类名任意;
- 方法名任意;
- 参数和返回值类型要符合逻辑【即将什么类型转换为什么类型,这个认知决定了方法形参类型和方法返回值类型】。
第四步:使用和验证
@SpringBootTest
class MallFrontApplicationTests {
@Autowired
private DepartmentConverter departmentConverter;
/**
* po--->Dto的代码
*/
@Test
public void contextLoads() {
Department po = Department.builder()
.id(1l)
.name("java")
.location("青岛").build();
DepartmentDto dto =departmentConverter.from(po);
System.out.println(dto);
System.out.println("=============================================");
List<Department> poList=Arrays.asList(
Department.builder()
.id(1l)
.name("java")
.location("武汉").build(),
Department.builder()
.id(2l)
.name("python")
.location("烟台").build(),
Department.builder()
.id(3l)
.name("web")
.location("哈尔滨").build()
);
List<DepartmentDto> dtoList =departmentConverter.from(poList);
System.out.println(dtoList);
}
}
[注意] 提示
其实 MapStruct 的实现原理很简单,就是根据我们在 Mapper 接口中使用的 @Mapper 和 @Mapping 等注解,在运行时生成接口的实现类,写完接口后,可以利用compile命令编译后,打开项目的 target 目录查看xxxConverter对应的xxxConverterImpl实现类代码。
DTO 和 PO 一丢丢不一样
-
情况一:属性数量不一样。
通常页面上常常不需要返回那么多的数据,所以 DTO 的属性可能比 PO 属性少。这种情况最简单,上面的代码不需要做任何变化即可。删除 DepartmentDto 中的 location 属性,验证。
-
情况二:属性名称不一样。
这种情况下,需要在转换类的转换方法上额外使用 @Mapping 注解,标识出不一样的那些些属性(一样的属性不用标识)。例如:
情况一和情况二的解决办法:
@Data
public class DepartmentDto {
private Long id;
private String departmentName; // 和表的字段名不一样
// private String location; // 少了一个字段
}
@Mapper
public interface DepartmentDtoConverter {
@Mapping(source = "name", target = "departmentName")
DepartmentDto from(Department po);
@Mapping(source = "name", target = "departmentName")
List<DepartmentDto> from(List<Department> po);
}
-
情况三:数据类型不一样。
这种情况最常见是出现在 po 类是日期时间类型,而 dto 是 String 类型上。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDto {
private String hireDate; // 表和 PO 类中是 date 类型
… 其他属性
}
@Mapper
public interface EmployeeDtoConverter {
// @Mapping 注解的 source 属性可省略。不过写上逻辑更清晰一些。
@Mapping(target = "hireDate", dateFormat = "yyyy-MM-dd")
EmployeeDto from(Employee po);
@Mapping(target = "hireDate", dateFormat = "yyyy-MM-dd")
List<EmployeeDto> from(List<Employee> po);
}
[注意] 提示
上面三种情况可能会参杂在一起。
MapStruct使用expression处理某些属性使用特定值的情况
第一步:准备好“源”
首先我们准备好 PO 类 Department:
@Data //getter和setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Department {
private Long id;
private String name;
private String location;
private LocalDateTime createTime;
private Integer status;//状态 1 2
}
第二步:准备好“目标”
然后我们再准备好 DTO 类 DepartmentDto,此处的特点是:status变成了String类型,我们希望dto中status保存“正常”或“异常”格式:
@Data
public class DepartmentDto {
private Long id;
private String name;
private String loc;
private String myTime;
private String status;
}
第三步:准备好转换器
最后我们准备好转换工具类 Mapper/Converter:
@Mapper(componentModel = "spring",uses = EmployeeConverter.class) //componentModel设置转换器对象获取的方式:spring
public interface DepartmentConverter {
//单个po-->单个dto
// 目标类型 自定义方法名(源类型 姓名自定会议);
@Mapping(source = "location",target = "loc")
@Mapping(source = "createTime",target = "myTime",dateFormat = "yyyy年MM月dd日 HH:mm:ss")
@Mapping(target = "status",expression = "java(po.getStatus()==1?\"正常\":\"禁用\")")
@Mapping(target = "empDtoList",source = "empPoList")
DepartmentDto from(Department po);
//多个po-->多个dto List<po>--->List<dto>
// List<目标类型> 自定义方法名(List<源类型> 姓名自定会议);
@Mapping(source = "location",target = "loc")
@Mapping(source = "createTime",target = "myTime",dateFormat = "yyyy年MM月dd日 HH:mm:ss")
//使用expression指定表达式的值赋给target属性,所以不要再写source了
@Mapping(target = "status",expression = "java(po.getStatus()==1?\"正常\":\"禁用\")")
List<DepartmentDto> from(List<Department> pos);
}
第四步:使用和验证
@SpringBootTest
class WoniumallFrontApplicationTests {
@Autowired
private DepartmentConverter departmentConverter;
/**
* po--->Dto的代码
*/
@Test
public void contextLoads() {
Department po = Department.builder()
.id(1l)
.name("java")
.status(1)
.location("青岛").build();
DepartmentDto dto =departmentConverter.from(po);
System.out.println(dto);
}
}
MapStruct完成级联对象的转换
第一步:准备好“源”
首先我们准备好 PO 类 Employee,其内部提供了一个Department的实体类属性:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee {
private Integer id;
private String empName;
//员工和部门的实体关系:1-1
/**
* po里面如果要建议其他对象的引用,内部的引用对象类型一定是po
*/
private Department department;
}
第二步:准备好“目标”
然后我们再准备好 DTO 类 EmployeeDto,此处的特点是:员工所属部门是DepartmentDto对象
@Data
public class EmployeeDto {
private Integer id;
private String empName;
//员工和部门的实体关系:1-1
/**
* po里面如果要建议其他对象的引用,内部的引用对象类型一定是po
*/
private DepartmentDto departmentDto;
}
第三步:准备好转换器
最后我们准备好转换工具类 Mapper/Converter:
/**
* 因为EmployeeDto中有一个DepartmentDto,所以声明转换器需要指定内部对象的转换格式
*/
@Mapper(componentModel = "spring",uses = DepartmentConverter.class ) //uses指定当前转换器要引用其他转换器协助完成转换工作
public interface EmployeeConverter {
//定义转换方法
@Mapping(source = "department",target = "departmentDto")
EmployeeDto from(Employee po);
@Mapping(source = "department",target = "departmentDto")
List<EmployeeDto> from(List<Employee> po);
}
第四步:使用和验证
@SpringBootTest
class WoniumallFrontApplicationTests {
@Autowired
private EmployeeConverter employeeConverter;
@Test
public void contextLoads3() {
Employee emp=new Employee().builder()
.id(1001)
.empName("小心")
.department(
Department.builder()
.id(1l)
.name("java")
.location("青岛")
.status(1)
.createTime(LocalDateTime.now()).build()
)
.build();
System.out.println(employeeConverter.from(emp));
}
}
分页查询的结果映射
PageInfo PageInfo
└──> PO ==> └──> DTO
对于 mybatis 的 page-helper 的分页查询的结果,如果需要映射,需要使用上一章的子对象映射功能。将 PageInfo<PO> 转换成 PageInfo<Dto> 。
[!cite] 提示
解决思路就是上一个案例中级联对象操作的方案
@Mapper(componentModel = "spring", uses = DepartmentDtoConverter.class)
public interface EmployeeDtoConverter {
@Mapping(source = "department",target = "departmentDto")
EmployeeDto from(Employee po);
@Mapping(source = "department",target = "departmentDto")
List<EmployeeDto> from(List<Employee> poList);
@Mapping(source = "list", target = "list")
PageInfo<EmployeeDto> from(PageInfo<Employee> poPage);
}
MapStruct扩展了解
MapStruct 使用对象工厂
默认情况下,MapStruct 使用的是对象的默认构造器(即,无参构造器)创建对象,而后,再对其属性进行复制。
但是,有时候我们不希望 MapStruct 使用默认构造来创建目标对象,而是使用对象工厂。最典型的场景就是:需要向 new 出来的对象中注入一个单利对象。比如,向 Java Bean 中注入 Repository 或者是“更厉害”的 ApplicationContext 。
这种情况下,需要结合 @ObjectFactory 注解和 @Mapper 注解的 uses 属性来实现。
首先,为目标对象准备好工厂类,例如:
@Component
@RequiredArgsConstructor
public class DepartmentFactory {
private final ApplicationContext applicationContext;
@ObjectFactory // <- 看这里
public Department createDepartment() {
System.out.println("通过 DepartmentFactory 创建 Department 对象");
return new Department(applicationContext);
}
}
在工厂方法上标注 @ObjectFactory 注解。
然后,在转换器的 @Mapper 注解中使用 ueses 属性来“告知”MapStruct 使用这个工厂类(的工厂)方法来创建目标对象。
@Mapper(componentModel = "spring", uses = DepartmentFactory.class)
public interface DepartmentConverter {
Department from(DepartmentPo po);
}
另外,在更复杂的情况中,如果有两个类有引用关系,需要转换器转换,这里的工厂类和之前的内容不冲突:
@Mapper(componentModel = "spring", uses = {
EmployeeFactory.class,
DepartmentConverter.class
})
public interface EmployeeConverter {
Employee from(EmployeePo po);
}
在上面的例子中,使用工厂类 EmployeeFactory 来创建 Employee 类的对象,而 Employee 对象有 deparmtent 属性,它又是利用 DepartmentConverter 转换器创建出来的。
合并映射
MapStruct 也支持把多个对象属性“聚拢”到一个对象中去。
- 例如这里把 Member 和 Order 的部分属性映射到 MemberOrderDto 中去;
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
private String orderSn;
private String receiverAddress;
}
- 然后在 Mapper 中添加 toMemberOrderDto 方法,这里需要注意的是由于参数中具有两个属性,需要通过
<参数名称>.<属性>
的名称来指定 source 来防止冲突(这两个参数中都有 id 属性);
@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(source = "member.phone", target = "phoneNumber")
@Mapping(source = "member.birthday", target = "birthday",dateFormat = "yyyy-MM-dd")
@Mapping(source = "member.id", target = "id")
@Mapping(source = "order.orderSn", target = "orderSn")
@Mapping(source = "order.receiverAddress", target = "receiverAddress")
MemberOrderDto toMemberOrderDto(Member member, Order order);
}
- 接下来在 Controller 中创建测试接口,直接通过 Mapper 中的 INSTANCE 实例调用转换方法 toMemberOrderDto;
略
使用常量、默认值和表达式
使用 MapStruct 映射属性时,我们可以设置属性为常量或者默认值,也可以通过 Java 中的方法编写表达式来自动生成属性。
- 例如下面这个商品类 Product 对象;
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
private Long id;
private String productSn;
private String name;
private String subTitle;
private String brandName;
private Double price;
private Integer count;
}
- 我们想把 Product 转换为 ProductDto 对象,id 属性设置为常量,count 设置默认值为 1 ,productSn 设置为 UUID 生成;
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDto {
private Long id; // 使用常量
private String productSn; // 使用表达式生成属性
private String name;
private String subTitle;
private String brandName;
private Double price;
private Integer count; // 使用默认值
}
- 创建 ProductMapper 接口,通过 @Mapping 注解中的 constant、defaultValue、expression 设置好映射规则;
@Mapper(imports = {UUID.class})
public interface ProductMapper {
ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);
@Mapping(target = "id", constant = "-1L")
@Mapping(target = "count", source = "count", defaultValue = "1")
@Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product);
}
ProductDtoConverter instance = ProductDtoConverter.INSTANCE;
Product product = new Product(1L, "aaa", "bbb", "ccc", "ddd", 1.2, null);
ProductDto dto = instance.toDto(product);
System.out.println(dto);
在映射前后进行自定义处理
MapStruct 也支持在映射前后做一些自定义操作,类似 AOP 中的切面。
由于此时我们需要创建自定义处理方法,创建一个抽象类 ProductRoundMapper ,通过 @BeforeMapping 注解自定义映射前操作,通过 @AfterMapping 注解自定义映射后操作;
@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
public static ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);
@Mapping(target = "id",constant = "-1L")
@Mapping(source = "count",target = "count",defaultValue = "1")
@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
public abstract ProductDto toDto(Product product);
@BeforeMapping
public void beforeMapping(Product product){
//映射前当price<0时设置为0
if(product.getPrice().compareTo(BigDecimal.ZERO)<0){
product.setPrice(BigDecimal.ZERO);
}
}
@AfterMapping
public void afterMapping(@MappingTarget ProductDto productDto){
//映射后设置当前时间为createTime
productDto.setCreateTime(new Date());
}
}
- 测试
略
处理映射异常
代码运行难免会出现异常,MapStruct 也支持处理映射异常。
- 我们需要先创建一个自定义异常类;
public class ProductValidatorException extends Exception {
public ProductValidatorException(String message) {
super(message);
}
}
- 然后创建一个验证类,当 price 设置小于 0 时抛出我们自定义的异常;
public class ProductValidator {
public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
if(price.compareTo(BigDecimal.ZERO)<0){
throw new ProductValidatorException("价格不能小于0!");
}
return price;
}
}
- 之后我们通过 @Mapper 注解的 uses 属性运用验证类;
@Mapper(uses = {ProductValidator.class},imports = {UUID.class})
public interface ProductExceptionMapper {
ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);
@Mapping(target = "id",constant = "-1L")
@Mapping(source = "count",target = "count",defaultValue = "1")
@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
ProductDto toDto(Product product) throws ProductValidatorException;
}
- 然后在 Controller 中添加测试接口,设置 price 为 -1 ,此时在进行映射时会抛出异常;