文章目录
- 前言
- 何不可变类
- 对象复制方式
-
- 1.直接赋值
- 2.浅拷贝
- 3.深拷贝
- 对象复制方案
-
- 1.get/set
- 2.Spring BeanUtils
- 3.Apache BeanUtils
- 4.BeanCopier
- 5.Orika
- 6.Dozer
- 7.MapStruct
- 8.Bean Mapping
- 9.Bean Mapping ASM
- 10.ModelMapper
- 11.JMapper
- 12.Json2Json
- 复制方案选择
前言
在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息。比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。
这种转换最原始的方式就是手动编写大量的get/set代码,属性少的时候还好,属性多的时候就非常繁琐了,一个合格的程序员显然不会仅仅局限于get/set。
针对这个问题,市场上诞生了很多方便的类库,用于对象拷贝。常用的有apache BeanUtils、spring BeanUtils、Dozer、Orika等拷贝工具。
何不可变类
当类的实例一经创建,其内容便不可改变,即无法修改其成员变量。
Java中有一些特殊的类是不可变类,八个基本类型的包装类和String类都属于不可变类。
不可变类的特殊性:
有两个不可变类对象,当两个对象指向同一引用时,修改某一对象的值,不会对另一个对象造成影响。
下面举例说明
-
源码:
public static void main(String[] args) { Integer a = 1; String aa = "1"; Integer b = a; String bb = aa; System.out.println("a修改前b:"+b); System.out.println("aa修改前bb:"+bb); a = 2; aa = "2"; System.out.println("a修改后b:"+b); System.out.println("aa修改后bb:"+bb); }
-
debug:
-
日志:
a修改前b:1 aa修改前bb:1 a修改后b:1 aa修改后bb:1
由此可见,a和aa的修改不会影响b和bb的值。
方法参数的传递:
- 基本类型传递的是值;
- 引用类型传递的是对象的引用。
- 不可变类型,在方法里修改了对象的值,也不会影响到原对象(不可变性);
- 可变类型,在方法里修改了对象的值,原对象相应的值也会变动。
对象复制方式
对象复制有三种方式:直接赋值、浅拷贝、深拷贝。
1.直接赋值
- 基本数据类型复制的是值;
- 引用数据类型复制的是对象的引用,原始对象及目标对象引用的是同一个对象。
2.浅拷贝
创建一个新对象,然后将当前对象的非静态字段复制到该新对象。
- 基本数据类型复制的是值;
- 引用数据类型复制的是对象的引用(不可变类型特殊)。
注意:String类型、Integer等基本数据类型的包装类型,因为时不可变类型,所以即使进行的是浅拷贝,原始对象的改变并不会影响目标对象。
3.深拷贝
创建一个新对象,然后将当前对象的非静态字段复制到该新对象。
- 无论该字段是基本类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
对象复制方案
一个好用的属性复制方案,需要有哪些特性:
- 支持基本的属性复制;
- 支持不同类型的属性赋值,比如基本类型与其包装类型等;
- 支持不同字段名的属性赋值,当然字段名应该尽量保持一致,但是实际业务中,确实会有字段名不一致的情况;
- 浅拷贝/深拷贝,浅拷贝会引用同一对象,如果稍微不慎,同时改动对象,就会踩到意想不到的坑。
市场上的对象转换方案主要分类:
- 直接编写get、set代码(硬编码);
- 通过反射实现;
- 编译期生成get、set代码;
- 基于AOP、ASM、CGlib等技术实现。
12种对象转换方案归纳:
方案 | 推荐指数 | 性能指数 | 原理 | 点评 |
---|---|---|---|---|
get/set | ★★★☆☆ | ★★★★★ | 手写get、set | 日常使用最多,性能好,只是较麻烦,需要手写。 |
Spring BeanUtils | ★★★☆☆ | ★★★★☆ | 基于反射 | 日常使用较多,性能较好,推荐使用 |
Apache BeanUtils | ☆☆☆☆☆ | ★☆☆☆☆ | 基于反射 | 兼容性较差,性能差,不推荐使用 |
BeanCopier | ★★★☆☆ | ★★★★☆ | 基于CGlib | 性能较好,使用也不复杂,可以使用 |
Orika | ★★☆☆☆ | ★★★☆☆ | 基于Javasisst字节码增强 | 性能不太突出 |
Dozer | ★☆☆☆☆ | ★★☆☆☆ | 基于反射的属性映射(递归映射) | 性能较差,不太推荐使用 |
MapStruct | ★★★★★ | ★★★★★ | 编译期生成get、set | 性能好,结合到框架中使用方便,推荐使用 |
Bean Mapping | ★★☆☆☆ | ★★★☆☆ | 基于反射 | 性能一般,不太推荐使用 |
Bean Mapping ASM | ★★★☆☆ | ★★★★☆ | 基于ASM字节码增强 | 性能较好,但暂时不够灵活,可以使用 |
ModelMapper | ★★★☆☆ | ★★★☆☆ | 基于反射 | 性能一般,不太推荐使用 |
JMapper | ★★★★☆ | ★★★★★ | 映射器方式实现 | 性能较好,使用略微麻烦,可以使用 |
Json2Json | ☆☆☆☆☆ | ★☆☆☆☆ | 基于JSON序列化和反序列化 | 野路子,性能较差,不推荐使用 |
分别测试这12种属性转换操作分别在一百次、一千次、一万次、十万次、一百万次时候的性能时间对比。
- BeanUtils.copyProperties是大家代码里最常出现的工具类,但只要你不把它用错成Apache包下的,而是使用Spring提供的,就基本还不会对性能造成多大影响。
- 但如果说性能更好,可替代手动get、set的,还是MapStruct更好用,因为它本身就是在编译期生成get、set代码,和我们写get、set一样。
- 其他一些组件包主要基于AOP、ASM、CGlib等技术手段实现的,所以也会有相应的性能损耗。
1.get/set
直接手写get/set方法实现数据的复制。
这种方式也是日常使用的最多的,性能较好,就是操作起来有点麻烦。尤其是当对象属性较多的时候。
减少手写代码的方式:
- 通过一些快捷的操作方式,比如你可以通过 Shift+Alt 选中所有属性,Shift+Tab 归并到一列,接下来在使用 Alt 选中这一列,批量操作粘贴 userDTO.set 以及快捷键大写属性首字母,最后切换到结尾补充括号和分号,最终格式化一下就搞定了。
- 通过IDEA的插件,如GenerateAllSetter插件。光标移至需生成get/set方法的对象名称,alt+enter出现快捷选项,选择需要生成的选项,自动生成相应的代码。
2.Spring BeanUtils
同样是基于反射的属性拷贝(Introspector机制获取到类的属性来进行赋值操作)。Spring 提供的copyProperties要比Apache好用得多,这也是大家用得比较多的一种复制方式。
Spring BeanUtils的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。成员变量赋值是基于目标对象的成员列表,并且会跳过ignoreProperties的以及在源对象中不存在,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同。
Introspector:
是一个专门处理bean的工具类,用来获取Bean体系里的propertiesDescriptor、methodDescriptor利用反射获取Method信息,是反射的上层。只进行一次反射解析,通过WeakReference静态类级别缓存Method,在jvm不够时会被回收。
特点:
- 字段名不一致,属性无法复制;
- 类型不一致,属性无法复制。但是注意,如果类型为基本类型以及基本类型的包装类,这种可以转化;
- 浅拷贝。
依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
方法:
/**
* source:源对象
* target:目标对象
* editable:目标对象类的Class对象(需复制的属性基于该Class,当该值为null时需复制的属性基于目标对象的Class)
* ignoreProperties:需忽略的属性列表
*/
BeanUtils.copyProperties(Object source, Object target)
BeanUtils.copyProperties(Object source, Object target, Class<?> editable)
BeanUtils.copyProperties(Object source, Object target, String... ignoreProperties)
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDTO, userVO);
System.out.println(JSON.toJSONString(userVO));
}
3.Apache BeanUtils
推荐:☆☆☆☆☆
性能:★☆☆☆☆
手段:Introspector机制获取到类的属性来进行赋值操作
点评:兼容性交差,效率较低,不建议使用
Apache BeanUtils使用起来很方便,不过其底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,做了类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,过度的追求完美反而导致兼容性变差,也导致了性能较低,所以阿里巴巴开发手册上强制避免使用Apache BeanUtils。
特点:
- 字段名不一致的属性无法被复制;
- 类型不一致的字段,将会进行默认类型转化;
- 浅拷贝。
依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
方法:
/**
* dest:目标对象
* orig:源对象
*/
BeanUtils.copyProperties(Object dest, Object orig)
实现:
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userVO, userDTO);
System.out.println(JSON.toJSONString(userVO));
}
4.BeanCopier
Cglib BeanCopier的原理与上面两个Beanutils原理不太一样,其主要使用CGlib字节码技术动态生成一个代理类,代理类实现get和set方法。生成代理类过程存在一定开销,但是一旦生成,我们可以缓存起来重复使用,所有Cglib性能相比以上两种Beanutils性能比较好。
特点:
- 字段名不一致,属性无法复制
- 类型不一致,属性无法复制。如果类型为基本类型/基本类型的包装类型,这两者也无法被拷贝。但可自定义转换器实现不同类型的拷贝。
- 浅拷贝
依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
方法:
/**
* source 源Class
* target 目Class
* useConverter 是否使用转换器
* from 源对象
* to 目标对象
* converter 转换器
*/
BeanCopier beanCopier = BeanCopier.create(Class source, Class target, boolean useConverter);
beanCopier.copy(Object from, Object to, Converter converter);
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
BeanCopier beanCopier = BeanCopier.create(UserDTO.class, UserVO.class, false);
beanCopier.copy(userDTO, userVO, null);
System.out.println(JSON.toJSONString(userVO));
}
5.Orika
Orika也是一个跟Dozer类似的重量级属性复制工具类,也提供诸如Dozer类似的功能。但是Orika无需使用繁琐 XML配置,它自身提供一套非常简洁的 API 用法,非常容易上手。
Orika底层基于Javassist生成字段属性的映射的字节码,然后直接动态加载执行字节码文件,相比于Dozer的这种使用反射原来的工具类,速度上会快很多。Orika的整个流程其实是需要使用到Java的反射的,只是在真正拷贝的属性的时候没有使用反射。
Orika的执行流程:
- 先通过内省(反射)把JavaBean的属性(getset方法等)解析出来;
- 进而匹配目标和源的属性;
- 接着根据这些属性和目标/源的匹配情况基于Javasisst生成一个 GeneratedMapper的代理对象(真正的执行复制的对象)并放到缓存中;
- 接着就基于这个对象的 mapAtoB和mapBtoA方法对属性进行复制。
Orikade的使用需要创建两个对象MapperFactory与MapperFacade,其中MapperFactory 可以用于字段映射,配置转换器等,而MapperFacade 的作用就与Beanutils一样,用于负责对象的之间的映射。
特点:
- 默认支持类型不一致(基本类型/包装类型)转换
- 指定不同字段名映射关系,属性可以被成功复制
- 深拷贝
依赖:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
方法:
/**
* sourceObject 源对象
* destinationClass 目标Class
* targetObject 目标对象
*/
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapper = mapperFactory.getMapperFacade();
D targetObject = mapper.map(S sourceObject, Class<D> destinationClass);
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapper = mapperFactory.getMapperFacade();
UserVO userVO = mapper.map(userDTO, UserVO.class);
System.out.println(JSON.toJSONString(userVO));
}
6.Dozer
Dozer相对BeanUtils这类工具类来说,拥有许多高级功能,所以相对来说这是一个重量级工具类。其底层本质上还是使用了反射完成属性的复制(属性映射,递归的方式复制对象),所以执行速度并不是那么理想。
Dozer需要我们新建一个DozerBeanMapper,这个类作用等同与BeanUtils,负责对象之间的映射,属性复制。
生成DozerBeanMapper实例需要加载配置文件,随意生成代价比较高。因此在我们应用程序中,应该尽量使用单例模式,重复使用DozerBeanMapper。
另外,强大的配置功能,我们可以通过XML、API或注解的方式配置源对象和目标对象属性映射关系和类型转换。
特点:
- 类型不一致的字段,属性被复制
- 通过配置字段名的映射关系,不一样字段的属性也被复制
- 深拷贝
依赖:
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.4.0</version>
</dependency>
方法:
/**
* source 源对象
* destinationClass 目标Class
* target 目标对象
*/
DozerBeanMapper mapper = new DozerBeanMapper();
T target = mapper.map(Object source, Class<T> destinationClass);
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
DozerBeanMapper mapper = new DozerBeanMapper();
UserVO userVO = mapper.map(userDTO, UserVO.class);
System.out.println(JSON.toJSONString(userVO));
}
7.MapStruct
MapStruct运行速度与硬编码差不多,这是因为他在编译期间就生成了Java Bean属性复制的代码(属性对应的get、set),运行期间就无需使用反射或者字节码技术,所以确保了高性能。
与硬编码方式相比,不管使用反射,还是使用字节码技术,这些都需要在代码运行期间动态执行所以它们的执行速度都会比硬编码慢很多。
特点:
- 名不一致,默认不支持复制
- 类型不一致,默认不支持复制(但支持基本类型与包装类型、基本类型的包装类型与String的自动转换)
- 可通注解配置实现名称不一致、类型不一致的复制
- 是深拷贝
依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
插件:
由于MapStruct需要在编译器期间生成代码,所以我们需要maven-compiler-plugin插件中配置。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
方法:
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mappings({
@Mapping(source = "id", target = "id"),
@Mapping(source = "createTime", target = "createTime")
})
UserVO dtoToVo(UserDTO userDTO);
}
编译:
MapStruct没有想象中的神奇,其实就是在编译期生成了接口的实现类,里面的转换方法实现了转换功能。相当于帮我们手写get/set设值,所以它的性能会很好。
public class UserMapperImpl implements UserMapper {
public UserMapperImpl() {
}
public UserVO dtoToVo(UserDTO userDTO) {
if (userDTO == null) {
return null;
} else {
UserVO userVO = new UserVO();
userVO.setIdd(userDTO.getId());
userVO.setCreateTime(userDTO.getCreateTime());
userVO.setUserName(userDTO.getUserName());
return userVO;
}
}
}
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = UserMapper.INSTANCE.dtoToVo(userDTO);
System.out.println(JSON.toJSONString(userVO));
}
可能出现的问题:
- 如果我们对象使用 Lombok 的话,使用 @Mapping指定不同字段名,编译期间可能会抛出如下的错误
原因主要是因为Lombok也需要编译期间自动生成代码,这就可能导致两者冲突,当MapStruct生成代码时,还不存在Lombok生成的代码。解决办法可以在 maven-compiler-plugin插件配置中加入Lombok。
8.Bean Mapping
基于反射的属性拷贝。0.0.2版本引入了@BeanMapping,通过@BeanMapping注解可实现灵活的复制方式。
注解定义在 bean-mapping-api 模块中,bean-mapping-core 会默认引入此模块。
特点:
- 名不一致,不支持复制
- 类型不一致,不支持复制(但支持基本类型转为包装类型,反过来不支持)
通过@BeanMapping注解来灵活控制复制,支持名称不一致的复制、类型不一致的复制和控制是否复制。
- 浅拷贝
依赖:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>bean-mapping-core</artifactId>
<version>0.2.5</version>
</dependency>
注解:
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanMapping {
/**
* 字段别名
* 如果不填,则默认使用字段的名称
* 会将source的属性值赋值给target和当前name属性一致的属性
* @return 字段别名
*/
String name() default "";
/**
* 生效条件(默认为生效)
* 1.当放在source字段上时,表示是否将值赋给target字段
* 2.当放在target字段上时,表示是否接受赋值。
* 3.source+target只有同时生效时,才会发生赋值。
* @return 具体的生效实现
*/
Class<? extends ICondition> condition() default ICondition.class;
/**
* 类型转换(默认不进行转换)
* 当source的值转换后可以设置给target,才会将source转换后的值赋值给target对应属性,其他情况不会对值产生影响。
* @return 具体的转换实现
*/
Class<? extends IConvert> convert() default IConvert.class;
}
方法:
/**
* source 源对象
* target 目标对象
*/
BeanUtil.copyProperties(Object source, Object target)
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
BeanUtil.copyProperties(userDTO, userVO);
System.out.println(JSON.toJSONString(userVO));
}
9.Bean Mapping ASM
推荐:★★★☆☆
性能:★★★★☆
手段:基于ASM字节码框架实现
点评:与普通的Bean Mapping 相比,性能有所提升,可以使用。
Bean Mapping基于ASM的字节码增强技术的复制方式要比Bean Mapping普通的方式新能要提升不少,但有个缺点就是暂不支持@BeanMapping注解等更加丰富的功能。
特点:
- 名不一致,不支持复制
- 类型不一致,不支持复制(但支持基本类型转为包装类型,反过来不支持)
- 效率比传统的Bean Mapping要好些,但暂不支持@BeanMapping注解的灵活复制
- 浅拷贝
依赖:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>bean-mapping-asm</artifactId>
<version>0.2.5</version>
</dependency>
方法:
/**
* source 源对象
* target 目标对象
*/
AsmBeanUtil.copyProperties(Object source, Object target)
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
AsmBeanUtil.copyProperties(userDTO, userVO);
System.out.println(JSON.toJSONString(userVO));
}
10.ModelMapper
ModelMapper是利用反射的原理实现的。转换对象数量较少时性能不错,如果同时大批量转换对象,性能有所下降。
依赖:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.0</version>
</dependency>
实现:
简单使用
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = new UserVO();
ModelMapper modelMapper = new ModelMapper();
modelMapper.map(userDTO, userVO);
System.out.println(JSON.toJSONString(userVO));
}
ModelMapper的具体使用可参考文章:实体映射类库(modelmapper和MapStruct)
11.JMapper
JMapper通过映射器方式实现。
依赖:
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.0</version>
</dependency>
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
JMapper<UserVO, UserDTO> jMapper = new JMapper<>(UserVO.class, UserDTO.class, new JMapperAPI()
.add(JMapperAPI.mappedClass(UserVO.class)
.add(JMapperAPI.attribute("id").value("id"))
.add(JMapperAPI.attribute("userName").value("userName"))
.add(JMapperAPI.attribute("createTime").value("createTime"))
));
UserVO userVO = jMapper.getDestination(userDTO);
System.out.println(JSON.toJSONString(userVO));
}
12.Json2Json
这种通过JSON序列化和反序列化的方式,把源对象转为JSON串,再把JSON串转为目标对象,虽然也能达到复制的目的,但不推荐使用。
实现:
public static void main(String[] args) {
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("哈哈");
userDTO.setCreateTime(new Date());
UserVO userVO = JSON.parseObject(JSON.toJSONString(userDTO), UserVO.class);
System.out.println(JSON.toJSONString(userVO));
}
复制方案选择
- 手写get/set肯定是效率最高的;
- 不要使用Apache Beanutils,因为效率低,阿里巴巴规范都直接禁止使用;
- 一般情况使用Spring Beanutils就可以了,效率OK,且本来就是Spring中的东西,不用依赖其它包。
参考文章:对比 12 种 Bean 自动映射工具