BeanUtils是用Spring的还是Apache的

Spring的BeanUtils和Apache的BeanUtils这两种工具实质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释。


前言

在Java中,除了 基本数据类型之外,还存在类的实例对象这个引用数据类型,而一般使用 “=” 号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。

反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

简单来说:

  • 浅拷贝:两个对象拷贝时,对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  • 深拷贝:两个对象拷贝时,对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

BeanUtils

Apache 的 BeanUtils

BeanUtils的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class PersonSource {
private Integer id;
private String username;
private String password;
private Integer age;
}
@Data
public class PersonDest {
private Integer id;
private String username;
private Integer age;
}
1
2
3
4
5
6
7
8
9
10
11
public class TestApacheBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
// 下面只是用于单独测试
PersonSource personSource = new PersonSource(1, "LiJing", "12345", 18);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personDest,personSource);
System.out.println("persondest: "+personDest);
}
}

// persondest: PersonDest{id=1, username='LiJing', age=18}

从上面的例子可以看出,对象拷贝非常简单,BeanUtils最常用的方法就是:

1
2
3
4
// 将源对象中的值拷贝到目标对象
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

默认情况下,使用org.apache.commons.beanutils.BeanUtils对复杂对象的复制是引用,这是一种浅拷贝

但是由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件上也明确指出:

Ali-Check | 避免用Apache Beanutils进行属性的copy。

commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,反射,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException {
// Validate existence of the specified beans
if (dest == null) {
throw new IllegalArgumentException
("No destination bean specified");
}
if (orig == null) {
throw new IllegalArgumentException("No origin bean specified");
}
if (log.isDebugEnabled()) {
log.debug("BeanUtils.copyProperties(" + dest + ", " +
orig + ")");
}
// Copy the properties, converting as necessary
if (orig instanceof DynaBean) {
final DynaProperty[] origDescriptors =
((DynaBean) orig).getDynaClass().getDynaProperties();
for (DynaProperty origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
// Need to check isReadable() for WrapDynaBean
// (see Jira issue# BEANUTILS-61)
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
final Object value = ((DynaBean) orig).get(name);
copyProperty(dest, name, value);
}
}
} else if (orig instanceof Map) {
@SuppressWarnings("unchecked")
final
// Map properties are always of type <String, Object>
Map<String, Object> propMap = (Map<String, Object>) orig;
for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
final String name = entry.getKey();
if (getPropertyUtils().isWriteable(dest, name)) {
copyProperty(dest, name, entry.getValue());
}
}
} else /* if (orig is a standard JavaBean) */ {
final PropertyDescriptor[] origDescriptors =
getPropertyUtils().getPropertyDescriptors(orig);
for (PropertyDescriptor origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
if ("class".equals(name)) {
continue; // No point in trying to set an object's class
}
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
try {
final Object value =
getPropertyUtils().getSimpleProperty(orig, name);
copyProperty(dest, name, value);
} catch (final NoSuchMethodException e) {
// Should not happen
}
}
}
}
}

Spring的 BeanUtils

使用Spring的BeanUtils进行对象拷贝:

1
2
3
4
5
6
7
8
9
10
public class TestSpringBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
// 下面只是用于单独测试
PersonSource personSource = new PersonSource(1, "LiJing", "12345", 18);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personSource,personDest);
System.out.println("persondest: "+personDest);
}
}
// persondest: PersonDest{id=1, username='LiJing', age=18}

Spring下的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。

去源码里面:

  • 版本:spring-beans-4.3.8.RELEASE
  • 方法:org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
}

actualEditable = editable;
}

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] var7 = targetPds;
int var8 = targetPds.length;

for(int var9 = 0; var9 < var8; ++var9) {
PropertyDescriptor targetPd = var7[var9];
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}

Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}

writeMethod.invoke(target, value);
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}
}

这个方法的源码其实很短,只有 44 行,我给大家把关键的地方写上注释,截图如下:

可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同。

所以从上面的源码解读中我可以得到这样的几条结论:

  1. 对于要复制的属性,目标对象必须要有对应的 set 方法(上图的第 27 行),源对象必须要有对应的 get 方法(上图的第 34 行)。
  2. 对于要复制的属性,目标对象和源对象的属性名称得一模一样。(上图的第 34 行)。
  3. 目标对象对应的 set 方法的入参和源对象对应的 get 方法的返回类型必须得一致(上图的第 37 行)。

进行对象之间的属性赋值

对象之间属性的赋值

使用org.springframework.beans.BeanUtils.copyProperties方法进行对象之间属性的赋值,避免通过getset方法一个一个属性的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 对象属性拷贝
* 将源对象的属性拷贝到目标对象
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyProperties(Object source, Object target) {
try {
BeanUtils.copyProperties(source, target);
} catch (BeansException e) {
LOGGER.error("BeanUtil property copy failed :BeansException", e);
} catch (Exception e) {
LOGGER.error("BeanUtil property copy failed:Exception", e);
}
}

List集合之间的对象属性赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param input 输入集合
* @param clzz 输出集合类型
* @param <E> 输入集合类型
* @param <T> 输出集合类型
* @return 返回集合
*/
public static <E, T> List<T> convertList2List(List<E> input, Class<T> clzz) {
List<T> output = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(input)) {
for (E source : input) {
T target = BeanUtils.instantiate(clzz);
BeanUtil.copyProperties(source, target);
output.add(target);
}
}
return output;
}

比如有两个类,User和Employee,将存储Employee对象的List赋给存储User对象的List。

User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class User {
private String name;
private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Employee类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Employee {
private String name;

private Integer age;
private String dept;

public Employee(String name, Integer age, String dept) {
this.name = name;
this.age = age;
this.dept = dept;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getDept() {
return dept;
}

public void setDept(String dept) {
this.dept = dept;
}

@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", dept='" + dept + '\'' +
'}';
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test(){
Employee ee1=new Employee("A",21,"it");
Employee ee2=new Employee("B",23,"account");
User user=new User();
BeanUtil.copyProperties(ee1, user);
System.out.println(user);
System.out.println("-------------分割线--------------");
List<User> output=new ArrayList<>();
List<Employee> source= Arrays.asList(ee1,ee2);
output=BeanUtil.convertList2List(source,User.class);
for (User str:output) {
System.out.println(str);
}
}

深拷贝,浅拷贝

这里说的是Spring的BeanUtils.copyProperties

场景

开发中经常遇到,把父类的属性拷贝到子类中。通常有2种方法:

  • 一个一个set
  • BeanUtils.copyProperties

很显然BeanUtils更加方便,也美观很多。

那么任何情况都能使用BeanUtils么,当然不是,要先了解他。

BeanUtils是深拷贝,还是浅拷贝?

是浅拷贝。

  • 浅拷贝:只是调用子对象的set方法,并没有将所有属性拷贝。(也就是说,引用的一个内存地址)
  • 深拷贝:将子对象的属性也拷贝过去。

什么情况适合用BeanUtils

如果都是单一的属性,那么不涉及到深拷贝的问题,适合用BeanUtils。

有子对象就一定不能用BeanUtils么

并不绝对,这个要区分考虑:

  • 子对象还要改动。
  • 子对象不怎么改动。

虽然有子对象,但是子对象并不怎么改动,那么用BeanUtils也是没问题的。

代码例子

下面用代码说明下。

  • 翠山有个儿子无忌,儿子继承了他的face和height。
  • 但是life应该是自己的。
  • 后来翠山自刎而死,无忌也变成dead状态了,这就是浅拷贝,无忌用的life引用的翠山的life对象。

Father类:

1
2
3
4
5
6
@Data
public class Father {
private String face; // 长相
private String height; // 身高
private Life life; // 生命
}

Life 类:

1
2
3
4
@Data
public class Life {
private String status;
}

Son类和main方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class Son extends Father{
private Life life;

public static void main(String[] args) {
Father cuishan = new Father();
cuishan.setFace("handsome");
cuishan.setHeight("180");
Life cuishanLife = new Life();
cuishanLife.setStatus("alive");
cuishan.setLife(cuishanLife);
Son wuji=new Son();
BeanUtils.copyProperties(cuishan,wuji);

// Life wujiLife = wuji.getLife();
// wujiLife.setStatus("alive");
// wuji.setLife(wujiLife);
// cuishanLife.setStatus("dead"); // 翠山后来自刎了

System.out.println(JSON.toJSONString(cuishan));
System.out.println(JSON.toJSONString(wuji));
}
}

上面注释出的代码可以如下替换:

case1和case2还是受浅拷贝的影响,case3不受。

case1:翠山自刎,无忌也挂了

1
2
3
4
//        Life wujiLife = wuji.getLife();
// wujiLife.setStatus("alive");
// wuji.setLife(wujiLife);
// cuishanLife.setStatus("dead"); // 翠山后来自刎了

case2:翠山自刎,无忌设置或者,翠山也活了

1
2
3
4
//        cuishanLife.setStatus("dead"); // 翠山后来自刎了
// Life wujiLife = wuji.getLife();
// wujiLife.setStatus("alive");
// wuji.setLife(wujiLife);

case3:翠山和无忌互不影响

1
2
3
4
5
cuishanLife.setStatus("dead"); // 翠山自刎了,该行放在上下均可
// 无忌用个新对象,不受翠山影响了
Life wujiLife = new Life();
wujiLife.setStatus("alive");
wuji.setLife(wujiLife);

dest,src 还是 src,dest

常见的BeanUtils有2个:

  • Spring有BeanUtils

  • apache的commons也有BeanUtils

区别如下:

这2个用哪个都行,但是要注意区别。

因为他们2个的src和dest是正好相反的,要特别留意。

小结

以上简要的分析两种BeanUtils,因为Apache下的BeanUtils性能较差,不建议使用,可以使用 Spring的BeanUtils,或者使用其他拷贝框架,比如 cglib BeanCopier,基于javassist的Orika等,这些也是非常优秀的类库,值得去尝试。

本文标题:BeanUtils是用Spring的还是Apache的

文章作者:LiJing

发布时间:2022年03月26日 - 13:47:13

最后更新:2023年06月03日 - 10:00:51

原始链接:https://blog-next.xiaojingge.com/posts/1699489757.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------