MapStruct简易使用

304 阅读9分钟

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支持sourcetarget属性之间的数据类型转换。它还提供了基本类型及其相应的包装类之间的自动转换。

自动类型转换适用于:

  • 基本类型及其对应的包装类之间。比如, intIntegerfloatFloatlongLongbooleanBoolean 等。
  • 任意基本类型与任意包装类之间。如 intlongbyteInteger 等。
  • 所有基本类型及包装类与String之间。如 booleanStringIntegerStringfloatString 等。
  • 枚举和String之间。
  • Java大数类型(java.math.BigIntegerjava.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
    }
}