如何自定义ModelMapper

Joh*_*kel 8 java rest modelmapper

我想使用ModelMapper将实体转换为DTO并返回.它主要是有效的,但我如何自定义它.它有很多选择,很难弄清楚从哪里开始.什么是最佳做法?

我将在下面自己回答,但如果另一个答案更好,我会接受它.

Joh*_*kel 62

首先是一些链接

我对mm的印象是它的设计非常好.代码很扎实,很高兴阅读.但是,文档非常简洁,只有很少的例子.api也令人困惑,因为似乎有10种方法可以做任何事情,而且没有迹象表明你为什么要这样或那样做.

有两种选择:Dozer是最受欢迎的,而Orika因易于使用而获得好评.

假设你还想使用mm,这就是我对它的了解.

主要类ModelMapper应该是您应用中的单例.对我来说,这意味着使用Spring的@Bean.对于简单的情况,它开箱即用.例如,假设您有两个类:

class DogData
{
    private String name;
    private int mass;
}

class DogInfo
{
    private String name;
    private boolean large;
}
Run Code Online (Sandbox Code Playgroud)

与适当的getter/setters.你可以这样做:

    ModelMapper mm = new ModelMapper();
    DogData dd = new DogData();
    dd.setName("fido");
    dd.setMass(70);
    DogInfo di = mm.map(dd, DogInfo.class);
Run Code Online (Sandbox Code Playgroud)

并且"名称"将从dd复制到di.

有许多方法可以自定义mm,但首先您需要了解它的工作原理.

mm对象包含每个有序类型对的TypeMap,例如<DogInfo,DogData>和<DogData,DogInfo>将是两个TypeMaps.

每个TypeMap都包含一个带有映射列表的PropertyMap.因此,在示例中,mm将自动创建一个TypeMap <DogData,DogInfo>,其中包含一个具有单个映射的PropertyMap.

我们可以写这个

    TypeMap<DogData, DogInfo> tm = mm.getTypeMap(DogData.class, DogInfo.class);
    List<Mapping> list = tm.getMappings();
    for (Mapping m : list)
    {
        System.out.println(m);
    }
Run Code Online (Sandbox Code Playgroud)

它会输出

PropertyMapping[DogData.name -> DogInfo.name]
Run Code Online (Sandbox Code Playgroud)

当你调用mm.map()时,这就是它的作用,

  1. 查看TypeMap是否存在,如果没有为<S,D>源/目标类型创建TypeMap
  2. 调用TypeMap Condition,如果返回FALSE,则不执行任何操作并停止
  3. 如有必要,调用TypeMap Provider来构造新的目标对象
  4. 如果有TypeMap PreConverter,请调用它
  5. 执行以下操作之一:
    • 如果TypeMap有自定义Converter,请调用它
    • 或者,生成一个PropertyMap(基于配置标志加上添加的任何自定义映射),并使用它(注意:TypeMap还有可选的自定义Pre/PostPropertyConverters,我认为每次映射之前和之后此时都会运行.)
  6. 如果有TypeMap PostConverter,请调用它

警告:这个流程图有点记录,但我不得不猜测很多,所以它可能不正确!

您可以自定义此过程的每个步骤.但最常见的两个是

  • 步骤5a. - 编写自定义TypeMap转换器,或
  • 步骤5b. - 编写自定义属性映射.

以下是自定义TypeMap转换器的示例:

    Converter<DogData, DogInfo> myConverter = new Converter<DogData, DogInfo>()
    {
        public DogInfo convert(MappingContext<DogData, DogInfo> context)
        {
            DogData s = context.getSource();
            DogInfo d = context.getDestination();
            d.setName(s.getName());
            d.setLarge(s.getMass() > 25);
            return d;
        }
    };

    mm.addConverter(myConverter);
Run Code Online (Sandbox Code Playgroud)

请注意,转换器是单向的.如果要将DogInfo自定义为DogData,则必须编写另一个.

以下是自定义PropertyMap的示例:

    Converter<Integer, Boolean> convertMassToLarge = new Converter<Integer, Boolean>()
    {
        public Boolean convert(MappingContext<Integer, Boolean> context)
        {
            // If the dog weighs more than 25, then it must be large
            return context.getSource() > 25;
        }
    };

    PropertyMap<DogData, DogInfo> mymap = new PropertyMap<DogData, DogInfo>()
    {
        protected void configure()
        {
            // Note: this is not normal code. It is "EDSL" so don't get confused
            map(source.getName()).setName(null);
            using(convertMassToLarge).map(source.getMass()).setLarge(false);
        }
    };

    mm.addMappings(mymap);
Run Code Online (Sandbox Code Playgroud)

pm.configure功能非常时髦.这不是实际的代码.它是虚拟EDSL代码,以某种方式解释.例如,setter的参数不相关,它只是一个占位符.你可以在这里做很多事情,比如

  • 当(条件).MAP器(getter).setter
  • when(condition).skip().setter - 安全地忽略字段.
  • 使用(转换器).map(getter).setter - 自定义字段转换器
  • with(provider).map(getter).setter - 自定义字段构造函数

注意:映射自定义添加到默认的映射,这样你就不会需要,例如,指定

            map(source.getName()).setName(null);
Run Code Online (Sandbox Code Playgroud)

在自定义PropertyMap.configure()中.

在这个例子中,我不得不编写一个转换器来将Integer映射到Boolean.在大多数情况下,这不是必需的,因为mm会自动将Integer转换为String等.

我被告知你也可以使用Java 8 lambda表达式创建映射.我试过了,但我无法弄明白.

最终建议和最佳实践

默认情况下mm使用MatchingStrategies.STANDARD哪个是危险的.它可以轻松选择错误的映射并导致奇怪,很难找到错误.如果明年其他人在数据库中添加新列怎么办?所以不要这样做.确保使用STRICT模式:

    mm.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
Run Code Online (Sandbox Code Playgroud)

始终编​​写单元测试并确保所有映射都经过验证.

    DogInfo di = mm.map(dd, DogInfo.class);
    mm.validate();   // make sure nothing in the destination is accidentally skipped
Run Code Online (Sandbox Code Playgroud)

修复任何验证失败,mm.addMappings()如上所示.

将所有映射放在中心位置,即创建mm单例.


Muh*_*mon 8

我在使用 ModelMapper 映射时遇到了一个问题。不仅属性不同,我的源和目标类型也不同。我通过这样做解决了这个问题->

如果源和目标类型不同。例如,

@Entity
class Student {
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "laptop_id")
    private Laptop laptop;
}
Run Code Online (Sandbox Code Playgroud)

和 Dto ->

class StudentDto {
    private Long id;
    private LaptopDto laptopDto;
}
Run Code Online (Sandbox Code Playgroud)

在这里,源和目标类型是不同的。因此,如果您的 MatchingStrategies 是 STRICT,您将无法在这两种不同类型之间进行映射。现在要解决这个问题,只需简单地将下面的代码放在您的控制器类或任何您想使用 ModelMapper 的类的构造函数中->

private ModelMapper modelMapper;

public StudentController(ModelMapper modelMapper) {
    this.modelMapper = modelMapper;
    this.modelMapper.typeMap(Student.class, StudentDto.class).addMapping(Student::getLaptop, StudentDto::setLaptopDto);
}
        
Run Code Online (Sandbox Code Playgroud)

就是这样。现在您可以轻松使用 ModelMapper.map(source, destination)。它会自动映射

modelMapper.map(student, studentDto);
Run Code Online (Sandbox Code Playgroud)