无涯

无所谓无 无所谓有

轻量高效的Java属性映射工具oneyoung-converter

前言

在多层应用中,不管是之前的经典三层模型、还是现在非常流行的DDD模型,都需要对各种不同的分层对象(JavaBean)进行转换,也就是JavaBean的属性映射问题。最常见和最简单的方式是编写对象属性转换函数,即普通的 Getter/Setter 方法。除此之外各种各种属性映射工具。

几种常见的 Java 属性映射工具及原理

工具

  1. org.springframework.beans.BeanUtils#copyProperties
  2. org.apache.commons.beanutils.BeanUtils#copyProperties
  3. net.sf.cglib.beans.BeanCopier#copy
  4. mapstruct

原理

反射技术

Spring框架的BeanUtilsAppachBeanUtils都是使用的Java反射技术实现映射。效率相对较差。

字节码生成技术

cglibBeanCopier使用的是动态生成字节码的技术,动态生成对象间的转换器,与手动写gettersetter性能相当。

编译期技术

类似Lombokmapstruct其实也是一种代码生成技术,在编译期生成真实的转换代码,所以性能也与手动写gettersetter性能相当。

因此从性能来讲首推 Getter/Setter 方式(含 MapStruct),其次是 cglib

选用哪种技术?

属性转换工具的优势:用起来方便,往往一行行代码就实现多属性的转换。

mapstruct在属性不对应的情况下,还可以通过注解或者修改配置方式自动适配,功能非常强大。

属性转换工具的缺点

  1. 多次对象映射(从 A 映射到 B,再从 B 映射到 C)如果属性不完全一致容易出错;
  2. 有些转换工具,属性类型不一致自动转换容易出现意想不到的 BUG;
  3. 基于反射和字节码增强技术的映射工具实现的映射,对一个类属性的修改不容易感知到对其它转换类的影响。

我们可以想想这样一个场景

一个UserDO如果属性超多,转换到UserDTO再被转换成UserVO。如果你修改UserDTO的一个属性命名,其它类待映射的类新增的对应属性有一个字母写错了,编译期间不容易发现问题,造成 BUG。

如果使用原始的 Getter/Setter 方式转换,修改了UserDO的属性,那么转换代码就会报错,编译都不通过,这样就可以逆向提醒我们注意到属性的变动的影响。

因此强烈建议使用定义转换类和转换函数,使用插件实现转换,不需要引入其它库,降低了复杂性,可以支持更灵活的映射。

大家可以想想这种场景:

如果一个 A 映射到 B,B 有两个属性来自 C,一个属性来自于传参或者计算等。

此时自定义转换函数就更方便。

如果使用属性映射工具推荐使用 MapStruct,更安全一些,转换效率也很高。

但是MapStruct也有他的缺点,对代码有一定的侵入性。及时两个对象拥有类型、名称相同属性,每两个转换对象需要定义Mapper

oneyoung-converter

oneyoung-converter结合了两种主流的映射方案。使用方式简单方便,开箱即用。

  • 支持自动映射,像BeanUtils一样,只需要传入源对象(Object)和目标对象(Class),即可完成转换。
    • 底层使用了Java动态编译技术,自动生成转换器代码,编译并载入JVM
    • 两个对象间,自动转换器只会生成一次,然后就会装入Cache复用
    • 性能与自定义Getter/Setter一致。
  • 支持自定义映射,你可以自定义转换器,只需要实现它提供的接口top.oneyoung.converter.Converter,并注册到转换器工厂。ConverterFactory.register(new CreateSchoolRequestToCreateSchoolServiceRequestConverter());
  • 提供自动映射启停开关,若开启自动映射且添加了自定义映射,将优先使用自定义的转换器完成映射。若关闭了自动映射且没有自定义转换器,则会抛出找不到转换器的异常ConvertException.

不管你是自动映射还是自定义映射,oneyoung-converter的使用方式只有下面这几种。

  1. 直接转换两个对象
1
2
CreateSchoolRequest request = CreateSchoolRequest.builder().name("test").build();
CreateSchoolServiceRequest createSchoolServiceRequest = Converter.directConvert(request, CreateSchoolServiceRequest.class);
  1. 转换集合对象
1
2
3
4
5
6
7
8
9
10
CreateSchoolRequest input = CreateSchoolRequest.builder()
.name("test1")
.address("test address1")
.build();
CreateSchoolRequest input1 = CreateSchoolRequest.builder()
.name("test2")
.address("test address2")
.build();
List<CreateSchoolRequest> createSchoolRequests = Arrays.asList(input, input1);
List<CreateSchoolServiceRequest> createSchoolServiceRequests = Converter.directConvertCollectionSingle(createSchoolRequests, CreateSchoolServiceRequest.class);

oneyoung-converter集成spring-boot

引入依赖

依赖已发布至Maven中央仓库

https://repo1.maven.org/maven2/top/oneyoung/

1
2
3
4
5
<dependency>
<groupId>top.oneyoung</groupId>
<artifactId>oneyoung-converter-starter</artifactId>
<version>1.0.0</version>
</dependency>

配置

1
2
# 控制自动映射开闭
oneyoung.converter.auto-convert=true

添加自定义映射

创建一个自定义转换器,只需要实现接口top.oneyoung.converter.Converter,并注册为spring bean,即会载入转换器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Component
public class RequestToResponseConverter implements Converter<Request, Response> {

@Override
public Response convert(Request source) {
if (source == null) {
return null;
}
log.info("RequestToResponseConverter.convert: {}", source);
Response target = new Response();
target.setName(source.getName());
return target;
}
}

spring在加载初期就会自动扫描需要加载的bean,通过实现BeanPostProcessor我们做了拦截,将Converter载入ConverterFactory

看到日志下面这种日志,说明我们成功注册了自定义转换器。

1
2022-04-16 12:26:56.584  INFO 75693 --- [           main] t.o.c.starter.SpringFactoryRegister      : ConverterFactory Register: class top.oneyoung.portal.entity.Request-->class top.oneyoung.portal.entity.Response By class top.oneyoung.portal.converter.RequestToResponseConverter

使用

与上面列举的使用方式一致。

开源

这个转换器已经被我开源。欢迎Star https://github.com/oneyoungg/oneyoung