当前位置: 首页 > 工具软件 > ModelMapper > 使用案例 >

ModelMapper使用

范成周
2023-12-01

# ModelMapper使用介绍

## ModelMapper是什么
ModelMapper是一个object-object映射器,它帮助你高效的将一个类型的对象转换成另一个不同类型的对象。
## 为什么使用ModelMapper
- object-object映射是一个无聊但代码量很大的工作,ModelMapper可以提供简介的配置代码简化工作
- 对象映射几乎遍布所有的业务类代码,使用ModelMapper可以减少重复代码
- 根据单一职能原则,对象映射已经可以视为一项单独领域的工作,ModelMapper便是专注此领域的职能类
- 使用ModelMapper对硬编码对象映射有更好的可读性和可维护性
## 简单示例

使用ModelMapper十分简单,分为配置代码和映射执行代码  

### 定义源类和目标类
```
 public  class Source{

        private Integer value;
        // 这里忽略了get set方法,以下的POJO类都会如此
}
    
public  class Destination{

        private Integer value;

}
```
### 配置代码
```
ModelMapper modelMapper=new ModelMapper();

//定义映射
modelMapper.createTypeMap(Source.class,Destination.class)
           .addMapping(Source::getValue,Destination::setValue);
```
### 映射
有两种方式
- 创建一个新对象
```
        Source source=new Source();
        source.setValue(10);
        Destination destination = modelMapper.map(source, Destination.class);
        assert destination.getValue().equals(10);
```
- 映射一个现有的对象
```
        Source source=new Source();
        source.setValue(10);
        Destination destination=new Destination();
        modelMapper.map(source,destination);
        assert destination.getValue().equals(10);
```
## ModelMapper 特性
### 属性映射(Property Mapping)
这里“属性”的含义是指get/set方法。只有这种类型的方法才能使用java的lambda表达式定义映射,其他方法在运行时会抛出异常。  
示例:
```
typeMap.addMapping(Source::getFirstName, Destination::setName);
typeMap.addMapping(Source::getAge, Destination::setAgeString);
// 嵌套对象的get/set方法也是可以的
typeMap.addMapping(src -> src.getCustomer().getAge(), PersonDTO::setAge);
typeMap.addMapping(src -> src.getPerson().getFirstName(), (dest, v) -> dest.getCustomer().setName(v));
```
### 隐式映射
对于源类和目标类型属性相同(get/set方法签名相同),ModelMapper会隐式支持其映射关系。
在简单示例中的配置代码不用编写,映射操作也会执行成功。
```
//定义映射
//modelMapper.createTypeMap(Source.class,Destination.class)
//                   .addMapping(Source::getValue,Destination::setValue);
```
> *但是建议显示编写映射代码,因为如果需要重构属性名称,很容易忘记同时修改这部分映射代码,容易产生难以查找的bug*

上面这句话,是我以前的建议,后面发现对ModelMapper进行下面的配置,是在程序启动时避免上句话所提到的问题的。
```
 ModelMapper modelMapper=new ModelMapper();
        //默认为standard模式,设置为strict模式
        
        //standard模式会对source中的属性进行分词,例如getTrueName,会分为True,Name两个词,
        // 只要destination中匹配一个词就会建立映射,例如setName,会建立映射getTrueName->setName
       
        //strict模式要求所有的分词相同,才会建立映射
        
        //standard模式和strict模式还有一项要求destination的所有属性必须建立映射,否则,进行映射到时候,会产生异常
        //这样设置为strict模式,并且在配置完类型映射,再调用 modelMapper.validate() ,就可以在项目启动时,检验 modelmapper
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        modelMapper.createTypeMap(User.class, UserService.UserDTO.class)
                .addMapping(User::getId, UserService.UserDTO::setId);
//                .addMapping(User::getName, UserService.UserDTO::setName);

        modelMapper.createTypeMap(UserService.CreateUserRequest.class,User.class)
                .addMappings(mapping->mapping.skip(User::setId))
                .addMapping(UserService.CreateUserRequest::getName,User::setName);

        modelMapper.createTypeMap(UserService.UpdateUserRequest.class,User.class)
                .includeBase(UserService.CreateUserRequest.class,User.class);
        modelMapper.validate();
        return modelMapper;
```


### 自定义属性映射(Converter)
对于属性映射,如果在get后,set前需要做一些数据处理,那么需要进行自定义属性映射
ModelMapper定义了一个Converter范型类,需要开发者自己实现相关映射。

示例:
- 源类
```
class CalenderEvent{

    private LocalDateTime date;

    private String title;
}
```
- 目标类
```
class CalenderEventForm{

    private LocalDate eventDate;

    private Integer eventHour;

    private Integer eventMinute;

    private String title;
}    
```
- 映射代码
```
// 这里需要将CalenderEvent中的date,映射到CalenderEventForm中的eventDate,eventHour,
eventMinute三个字段
//使用using方法调用Converter类,表示使用这个Converter执行属性映射
 modelMapper.createTypeMap(CalenderEvent.class,CalenderEventForm.class)
                .addMappings(mapper->mapper.using(new LocalDateTime2HourConverter()).map(CalenderEvent::getDate,CalenderEventForm::setEventHour))
                 .addMappings(mapper->mapper.using(new Converter<LocalDateTime,Integer>(){

                     @Override
                     public Integer convert(MappingContext<LocalDateTime, Integer> context) {
                         return context.getSource().getMinute();
                     }
                 }).map(CalenderEvent::getDate,CalenderEventForm::setEventMinute))
                .addMappings(mapper->mapper.using(new Converter<LocalDateTime,LocalDate>(){

                    @Override
                    public LocalDate convert(MappingContext<LocalDateTime, LocalDate> context) {
                        return context.getSource().toLocalDate();
                    }
                }).map(CalenderEvent::getDate,CalenderEventForm::setEventDate));
                
                
  class LocalDateTime2HourConverter implements Converter<LocalDateTime,Integer>{

    @Override
    public Integer convert(MappingContext<LocalDateTime, Integer> context) {
        return context.getSource().getHour();
    }
}
```
Converter类,并不是只应用于自定义属性映射,还可以其他场景可以使用:给目标属性赋值一个常数;下文列出的忽略映射,条件映射等等。

### 忽略映射(Skipping Properties)
如果目标类的属性,不来自于源类型,使用默认值,那么使用*忽略映射*跳过映射处理
```
typeMap.addMappings(mapper -> mapper.skip(Destination::setName));
```
### 条件映射(Conditional Mapping)
当满足一定条件的时候,执行属性映射。
示例:
```
 modelMapper.createTypeMap(CalenderEvent.class,CalenderEventForm.class)
                   .addMappings(mapping ->mapping.using(new Converter<String, String>() {
                       @Override
                       public String convert(MappingContext<String, String> context) {
                           if(context.getSource()==null){
                               return "NULL";
                           }
                           return context.getSource();
                       }
                   }).map(CalenderEvent::getTitle,CalenderEventForm::setTitle));
```
### 嵌套映射(Nested Mapping)
如果源类型和目标类型包含复杂类型,那么复杂类型定义的映射关系可以复用。
示例:
```
public static class NestedSource{

        private Integer value;
        //getter and setter

    }

    public static class NestedDestination{
        private Integer value;

        private Integer value2;
        //getter and setter

    }

    public static class Source{
        private Integer value;

        private NestedSource nestedSource;
        //getter and setter


    }

    public static class Destination{
        private Integer value;

        private NestedDestination nestedDestination;
        //getter and setter
    }

ModelMapper modelMapper =new ModelMapper();

modelMapper.createTypeMap(NestedSource.class,NestedDestination.class)
                   .addMapping(NestedSource::getValue,NestedDestination::setValue)
                   .addMapping(NestedSource::getValue,NestedDestination::setValue2);
 modelMapper.createTypeMap(Source.class,Destination.class)
                .addMapping(Source::getValue,Destination::setValue)
                // 下面这一句必须写,其映射会使用上面定义的NestedSource.class,NestedDestination.class映射
                .addMapping(Source::getNestedSource, Destination::setNestedDestination);
```

### 继承
对于子类进行映射,使用includeBase方法可以使父类定义过的映射代码同时可以继承下来。
示例
```
modelMapper.createTypeMap(Source.class,Destination.class)
                   .addMapping(Source::getValue,Destination::setValue);
 modelMapper.createTypeMap(ChildSource.class,ChildDestination.class)
                    .addMapping(ChildSource::getValue2,ChildDestination::setValue2)
                    .includeBase(Source.class,Destination.class);
```
对于接口和抽象类,需要说明一下:接口和抽象类不能作为目标类型,可以作为源类型,因为目标类型都必须包含无参的构造函数。
### Providers
如果映射想在初始化目标类型对象的时候,做一些工作,可以使用Providers初始化一个对象。
示例:
```
Provider<Person> personProvider = req -> new Person();
typeMap.addMappings(mapper -> mapper.with(personProvider).map(Source::getPerson, Destination::setPerson));

//直接使用lambda表达式
typeMap.addMappings(mapper ->
    mapper.with(req -> new Person()).map(Source::getPerson, Destination::setPerson));
```
### 集合映射
ModelMapper支持集合映射,不过需要创建一个TypeToken类型对象来实现。
示例:
```
Type listType = new TypeToken<List<String>>() {}.getType();
List<String> characters = modelMapper.map(numbers, listType);
```
### 使用默认值
目标属性使用默认值,而不来自源类的任意一个字段。针对这种情况,ModelMapper并没有专门的代码来处理,我需要使用`Converter`变相来实现。  
我编写了一个支持范型的设置默认值的通用类`DefaultValueConverter<T>`,可以拷贝到ModelMapper配置类中使用。
```
   class DefaultValueConverter<T> implements Converter<Object, T> {

    private final T defaultValue;

    private final Supplier<T> supplier;

    /**
     * 创建默认值转换器(常量)
     * @param defaultValue 必须是常量
     */
    public DefaultValueConverter(T defaultValue) {
        if(defaultValue==null){
            throw new NullPointerException("defaultValue is null");
        }
        this.defaultValue = defaultValue;
        supplier=null;
    }

    /**
     * 创建默认值转换器(动态生成值,比如当前时间,自增ID)
     * @param supplier
     */
    public DefaultValueConverter(Supplier<T> supplier) {
        if(supplier==null){
            throw new NullPointerException("supplier is null");
        }
        this.supplier = supplier;
        defaultValue=null;
    }

    @Override
    public T convert(MappingContext<Object, T> context) {
        if(defaultValue!=null){
            return defaultValue;
        }
        else {
            return supplier.get();
        }
    }
}
   
     //modelMapper配置
        modelMapper.createTypeMap(Source.class,Destination.class)
                    .addMappings(mapping ->
                            //DefaultValueConverter的参数必须和Destination::setName的类型相同
                            mapping.using(new DefaultValueConverter("test"))
                                    //Source::getNumber并没有使用,可以任意选一个Source的属性
                                    .map(Source::getNumber,Destination::setName));
```
如果需要设置默认时间为当前时间,可以用下面的类:
```
class CurrentDateConverter implements Converter<Object, Date> {

    @Override
    public Date convert(MappingContext<Object, Date> context) {
        return new Date();
    }
}
```
### 多个属性映射为一个属性
针对多个属性映射为一个属性情况,我在社区中找到一个解决方案
```
    public static class Source{
        private String firstName;

        private String secodeName;

        //getter and setter
    }

    public static class Destination{
        private String fullName;

        //getter and setter
    }

    //ModelMapper配置
        modelMapper.createTypeMap(Source.class,Destination.class)
                .addMappings(new PropertyMap<Source, Destination>() {
            @Override
            protected void configure() {
                this.using(ctx->{
                    Source src=(Source)ctx.getSource();
                    return src.getFirstName()+" "+src.getSecodeName();
                }).map(source).setFullName(null);
            }
        });
```

### 验证映射
```
modelMapper.validate();
```
调用上面的代码可以验证编写的映射代码是否有误。比如默认情况下,所有的目标类型的属性必须全部都有映射的,如果有忘记编写,这句验证代码就会抛出相关异常,
## 注意点
### 内部类
- non-static内部类是不能进行配置映射的,因为non-static内部类实例依附一个宿主类,只有这个宿主类才可以创建non-static类。
- static内部类可以进行映射,其可以独立初始化。
- 接口内部类可以进行映射,接口内部类其实为static内部类,可以使用反编译工具查看。

java和C#的static类差异十分大,C#转java的同学,学习java时,请先忘记C#中static的用法
### 集合&继承
集合映射的目标类型为父类,那么映射就会创建指定的目标类型,并不会映射为相关子类。
示例:
```
        List<Source> sourceList=new LinkedList<>();
        Source source1=new Source(10);
        ChildSource source2=new ChildSource();
        source2.setValue(10);
        source2.setValue2(12);
        Source source3=new Source();
        sourceList.add(source1);
        sourceList.add(source2);
        sourceList.add(source3);

        Type type=new TypeToken<List<Destination>>(){}.getType();
        List<Destination> destinationList=modelMapper.map(sourceList,type);
        assert destinationList.get(0) instanceof Destination;
        // 下句代码会报错
        assert destinationList.get(1) instanceof ChildDestination;
        assert destinationList.get(2) instanceof Destination;
```

如果想要实现子类到子类的映射到,还是用foreach遍历实现吧。
### 继承映射重写
java的继承语法是支持子类可以重写父类方法的。但不幸的是,ModelMapper不支持,子类映射重写父类的映射。
```
modelMapper.createTypeMap(ChildSource.class,ChildDestination.class)
                    .addMapping(ChildSource::getValue2,ChildDestination::setValue2)
                    // 下句代码重写了父类的setValue映射,但是不起作用
                    .addMapping(ChildSource::getValue2,ChildDestination::setValue)
                    .includeBase(Source.class,Destination.class);
                    
```
如果需要重写父类映射,那么去除掉includeBase方法,自己再重写一遍父类所有的映射。

### 在SpringBoot中使用ModelMapper
 ModelMapper是线程安全的,在SpringBoot中可以建立一个单例模式的Bean供调用。
 示例:
 ```
 @Configuration
public class ModelMapperConfig {


    @Bean
    @Scope("singleton")
    public ModelMapper getModelMapper(){
        ModelMapper modelMapper=new ModelMapper();
        //默认为standard模式,设置为strict模式
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        
        /// 类型映射代码
        
        modelMapper.validate();
        // 配置代码
        return modelMapper;
    }

}


public class XXXService {
    private ModelMapper modelMapper;

    @Autowired
    public XXXService(ModelMapper modelMapper) {
        this.modelMapper = modelMapper;
    }
}
 ```

### 参考资料
- [ModelMapper - User Manual](http://modelmapper.org/user-manual/)

 类似资料: