ModelMapper 是什么?
是一个 Object To Object 的工具。是利用反射的原理实现的 Object To Object。
有什么用?
在项目中很多时候需要把 entity 和 DTO 两个模型类来回转换,保证 entity 对外是隐私的。而ModelMapper就是为了方便模型转换而实现的一个类库。
为什么选择modelMapper
ModelMapper的目标是让对象映射变得简单,根据惯例自动确定一个对象模型如何映射到另一个对象,就像人类一样,同时为处理特定用例提供一个简单、重构安全的API。
ModelMapper 的优点:
配置简单;
对于 DO-DTO-VO 转换中属性一致的对象转换基本一行代码搞定;
能够快速修正遗漏字段,保证数据一致性,运用场景有:数据清洗、数据同步;
可在转换时增加自定义逻辑;
缺点:
通过反射的方式进行映射,所以性能不如一些主流的映射框架,但是在实际项目中大多数的业务场景使用modelMapper是不会有什么影响的。
工作原理
ModelMapper 由两个单独的过程组成:匹配过程(其中源和目标类型的属性相互匹配)和映射过程(将匹配的属性值从源对象转换为目标对象)
调用该方法时,将分析源类型和目标类型,以确定根据匹配策略和其他配置隐式匹配的属性。然后根据这些匹配进行映射数据。map
即使源对象 和 目标对象及其属性不同,ModelMapper 也会根据配置的匹配策略,尽力确定属性之间的合理匹配。
模型映射器 - 工作原理
官方文档
模型映射器 - 入门
使用
pom引入modelmapper坐标本文以3.0.0为例
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.0.0</version>
</dependency>
映射
源模型
@Data
public class Order {
private Customer customer;
private Address billingAddress;
private String orderCode;
}
@Data
class Customer {
private Name name;
}
@Data
class Name {
private String firstName;
private String lastName;
}
@Data
class Address {
private String street;
private String city;
}
目标模型
@Data
public class OrderDTO {
private String customerFirstName;
private String customerLastName;
private String billingStreet;
private String billingStreetTest;
private String billingCity;
private String billingCityTest;
private String orderCode;
}
简单的 model 转换
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
Order order = new Order();
Customer customer = new Customer();
Name name = new Name();
name.setLastName("张");
name.setFirstName("三");
customer.setName(name);
Address address = new Address();
address.setCity("上海");
address.setStreet("安研路");
order.setCustomer(customer);
order.setBillingAddress(address);
OrderDTO orderDTO = modelMapper.map(order, OrderDTO.class);
String jsonString = JSON.toJSONString(orderDTO);
// jsonString = {"billingCity":"上海","billingStreet":"安研路","customerFirstName":"三","customerLastName":"张"}
System.out.println("jsonString = " + jsonString);
}
范型 model 转换
List<orderDTO> orderDTOList = modelMapper.map(orderList, new TypeToken<List<orderDTO>>() {}.getType());
虽然 ModelMapper 会尽最大努力为您隐式匹配源属性和目标属性,但有时可能需要显式定义属性之间的映射。
ModelMapper 支持多种映射方法,允许使用任意组合的方法和字段引用。
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
Order order = new Order();
Customer customer = new Customer();
Name name = new Name();
name.setLastName("张");
name.setFirstName("三");
customer.setName(name);
Address address = new Address();
address.setCity("上海");
address.setStreet("安研路");
order.setCustomer(customer);
order.setBillingAddress(address);
OrderDTO orderDTO = modelMapper
.typeMap(Order.class, OrderDTO.class)
.addMappings(mapper -> {
mapper.map(src -> src.getBillingAddress().getStreet(),
OrderDTO::setBillingStreetTest);
mapper.map(src -> src.getBillingAddress().getCity(),
OrderDTO::setBillingCityTest);
})
.map(order);
String jsonString = JSON.toJSONString(orderDTO);
// jsonString = {"billingCity":"上海","billingCityTest":"上海","billingStreet":"安研路","billingStreetTest":"安研路","customerFirstName":"三","customerLastName":"张"}
System.out.println("jsonString = " + jsonString);
}
深度映射
此示例将目标类型的方法映射到源类型的方法层次结构,允许在源方法和目标方法之间进行深度映射:setAgegetCustomer().getAge()
typeMap.addMappings(mapper -> mapper.map(src -> src.getCustomer().getAge(), PersonDTO::setAge));
此示例将目标类型的方法层次结构映射到源类型的属性层次结构:getCustomer().setName()person.getFirstName()
typeMap.addMappings(mapper -> mapper.<String>map(src -> src.getPerson().getFirstName(), (dest, v) -> dest.getCustomer().setName(v)));
注意:为了填充目标对象,深度映射要求方法具有相应的突变器,例如 一种方法 或 可访问字段 getCustomer setCustomer customer
跳过属性
虽然 ModelMapper 隐式创建从源类型到目标类型中每个属性的映射,但有时可能需要跳过某些目标属性的映射。
此示例指定在映射过程中应跳过目标类型的方法:setName
typeMap.addMappings(mapper -> mapper.skip(Destination::setName));
变换器
转换器允许在将源映射到目标属性时进行自定义转换(有关详细信息,请参阅转换器的常规页面)。
考虑这个转换器,它扩展了AbstractConverter并将字符串转换为大写字符串:
Converter<String, String> toUppercase =
ctx -> ctx.getSource() == null ? null : ctx.getSource().toUppercase();
// 使用转换器从源属性映射到目标属性很简单:toUppercase
typeMap.addMappings(mapper -> mapper
.using(toUppercase)
.map(Person::getName, PersonDTO::setName));
或者我们也可以使用 lambda 表达式 using
typeMap.addMappings(mapper -> mapper
.using(ctx -> ((String) ctx.getSource()).toUpperCase())
.map(Person::getName, PersonDTO::setName));
使用具有深度映射的转换器时,映射引用的最后一个源和目标属性将传递给变换器:
typeMap.addMappings(
// toUppercase 将使用getFirstName()和setName()中的属性类型调用
mapper -> mapper
.using(toUppercase)
.<String>map(
src -> src.getPerson().getFirstName(),
(dest, v) -> dest.getCustomer().setName(v)
)
);
提供者
提供程序允许,允许在映射之前提供自己的目标属性和类型实例(有关详细信息,请参阅提供程序上的常规页)。
请考虑此提供程序,它提供实例:Person
Provider<Person> personProvider = req -> new Person();
配置用于特定属性映射很简单:personProvider
typeMap.addMappings(mapper -> mapper.with(personProvider).map(Source::getPerson, Destination::setPerson));
或者我们也可以使用 lambda 表达式。with
typeMap.addMappings(mapper ->
mapper.with(req -> new Person()).map(Source::getPerson, Destination::setPerson));
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
Order order = new Order();
Customer customer = new Customer();
Name name = new Name();
name.setLastName("张");
name.setFirstName("三");
customer.setName(name);
Address address = new Address();
address.setCity("上海");
address.setStreet("安研路");
order.setCustomer(customer);
order.setBillingAddress(address);
order.setOrderCode("12394234323");
OrderDTO orderDTO = modelMapper
.typeMap(Order.class, OrderDTO.class)
.addMappings(mapper -> {
mapper
.with(r -> "自定义提供者,属性覆盖")
.map(Order::getOrderCode, OrderDTO::setOrderCode);
})
.map(order);
String jsonStringOrder = JSON.toJSONString(order);
String jsonStringOrderDto = JSON.toJSONString(orderDTO);
System.out.println("jsonStringOrderDto = " + jsonStringOrderDto);
System.out.println("jsonStringOrder = " + jsonStringOrder);
// jsonStringOrderDto = {"billingCity":"上海","billingStreet":"安研路","customerFirstName":"三","customerLastName":"张","orderCode":"自定义提供者,属性覆盖"}
// jsonStringOrder = {"billingAddress":{"city":"上海","street":"安研路"},"customer":{"name":{"firstName":"三","lastName":"张"}},"orderCode":"12394234323"}
}
条件映射
可以通过提供映射来使目标属性的映射成为条件。Condition
请考虑此条件,如果源类型不是:null
Condition notNull = ctx -> ctx.getSource() != null;
// 我们可以使用 Condition 指定仅在源不为 null 时对属性进行映射:notNull
typeMap.addMappings(mapper -> mapper
.when(notNull)
.map(Person::getName, PersonDTO::setName));
在此示例中,如果源不为 null,则将跳过映射,否则映射将从源对象的方法继续:setName getName
typeMap.addMappings(mapper -> mapper.when(notNull).skip(PersonDTO::setName));
问题排查 验证
虽然 ModelMapper 会尝试将每个目标属性与源属性进行匹配,但偶尔也会有一些目标属性找不到匹配项。若要验证所有目标属性是否匹配,请调用该方法:validate
modelMapper.validate();
如果验证失败,将抛出一条有用的消息,描述任何不匹配的目标属性。ValidationException
处理验证错误
- 要解决由不匹配的目标导致的验证错误,您有两个选择:
- 创建映射或忽略任何不匹配属性的映射
- 调整配置以尝试匹配任何不匹配的属性
或者,可以接受不执行任何操作,在这种情况下,在映射过程中将跳过任何不匹配的目标属性。
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
Order order = new Order();
Customer customer = new Customer();
Name name = new Name();
name.setLastName("张");
name.setFirstName("三");
customer.setName(name);
Address address = new Address();
address.setCity("上海");
address.setStreet("安研路");
order.setCustomer(customer);
order.setBillingAddress(address);
order.setOrderCode("12394234323");
OrderDTO orderDTO = modelMapper.map(order,OrderDTO.class);
modelMapper.validate();
}
Exception in thread "main" org.modelmapper.ValidationException: ModelMapper validation errors:
1) Unmapped destination properties found in TypeMap[Order -> OrderDTO]:
com.entity.OrderDTO.setBillingStreetTest()
com.entity.OrderDTO.setBillingCityTest()
1 error
at org.modelmapper.internal.Errors.throwValidationExceptionIfErrorsExist(Errors.java:246)
at org.modelmapper.ModelMapper.validate(ModelMapper.java:547)
at com.Test.main(Test.java:29)
通过异常可以看到 setBillingStreetTest() 和 setBillingCityTest() 未找到映射的目标属性,所以该值映射不到。
通过 处理不匹配 方式既可解决
配置
springBoot 中可以通过 ico 统一管理使用
@Configuration
public class ModelMapperConfig {
/**
* 范型 转换 modelMapper.map(raceModels, new TypeToken<List<object>>() {}.getType());
*/
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
// 开启字段匹配
.setFieldMatchingEnabled(true)
// 设置最大方法级别 为 private
.setMethodAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
// 设置匹配策略模式为 严格
.setMatchingStrategy(MatchingStrategies.STRICT)
// 设置 如果源对象如果为空 不拷贝到 目标对象 源user name = null 目标userdto name = xixi 拷贝的时候 userdot name 不会被覆盖成 null
.setSkipNullEnabled(Boolean.TRUE);
return modelMapper;
}
}