本篇文章,介绍orika的简单使用。以及借助SPI或Spring对orika进行组件化,让增加类型转换规则更加简单方便。
一、orika简介
在工作中,我们经常涉及到对象的DTO、DO等对象的转换。对于这些对象的转换,我们除了自己写大量的get/set方法外,还可以借助orika这样的Bean映射工具来帮我们完成。
二、几款Bean映射工具简单对比
1. BeanUtils
apache的BeanUtils和spring的BeanUtils 底层都是
基于放射
实现的Bean映射。而反射的性能是比较低的,因此BeanUtils的性能并不太理想。
2. BeanCopier
cglib的BeanCopier 直接使用ASM在字节码层面编写get/set 方法,然后生成class文件直接执行。由于没有使用反射,BeanCopier 的性能相对于BeanUtils有较大的提升。
3. Dozer
使用以上类库虽然可以不用手动编写get/set方法,但是他们
都不能对不同名称的对象属性进行映射
。在定制化的属性映射方面做得比较好的有Dozer,Dozer支持简单属性映射、复杂类型映射、双向映射、隐式映射以及递归映射。可使用xml或者注解进行映射的配置,支持自动类型转换,使用方便。但Dozer的底层仍然是基于反射做的,因此性能不太理想。
4. Orika
Orika底层采用了javassist类库
生成Bean映射的字节码
,之后直接加载执行生成的字节码文件,因此在速度上比使用反射进行赋值会快很多。且
支持对不同名称的对象属性进行映射
。
三、orika使用
导入orika的依赖
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
下面使用orika将OrderDTO转换成OrderDO。这是案例中用到的Bean:
// 转换类
@Data
@AllArgsConstructor
public class OrderDTO {
private String orderId;
private String brand;
private String address;
private String mobile;
private OrderItemDTO orderItem;
public OrderDTO(String orderId, String brand, String address, String mobile) {
this.orderId = orderId;
this.brand = brand;
this.address = address;
this.mobile = mobile;
}
}
// 目标类
@Data
public class OrderDO {
private String id;
private String brand;
private String address;
private String phone;
private OrderItemDo orderItem;
}
@Data
@AllArgsConstructor
public class OrderItemDTO {
private String orderId;
private String itemId;
private String name;
}
@Data
public class OrderItemDo {
private String orderId;
private String id;
private String name;
}
场景一:目标类与转换类字段名完全一致
// 俩个对象属性完全一致的场景
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
OrderDO order = mapperFactory.getMapperFacade().map(orderDTO, OrderDO.class);
System.out.println(order);
运行结果:
可以看到,名称相同的字段成功转换了。
场景二:目标类与转换类少数几个字段名不一致
对于少数几个字段名称不相同的,我们需要告诉orika他们之间的映射关系。
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 需要多一步,使用classMap,告诉orika字段名之间的映射关系
mapperFactory.classMap(orderDTO.getClass(),OrderDO.class)
.field("orderId","id")
.field("mobile","phone").byDefault().register();
OrderDO order = mapperFactory.getMapperFacade().map(orderDTO, OrderDO.class);
System.out.println(order);
运行结果:
场景三:转换集合
转换集合使用的是mapAsList而不是map,同样,如果转换类和目标类名称不一致的对象依旧需要我们告诉orika。
List<OrderDTO> orderDTOS = Arrays.asList(
new OrderDTO("1", "kfc", "南京路", "666"),
new OrderDTO("2", "kfc", "北京路", "999"),
new OrderDTO("3", "kfc", "东京路", "888"),
new OrderDTO("4", "kfc", "西京路", "555"));
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 如果存在字段名不一致,手动告诉orika他们之间的映射关系
mapperFactory.classMap(OrderDTO.class,OrderDO.class)
.field("orderId","id")
.field("mobile","phone").byDefault().register();
// 使用mapAsList进行转换
List<OrderDO> orders = mapperFactory.getMapperFacade().mapAsList(orderDTOS, OrderDO.class);
for (OrderDO order : orders) {
System.out.println(order);
}
运行结果:
场景四:转换对象内嵌了其他对象
在本例中OrderDTO还嵌套了OrderItemDTO对象。如果OrderItemDTO和OrderItemDO的字段名完全相同的话,我们就不需要做额外的处理。但如果存在差异,我们同样需要手动告诉orika他们之间的映射关系。
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
orderDTO.setOrderItem(new OrderItemDTO("1","1","汉堡包"));
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 如果存在字段名不一致,手动告诉orika他们之间的映射关系
mapperFactory.classMap(OrderDTO.class,OrderDO.class)
.field("orderId","id")
.field("mobile","phone")
.field("orderItem.itemId","orderItem.id") //orderItemDTO和OrderItemDO之间的映射关系
.byDefault().register();
OrderDO order = mapperFactory.getMapperFacade().map(orderDTO, OrderDO.class);
System.out.println(order);
运行结果:
场景五:自定义转换规则
有时候,我们不是要简单的转换,比如说我们希望转换后的brand统一变成大写,那么这个时候,我们就可以自定义转换规则做些额外的处理。
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 如果存在字段名不一致,手动告诉orika他们之间的映射关系
mapperFactory.classMap(OrderDTO.class,OrderDO.class)
.field("orderId","id")
.field("mobile","phone")
.field("orderItem.itemId","orderItem.id")
.byDefault()
// 自定义我们的转换规则
.customize(new CustomMapper<OrderDTO, OrderDO>() {
@Override
public void mapAtoB(OrderDTO orderDTO, OrderDO orderDO, MappingContext context) {
// 将品牌转换成大写
orderDO.setBrand(orderDTO.getBrand().toUpperCase(Locale.ROOT));
}
})
.register();
OrderDO order = mapperFactory.getMapperFacade().map(orderDTO, OrderDO.class);
System.out.println(order);
运行结果:
通过这五个案例,我们不难发现orika是很容易使用的
- 构建一个DefaultMapperFactory
- 如果存在字段名不一致的情况,告诉orika他们之间的映射关系
- 使用map/mapAsList进行对象转换
四、orika组件化
基于SPI进行组件化
其实一个DefaultMapperFactory是可以注册多个映射规则的。比如我们的项目中除了有Order需要转换外,现在又多了一个User需要转换。
@Data
@AllArgsConstructor
public class UserDTO {
private String id;
private String username;
}
@Data
public class UserDO {
private String id;
private String name;
}
注册多个映射规则:
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
orderDTO.setOrderItem(new OrderItemDTO("1","1","汉堡包"));
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 1.注册orderDTO和OrderDO的映射关系
mapperFactory.classMap(OrderDTO.class,OrderDO.class)
.field("orderId","id")
.field("mobile","phone")
.field("orderItem.itemId","orderItem.id")
.byDefault()
.register();
// 2.注册UserDTO和UserDO的映射关系
mapperFactory.classMap(UserDTO.class,UserDO.class)
.field("username","name")
.byDefault().register();
OrderDO order = mapperFactory.getMapperFacade().map(orderDTO, OrderDO.class);
System.out.println(order);
// 转换UserDTO
UserDTO userDTO = new UserDTO("1", "小王");
UserDO user = mapperFactory.getMapperFacade().map(userDTO, UserDO.class);
System.out.println(user);
运行结果:
基于这个特点,我们完全可以将DefaultMapperFactory作为一个
单例
,只创建一次即可。下面我们将对orika进行封装,利用SPI机制动态的往MapperFactory中注册多个映射规则。
1、定义BeanMapperRegistry接口,后续所有的映射规则都需要实现该接口
public interface BeanMapperRegistry {
void registry(MapperFactory mapperFactory);
}
2、定义Order和User映射规则
/**
* OrderDTO转OrderDO的映射规则
*/
public class OrderBeanMapper implements BeanMapperRegistry {
@Override
public void registry(MapperFactory mapperFactory) {
mapperFactory.classMap(OrderDTO.class, OrderDO.class)
.field("orderId","id")
.field("mobile","phone")
.field("orderItem.itemId","orderItem.id")
.byDefault()
.register();
}
}
/**
* UserDTO转UserDO的映射规则
*/
public class UseBeanMapper implements BeanMapperRegistry {
@Override
public void registry(MapperFactory mapperFactory) {
mapperFactory.classMap(UserDTO.class, UserDO.class)
.field("username","name")
.byDefault().register();
}
}
3、封装orika
/**
* 对orika进行简单的封装
*/
public class BeanMapper {
private static MapperFactory mapperFactory;
private static MapperFacade mapperFacade;
static {
mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFacade = mapperFactory.getMapperFacade();
// 利用SPI,注册Bean的转换规则
ServiceLoader<BeanMapperRegistry> serviceLoader = ServiceLoader.load(BeanMapperRegistry.class);
for (BeanMapperRegistry beanMapperRegistry : serviceLoader) {
beanMapperRegistry.registry(mapperFactory);
}
}
public static <S, T> T map(S sourceObj, Class<T> targetClass) {
return mapperFacade.map(sourceObj, targetClass);
}
public static <S, T> List<T> mapAsList(Iterable<S> sourceObj, Class<T> targetClass) {
return mapperFacade.mapAsList(sourceObj, targetClass);
}
}
4、在resources/META-INF/services/目录下创建fcp.orika.BeanMapperRegistry (BeanMapperRegistry 的全类名)
内容如下:
fcp.orika.convert.OrderBeanMapper
fcp.orika.convert.UseBeanMapper
5、使用我们封装后的BeanMapper
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
UserDTO userDTO = new UserDTO("1", "小王");
System.out.println(BeanMapper.map(orderDTO, OrderDO.class));
System.out.println(BeanMapper.map(userDTO, UserDO.class));
运行结果:
注:如果看蒙了,那么可能是对于ServiceLoader的使用不太了解。可以看看这篇文章
Java SPI机制 – ServiceLoader – 知乎 (zhihu.com)
改造后,以后增加新的映射规则的步骤就是:
- 添加一个类实现BeanMapperRegistry接口
-
在fcp.orika.BeanMapperRegistry 文件中追加新增类的全类名
符合开闭原则
比如在本案例中,涉及的映射规则有:
- OrderDTO转OrderDo
- UserDTO转UserDo
对应的转换类就有俩个
- OrderBeanMapper
- UseBeanMapper
基于Spring进行组件化
同样还是以OrderDTO<=>OrderDO,UserDTO<=>UserDO为例。利用Spring生命周期的特性,我们新建一个
OrikaBeanPostprocessor
,如果Bean实现了BeanMapperRegistry 接口,我们就调用它的registry方法注册映射规则。
@Component
public class OrikaBeanPostprocessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof BeanMapperRegistry) {
((BeanMapperRegistry) bean).registry(BeanMapper.getMapperFactory());
}
return bean;
}
}
当然,我们要确保之前的转换类也是一个Bean。
@Component
public class OrderBeanMapper implements BeanMapperRegistry {
@Override
public void registry(MapperFactory mapperFactory) {
mapperFactory.classMap(OrderDTO.class, OrderDO.class)
.field("orderId","id")
.field("mobile","phone")
.field("orderItem.itemId","orderItem.id")
.byDefault()
.register();
}
}
@Component
public class UseBeanMapper implements BeanMapperRegistry {
@Override
public void registry(MapperFactory mapperFactory) {
mapperFactory.classMap(UserDTO.class, UserDO.class)
.field("username","name")
.byDefault().register();
}
}
测试:
public static void main(String[] args) {
// 1. 创建Spring容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 2. 指定扫描的包(确保BeanMapperPostProcessor和转换类都能被扫描到)
context.scan("fcp.orika.*");
context.refresh();
// 3. 测试转换
OrderDTO orderDTO = new OrderDTO("1", "kfc", "南京路", "666");
orderDTO.setOrderItem(new OrderItemDTO("1", "1", "汉堡包"));
UserDTO userDTO = new UserDTO("1", "小王");
System.out.println(BeanMapper.map(orderDTO, OrderDO.class));
System.out.println(BeanMapper.map(userDTO, UserDO.class));
}
使用这种方式,后续我们在增加转换规则的时候,只需要增加一个转换类,然后加个@Component注解就可以了。总体上来讲,还是比SPI更方便使用。