orika入门使用及组件化

本篇文章,介绍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是很容易使用的

  1. 构建一个DefaultMapperFactory
  2. 如果存在字段名不一致的情况,告诉orika他们之间的映射关系
  3. 使用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)

改造后,以后增加新的映射规则的步骤就是:

  1. 添加一个类实现BeanMapperRegistry接口
  2. 在fcp.orika.BeanMapperRegistry 文件中追加新增类的全类名
    符合开闭原则

比如在本案例中,涉及的映射规则有:

  1. OrderDTO转OrderDo
  2. UserDTO转UserDo

对应的转换类就有俩个

  1. OrderBeanMapper
  2. 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更方便使用。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值