【问题标题】:How to customize ModelMapper如何自定义 ModelMapper
【发布时间】:2017-06-14 01:54:43
【问题描述】:

我想使用 ModelMapper 将实体转换为 DTO 并返回。大多数情况下它可以工作,但我该如何定制它。它有很多选择,很难弄清楚从哪里开始。什么是最佳做法?

我会在下面自己回答,但如果其他答案更好,我会接受。

【问题讨论】:

    标签: java rest modelmapper


    【解决方案1】:

    首先是一些链接

    我对 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;
    }
    

    使用适当的 getter/setter。你可以这样做:

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

    并且“名称”将从dd复制到di。

    自定义 mm 的方法有很多,但首先您需要了解它是如何工作的。

    mm 对象包含每个有序类型对的 TypeMap,例如 将是两个 TypeMap。

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

    我们可以这样写

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

    它会输出

    PropertyMapping[DogData.name -> DogInfo.name]
    

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

    1. 查看TypeMap是否存在,如果不存在则为 来源/目的地类型
    2. 调用TypeMapCondition,如果返回FALSE,什么都不做,停止
    3. 如有必要,调用 TypeMap Provider 构造新的目标对象
    4. 调用 TypeMap PreConverter(如果有的话)
    5. 执行以下操作之一:
      • 如果 TypeMap 有一个自定义转换器,调用它
      • 或者,生成一个PropertyMap(基于配置标志以及添加的任何自定义映射)并使用它 (注意:TypeMap 还具有可选的自定义 Pre/PostPropertyConverters,我认为会在每个映射之前和之后运行。)
    6. 调用 TypeMap PostConverter(如果有的话)

    警告:这个流程图是sort of documented,但我不得不猜测很多,所以可能并不完全正确!

    您可以自定义此过程的每一步。但最常见的两种是

    • 步骤 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);
    

    注意转换器是单向。如果要将 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);
    

    pm.configure 函数真的很时髦。这不是实际的代码。以某种方式被解释的是虚拟的EDSL code。例如,设置器的参数不相关,它只是一个占位符。你可以在这里做很多事情,比如

    • when(condition).map(getter).setter
    • when(condition).skip().setter – 安全地忽略字段。
    • using(converter).map(getter).setter – 自定义字段转换器
    • with(provider).map(getter).setter - 自定义字段构造函数

    注意自定义映射是添加到默认映射中的,所以您不需要,例如,指定

                map(source.getName()).setName(null);
    

    在您的自定义 PropertyMap.configure() 中。

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

    我听说您还可以创建映射using Java 8 lambda 表达式。我试过了,但我想不通。

    最终建议和最佳实践

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

        mm.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
    

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

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

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

    将所有映射放在一个中心位置,在那里创建 mm 单例。

    【讨论】:

    • 如果我是一个复杂的实例对象:Person{ private String name, private Address address}, Address{String city, String street} 并且我想映射一个 Dto 对象 DTO{ private String name, private String addressCity, private String addressStreet} 如果我使用模型映射器 STRICT 模式,应该称为 DTO 的属性?或者我需要配置两个自定义 PropertyMap?,因为在 STANDAR 模式下有效,但在 STRICT 模式下,它在示例中停止工作。谢谢!
    • @AlejoDev,在这种情况下,您可以使用标准模式(并了解风险)。或者你可以使用严格模式,你可以写两个 TypeMap。一种用于 Person 到 PersonDTO 的类型映射,另一种用于反向映射。
    • @JohnHenckel validate() 不会为我抛出异常。我有一个模型,其中属性称为 foo,在第二个模型中称为 bar。结果, foo 为空。我也在使用严格模式。你以前见过这个问题吗?
    • @Marc 抱歉,不,我没有看到。您应该得到每个不匹配的目标属性的错误。也许这些属性必须是公开的?我不知道。
    • @JohnHenckel,因为您在帖子中提到过使用 Spring。您是否注意到使用 ModelMapper 后配置 Spring 上下文所需的时间大幅增加?使用 2.3.2 版本,我们的 Spring Boot 应用程序在 1 分钟内启动,而使用 2.3.3 版本则需要 4 分钟才能启动,这非常令人沮丧。我注意到这是因为 .addMapping 方法。 github.com/modelmapper/modelmapper/issues/462
    【解决方案2】:

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

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

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

    和 Dto ->

    class StudentDto {
        private Long id;
        private LaptopDto laptopDto;
    }
    

    在这里,源类型和目标类型是不同的。因此,如果您的 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);
    }
            
    

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

    modelMapper.map(student, studentDto);
    

    【讨论】:

      【解决方案3】:

      我过去 6 个月一直在使用它,我将解释一下我对此的一些想法:

      首先,建议将其作为唯一实例(singleton,spring bean,...),这在手册中有说明,我想大家都同意。

      ModelMapper 是一个很棒的映射库并且非常灵活。由于它的灵活性,有很多方法可以获得相同的结果,这就是为什么它应该在最佳实践手册中说明何时使用一种或其他方式来做同样的事情。

      ModelMapper 开始有点困难,它的学习曲线非常紧凑,有时很难理解做某事的最佳方法,或者如何做其他事情。因此,开始之前需要准确地阅读和理解手册。

      您可以使用以下设置根据需要配置映射:

      Access level
      Field matching
      Naming convention
      Name transformer
      Name tokenizer 
      Matching strategy
      

      默认配置是最好的 (http://modelmapper.org/user-manual/configuration/),但如果你想自定义它,你可以做到。

      只有一件事与匹配策略配置有关,我认为这是最重要的配置,需要小心。我会使用StrictStandard,但从不使用Loose,为什么?

      • Due Loose 是最灵活、最智能的映射器,它可以映射一些您意想不到的属性。所以,绝对要小心。我认为最好创建自己的 PropertyMap 并在需要时使用 Converters,而不是将其配置为 Loose。

      否则,validate 所有属性匹配很重要,您验证它是否正常工作,并且使用 ModelMapper 更需要智能映射,因为它是通过反射完成的,因此您将没有编译器帮助,它将继续编译但是映射会在没有意识到的情况下失败。这是我最不喜欢的事情之一,但它需要避免样板文件和手动映射。

      最后,如果你确定要在你的项目中使用 ModelMapper,你应该按照它建议的方式使用它,不要将它与手动映射混合(例如),只要使用 ModelMapper,如果你不知道如何做一些确定是可能的(调查,...)。有时使用模型映射器(我也不喜欢它)很难像手动那样做,但这是您应该付出的代价,以避免在其他 POJO 中进行样板映射。

      【讨论】:

        【解决方案4】:
        import org.modelmapper.ModelMapper;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Service;
        
        @Service
        public class EntityDtoConversionUtil {
        
            @Autowired
            private ModelMapper modelMapper;
        
            public Object convert(Object object,Class<?> type) {
        
                Object MapperObject=modelMapper.map(object, type);
        
                return MapperObject;
        
            }
        
        
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-08-28
          • 1970-01-01
          • 2012-03-02
          • 2019-10-27
          • 2021-11-18
          • 1970-01-01
          • 2018-10-21
          • 1970-01-01
          相关资源
          最近更新 更多