# 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/)