MapStruct是一个Java Bean映射器,它可以在编译时自动生成两个Java Bean之间的映射代码。你可以使用注解来定义映射接口或抽象类,MapStruct会根据你的定义创建具体的实现。MapStruct支持简单类型和集合类型的映射,也可以自定义特殊的映射逻辑。
优点
使用MapStruct的原因有以下几点:
- MapStruct可以自动地根据注解生成映射实现类,减少了手写映射代码的工作量和出错的可能性。
- MapStruct生成的映射代码使用简单的方法调用,速度快,类型安全,易于理解和维护。
- MapStruct支持基于约定优于配置的方式进行映射,也可以通过注解自定义映射规则和表达式。
- MapStruct在编译时生成映射代码,可以在开发阶段就发现错误,并且不会增加运行时的开销。
局限性
MapStruct的局限性可能有以下几点:
- MapStruct只能映射Java Bean,不能映射其他类型的对象,如XML或JSON。
- MapStruct需要在编译时生成映射代码,这可能会增加编译时间和项目大小。
- MapStruct可能与其他使用注解处理器的依赖发生冲突,需要手动排除
MapStruct vs 其他Java Bean映射器
MapStruct和其他Java Bean映射器的区别主要有以下几点:
- MapStruct是一个注解处理器,它在编译时生成映射代码,而不是在运行时使用反射或字节码操作。这样可以提高性能和类型安全性。
- MapStruct使用简单的注解来定义映射接口,而不需要复杂的XML配置或API调用。这样可以提高可读性和维护性。
- MapStruct支持自定义映射逻辑,可以通过使用其他映射器或表达式来实现。这样可以提高灵活性和扩展性。
简单使用流程
要在你的项目中使用MapStruct,你需要做以下几个步骤:
1、在你的项目中添加MapStruct的依赖。
这里以Maven为例,添加以下依赖:
<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>
2、在你的项目中创建映射接口或抽象类,并使用@Mapper注解标记。
假设在某项业务中,我们有两个Java Bean,一个是Calculator,一个是CalculatorDto,它们的属性如下所示:
@Data
public class Calculator {
public String address;
public String publicKey;
public double balance;
public String operation;
}
@Data
public class CalculatorDto {
public String identity;
public String publicKey;
public double balance;
}
我们想要将Calculator对象映射为CalculatorDto对象,首先需要创建映射器,首先需要定义一个映射接口,并且使用org.mapstruct.Mapper注解进行注释,代码如下所示:
@Mapper
public interface CalculatorMapper {
/**
* 获取该类自动生成的实现类的实例
* 接口中的属性都是 public static final 的 方法都是public abstract的
*/
CalculatorMapper INSTANCES = Mappers.getMapper(CalculatorMapper.class);
}
3、在映射接口或抽象类中定义映射方法,并使用@Mapping或@Mappings注解指定源和目标属性之间的映射关系。
然后,在映射接口中,定义一个用于实现对象属性复制的方法。在实际应用场景中,可能会出现映射对象与被映射对象的属性名称不一致、对象属性值为空等情形。为了解决这些问题,需要使用@Mapping或@Mappings注解指定源和目标属性之间的映射关系。
在生成的方法实现中,source类型的所有可读属性都将复制到target类型的相应属性中
- 当一个属性于其target实体对应的名称相同时,它将被隐式映射;
- 当属性在target实体中具有不同的名称时,可以通过@Mapping注释指定其名称。
完成该步后,映射接口代码如下所示:
@Mapper
public interface CalculatorMapper {
CalculatorMapper INSTANCES = Mappers.getMapper(CalculatorMapper.class);
/**
* 这个方法就是用于实现对象属性复制的方法
*
* @Mapping 用来定义属性复制规则 source 指定被映射对象属性 target指定映射对象属性
*
* @param calculator 输入为被映射对象,也就是需要被复制的对象
* @return 返回值为映射对象
*/
@Mappings({
@Mapping(source = "address", target = "identity"),
@Mapping(source = "publicKey", target = "publicKey"),
@Mapping(source = "balance", target = "balance")
})
CalculatorDto toCalculatroDto(Calculator calculator);
}
4、在编译时,MapStruct会自动生成映射接口或抽象类的实现类,并在其中实现映射方法。
在编译时,MapStruct会自动生成这个接口的实现类,并在其中实现映射方法。我们可以在target/classes/…/MapStruct/中查看由MapStruct自动生成的映射实现类CalculatorMapper:
public class CalculatorMapperImpl implements CalculatorMapper {
public CalculatorMapperImpl() {
}
public CalculatorDto toCalculatroDto(Calculator calculator) {
if (calculator == null) {
return null;
} else {
CalculatorDto calculatorDto = new CalculatorDto();
calculatorDto.identity = calculator.address;
calculatorDto.publicKey = calculator.publicKey;
calculatorDto.balance = calculator.balance;
return calculatorDto;
}
}
}
可以看到MapStruct自动为我们实现了对象属性的复制操作。
5、在你的代码中调用映射方法,将一个Java Bean转换为另一个Java Bean。
CalculatorDto calculatorDto = CalculatorMapper.INSTANCES.toCalculatroDto(calculator);
System.out.println(calculatorDto.toString());
进阶
默认值
有时候,在转换过程中,可能因为空值或其他原因使得映射结果不正确,此时可以指定一个默认值,防止程序出错。
@Mappings( {
@Mapping(target = "balance", source = "balance", defaultValue = "20")
})
数据类型转换
映射属性在源对象和目标对象中并不总是具有相同的类型。 例如,一个属性在源bean中可能是int类型,而在目标bean中可能是Long类型。
隐式类型转换
MapStruct支持source和target属性之间的数据类型转换。它还提供了基本类型及其相应的包装类之间的自动转换。
自动类型转换适用于:
- 基本类型及其对应的包装类之间。比如,
int和Integer,float和Float,long和Long,boolean和Boolean等。 - 任意基本类型与任意包装类之间。如
int和long,byte和Integer等。 - 所有基本类型及包装类与
String之间。如boolean和String,Integer和String,float和String等。 - 枚举和
String之间。 - Java大数类型(
java.math.BigInteger,java.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则MapStrcut会自行处理类型转换。
如果不符合自动类型转换规则,则需要用户自行声明转换格式。
日期类型转换
假设有一个计算任务类ComputingTask,需要映射为DTO类ComputingTaskDTO:
@Data
public class ComputingTask{
public Date startTime;
public Date endTime;
public String modelName;
}
@Data
public class ComputingTaskDTO{
public String startTime;
public String endTime;
public String modelName;
}
由上可见,源类与目标类中的时间字段需要实现Date类型到String类型的转换。利用MapStruct,我们可以这样定义映射方法:
@Mapper
public interface ComputingTaskMapper {
ComputingTaskMapper INSTANCES = Mappers.getMapper(ComputingTaskMapper.class);
@Mappings({
@Mapping(source = "startTime", target = "startTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(source = "endTime", target = "endTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(source = "modelName", target = "modelName")
})
ComputingTaskDTO toComputingTaskDTO(ComputingTask computingTask);
}
自定义逻辑转换
MapStruct允许调用自定义逻辑的转换方法。
expression
可以使用expression(表达式)来实现由源字段类型到目标字段类型的转换。
简单示例代码如下所示:
@Data
public class ComputingTask {
public int taskId;
public String[] params;
public JSONObject result;
}
@Data
public class ComputingTaskDTO {
public int taskId;
public List<String> params;
public String value;
}
可以看出,ComputingTask与ComputingTaskDTO中的params字段存在类型不一致的情况。同时,ComputingTaskDTO中的value字段是由ComputingTask中的result字段获取的。我们可以使用expression简单实现这些字段的类型转换:
@Mapper
public interface ComputingTaskMapper {
ComputingTaskMapper INSTANCE = Mappers.getMapper(ComputingTaskMapper.class);
@Mappings({
@Mapping(source = "taskId", target = "taskId"),
//这里使用表达式,将String数组类型的参数转换成List类型
@Mapping(target = "params", expression = "java(java.util.Arrays.asList(computingTask.getParams()))"),
//调用自定义方法实现字段类型转换
@Mapping(target = "value", expression = "java(ComputingTaskMapper.getValue(computingTask.getResult()))")
})
ComputingTaskDTO toComputingTaskDTO(ComputingTask computingTask);
static String getValue(JSONObject result){
//这里写具体的业务代码
return result.getString("value");
}
}
qualifiedByName
我们也可以通过qualifiedByName标识转换规则,让MapStruct使用Mapper接口中的指定的默认方法来完成字段属性的转换。
还是上面的例子:
@Mapper
public interface ComputingTaskMapper {
ComputingTaskMapper INSTANCE = Mappers.getMapper(ComputingTaskMapper.class);
@Mappings({
@Mapping(source = "taskId", target = "taskId"),
//这里使用表达式,将String数组类型的参数转换成List类型
@Mapping(target = "params", expression = "java(java.util.Arrays.asList(computingTask.getParams()))"),
//使用qualifiedByName实现字段类型转换
@Mapping(source = "result", target = "value", qualifiedByName = "getValue")
})
ComputingTaskDTO toComputingTaskDTO(ComputingTask computingTask);
@Named("getValue")
default String getValue(JSONObject result){
//这里写具体的业务代码
return result.getString("value");
}
}
多个源类
MapStruct同样支持多个源参数的映射方法。在一些实际业务编码的过程中,不可避免地需要将多个对象转化为一个对象的场景,MapStruct也能很好的支持,对于这种最终返回信息来源于多个类,我们可以通过配置来实现多对一的转换。简单示例代码如下所示:
@Data
public class Calculator{
public String address;
public String publicKey;
public double balance;
}
@Data
public class CalculationModel{
public String modelName;
public double price;
}
@Data
public Class CalculatorDto{
public String publicKey;
public double balance;
public String modelName;
}
@Mapper
public interface CalculatorMapper {
CalculatorMapper INSTANCES = Mappers.getMapper(CalculatorMapper.class);
@Mappings({
@Mapping(source = "publicKey", target = "Calculator.publicKey"),
@Mapping(source = "balance", target = "Calculator.balance"),
@Mapping(source = "modelName", target = "CalculationModel.modelName")
})
CalculatorDto toCalculatroDto(Calculator calculator,CalculationModel calculationModel);
}
多层嵌套映射
有时候,我们需要多层映射,即源类也拥有自己的类,然后我们需要将源类中的包含类中的某些参数赋值给目标类。简单示例代码如下所示:
@Data
public class ComputingTask{
public int taskId;
public Date startTime;
public Date endTime;
public ComputingResult computingResult;
}
@Data
public class ComputingResult{
public String key;
public String value;
}
@Data
public Class ComputingTaskDto{
public int taskId;
public Date startTime;
public Date endTime;
public String value;
}
@Mapper
public interface ComputingTaskMapper {
ComputingTaskMapper INSTANCES = Mappers.getMapper(ComputingTaskMapper.class);
@Mappings({
@Mapping(source = "taskId", target = "taskId""),
@Mapping(source = "startTime", target = "startTime"),
@Mapping(source = "endTime", target = "endTime"),
//可以在Mapper类中使用符号"."的方式进行映射。
@Mapping(source = "computingTask.computingResult.value", target = "value")
})
ComputingTaskDTO toComputingTaskDTO(ComputingTask computingTask);
}
添加默认方法
在某些情况下,可能需要手动实现某种特定的MapStruct无法自动生成的Java Bean映射。 一种处理方法是在另一个类上实现自定义方法,然后由MapStruct生成的映射器使用这个类。 或者,当使用Java 8或更高版本时,你可以直接在映射器接口中实现自定义方法作为默认方法。 如果参数和返回类型匹配,生成的代码将调用默认方法。
举一个例子,假设从Calculator到CalculatorDto的映射存在一些MapStruct无法生成的特殊逻辑。然后你可以如此定义映射器:
@Mapper
public interface CalculatorMapper {
@Mapping(...)
...
CalculatorDto toCalculatroDto(Car car);
default CalculatorDto toCalculatroDto(CalculatorDto calculatorDto) {
//hand-written mapping logic
}
}