文章目录
- 概述
- Spring BeanUtils基本使用
- Code
- 忽略了属性类型导致拷贝失败
- 同一字段在不同的类中定义的类型不一致
- 同一个字段分别使用包装类和基本类型且没有传递实际值
- 布尔类型的属性分别使用了基本类型和包装类型且属性名使用is开头
- null值覆盖导致数据异常
- 内部类数据无法成功拷贝
- 浅拷贝 vs 深拷贝
- 引入了错误的包
- Performance - BeanUtils vs 原生set
- Apache Commons BeanUtils
概述
Spring BeanUtils 是 Spring 框架中的一部分,它提供了一套用于简化 Java 对象属性操作的工具类。尽管它的名字暗示了它可能与 Java Bean 相关,但实际上它并不操作 Java Bean 本身,而是操作对象的属性。
BeanUtils 的核心功能是提供属性复制的方法,这在需要将一个对象的属性值复制到另一个对象时非常有用。
Spring BeanUtils 的主要功能如下:
- 属性复制:
copyProperties
方法可以将一个对象的属性值复制到另一个对象中,前提是这两个对象中必须存在相同名称和类型的属性。 - 忽略特定属性:
copyProperties
方法可以指定一个或多个属性不被复制,通过传递一个字符串数组或单个字符串参数来实现。 - 类型匹配:Spring BeanUtils 会在复制属性时检查源对象和目标对象的属性类型是否匹配,如果不匹配,则不会复制该属性。
- 编辑域限制:可以指定哪些类及其父类中的属性可以被复制,通过传递一个
Class<?>
参数来实现。
使用 Spring BeanUtils 的好处是能够减少样板代码,提高代码的可读性和可维护性。例如,当你需要创建一个新对象并将其设置为与另一个对象相同的状态时,使用 BeanUtils 可以避免手动设置每个属性。
Spring BeanUtils 的使用场景非常广泛,尤其在需要对象间属性同步或数据传输对象(Data Transfer Object, DTO)转换时,它提供了一个简单而有效的解决方案。在 Spring MVC 中,它也常用于将请求参数映射到服务层的对象中。
需要注意的是,Spring BeanUtils 和 Apache Commons BeanUtils 是两个不同的库,虽然它们都提供了类似的功能,但在使用时需要明确区分。Spring 的 BeanUtils 通常被认为在性能上进行了优化,并且与 Spring 框架的其他部分集成得更好。
Spring BeanUtils基本使用
基本使用很简单,这里就不演示了,主要是熟悉下API即可 。
可以看下面的链接。
Spring - Copying properties using BeanUtils
Code
请注意看注释
忽略了属性类型导致拷贝失败
同一字段在不同的类中定义的类型不一致
两个Entity
同样为id , 一个是String类型,一个是Long类型 , 此时如果使用BeanUtils.copyProperties进行拷贝,会出现拷贝失败的现象,导致对应的字段为null
package com.artisan.bootbeanutils.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Source {
// id 类型为 String
private String id;
private String username;
}
package com.artisan.bootbeanutils.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Target {
// id 类型为 Long
private Long id;
private String username;
}
单元测试
package com.artisan.bootbeanutils;
import com.artisan.bootbeanutils.entity.Source;
import com.artisan.bootbeanutils.entity.Target;
import com.artisan.bootbeanutils.entity2.SourceWrappedValue;
import com.artisan.bootbeanutils.entity2.TargetPrimitiveValue;
import com.artisan.bootbeanutils.entity3.SourceBoolean;
import com.artisan.bootbeanutils.entity3.TargetBoolean;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
/**
* 属性类型不一致导致拷贝失败
*/
@SpringBootTest
class BootBeanUtilsApplicationTests1 {
/**
* 同一属性的类型不同
* <p>
* 在开发中,很可能会出现同一字段在不同的类中定义的类型不一致
* 例如ID,可能在A类中定义的类型为Long,在B类中定义的类型为String,
* 此时如果使用BeanUtils.copyProperties进行拷贝,会出现拷贝失败的现象,导致对应的字段为null
*/
@Test
public void testDiffPropertyType() {
// Source 和 Target 虽然都有 id属性,但类型却不同 一个为String 一个为Long
Source source = new Source("1", "artisan");
Target target = new Target();
// 通过BeanUtils的copyProperties方法完成对象之间属性的拷贝
BeanUtils.copyProperties(source, target);
System.out.println(source);
System.out.println(target);
// 输出
Assert.notNull(target.getUsername(), "copy过来的username属性不应为null, 请检查");
Assert.notNull(target.getId(), "copy过来的id属性不应为null, 请检查");
}
}
同一个字段分别使用包装类和基本类型且没有传递实际值
两个Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SourceWrappedValue {
// 包装类型
private Long id;
private String username;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TargetPrimitiveValue {
// 基本类型
private long id;
private String username;
}
单元测试
/**
* 如果同一个字段分别使用包装类和基本类型,在没有传递实际值的时候,会出现异常
* <p>
* 在没有传递实际值的时候,会出现异常
* 在没有传递实际值的时候,会出现异常
* 在没有传递实际值的时候,会出现异常
* </p>
*/
@Test
public void testWrappedValue() {
// 在传递了实际的值的情况下, 不会抛出异常
// 在传递了实际的值的情况下, 不会抛出异常
// 在传递了实际的值的情况下, 不会抛出异常
SourceWrappedValue wrappedValue = new SourceWrappedValue(1L, "artisan");
TargetPrimitiveValue primitiveValue = new TargetPrimitiveValue();
// 属性copy
BeanUtils.copyProperties(wrappedValue, primitiveValue);
System.out.println(primitiveValue);
System.out.println(wrappedValue);
// 输出
Assert.notNull(primitiveValue.getId(), "copy过来的id属性不应为null, 请检查");
Assert.notNull(primitiveValue.getUsername(), "copy过来的username属性不应为null, 请检查");
System.out.println("========================");
// 在没有传递了实际的值的情况下, 会抛出异常
// 在没有传递了实际的值的情况下, 会抛出异常
// 在没有传递了实际的值的情况下, 会抛出异常
SourceWrappedValue sourceWrappedValue = new SourceWrappedValue();
sourceWrappedValue.setUsername("artisanTest");
TargetPrimitiveValue targetPrimitiveValue = new TargetPrimitiveValue();
// 属性copy (这里就会抛出异常 FatalBeanException: Could not copy property 'id' from source to target)
BeanUtils.copyProperties(sourceWrappedValue, targetPrimitiveValue);
System.out.println(sourceWrappedValue);
System.out.println(targetPrimitiveValue);
Assert.notNull(targetPrimitiveValue.getId(), "copy过来的id属性不应为null, 请检查");
Assert.notNull(targetPrimitiveValue.getUsername(), "copy过来的username属性不应为null, 请检查");
}
布尔类型的属性分别使用了基本类型和包装类型且属性名使用is开头
两个Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SourceBoolean {
private Long id;
private String username;
// 基本类型,且属性名如果使用is开头
private boolean isDone;
// 基本类型,属性名没有使用is开头
private boolean finished;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TargetBoolean {
private Long id;
private String username;
// 包装类型,且属性名如果使用is开头
private Boolean isDone;
// 基本类型,属性名没有使用is开头
private Boolean finished;
}
单元测试
/**
* 如果一个布尔类型的属性分别使用了基本类型和包装类型,且属性名如果使用is开头,例如isDone,也会导致拷贝失败
*/
@Test
public void testBooleanAndIsXxx() {
// 在传递了实际的值的情况下, 不会抛出异常
SourceBoolean sourceBoolean = new SourceBoolean(1L, "artisan", true, false);
TargetBoolean targetBoolean = new TargetBoolean();
// 属性copy
BeanUtils.copyProperties(sourceBoolean, targetBoolean);
System.out.println(sourceBoolean);
System.out.println(targetBoolean);
// 输出
Assert.notNull(targetBoolean.getIsDone(), "copy过来的isDone属性不应为null, 请检查");
Assert.notNull(targetBoolean.getFinished(), "copy过来的finished属性不应为null, 请检查");
}
null值覆盖导致数据异常
两个Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Source {
private String id;
private String username;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Target {
private String id;
private String username;
}
单元测试
/**
* null值覆盖导致数据异常
*/
@SpringBootTest
class BootBeanUtilsApplicationTests2 {
/**
* 开发过程中,可能会有部分字段拷贝的需求,被拷贝的数据里面如果某些字段有null值存在,
* 但是对应的需要被拷贝过去的数据的相同字段的值并不为null,
* 如果直接使用 BeanUtils.copyProperties 进行数据拷贝,就会出现被拷贝数据的null值覆盖拷贝目标数据的字段,导致原有的数据失效
*/
@Test
public void testNullCopyToNotNull() {
// 模拟 username为null
Source source = new Source();
source.setId("1");
System.out.println("original source data: " + source);
// 模拟 username不为null
Target target = new Target();
target.setUsername("artisan");
System.out.println("original target data: " + target);
// 属性copy
BeanUtils.copyProperties(source, target);
System.out.println("copied target data: " + target);
Assert.notNull(target.getUsername(), "username不应为空, 请检查");
}
}
内部类数据无法成功拷贝
Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Source {
private Long id;
private String username;
// 内部类
private InnerClass innerClass;
@Data
@AllArgsConstructor
public static class InnerClass {
public String innerName;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Target {
private Long id;
private String username;
// 内部类
private InnerClass innerClass;
@Data
@AllArgsConstructor
public static class InnerClass {
public String innerName;
}
}
单元测试
/**
* 内部类数据无法成功拷贝
*/
@SpringBootTest
class BootBeanUtilsApplicationTests4 {
/**
* 内部类数据无法正常拷贝,即便类型和字段名均相同也无法拷贝成功
*/
@Test
public void testInnerClassCopy() {
// 模拟内部类
Source source = new Source(1L, "artisan", new Source.InnerClass("artisan-inner"));
Target target = new Target();
// 属性copy
BeanUtils.copyProperties(source, target);
System.out.println("source data: " + source);
System.out.println("copied data: " + target);
Assert.notNull(target.getInnerClass().getInnerName(), "Target#InnerClass#innername不应为空, 请检查");
}
}
浅拷贝 vs 深拷贝
Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PojoA {
private String name;
private PojoB pojoB;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PojoB {
private String info;
}
单元测试
/**
* BeanUtils.copyProperties是浅拷贝
* <p>
* 一旦在拷贝后修改了原始对象的引用类型的数据,就会导致拷贝数据的值发生异常,这种问题排查起来比较困难
*/
@SpringBootTest
class BootBeanUtilsApplicationTests5 {
/**
* 浅拷贝是指创建一个新对象,该对象的属性值与原始对象相同,但对于引用类型的属性,仍然共享相同的引用。
* 也就是说在浅拷贝下,当原始内容的引用属性值发生变化时,被拷贝对象的引用属性值也会随之发生变化。
* <p>
* 深拷贝是指创建一个新对象,该对象的属性值与原始对象相同,包括引用类型的属性。
* 深拷贝会递归复制引用对象,创建全新的对象,所以深拷贝拷贝后的对象与原始对象完全独立。
*/
@Test
public void testShadowCopy() {
PojoA sourcePojoA = new PojoA("artisan", new PojoB("pojoB"));
PojoA targetPojoA = new PojoA();
// 属性复制
BeanUtils.copyProperties(sourcePojoA, targetPojoA);
System.out.println(targetPojoA);
System.out.println("修改源sourcePojoA中对象的属性值,观察targetPojoA中的值是否有变化,用于验证是否是浅复制....");
// 修改source的属性,观察target属性值的变化
sourcePojoA.getPojoB().setInfo("测试Modify");
System.out.println(targetPojoA);
// 浅拷贝 原始对象值被修改后,目标对象的值也会被修改
Assert.isTrue("测试Modify".equals(targetPojoA.getPojoB().getInfo()), "浅复制BeanUtils.copyProperties");
}
}
引入了错误的包
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
package com.artisan.bootbeanutils;
import com.artisan.bootbeanutils.entity5.Source;
import com.artisan.bootbeanutils.entity5.Target;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
import java.lang.reflect.InvocationTargetException;
/**
* 导包错误导致拷贝数据异常
*/
@SpringBootTest
class BootBeanUtilsApplicationTests3 {
/**
* 如果项目中同时引入了Spring的beans包和Apache的beanutils包,
* 在导包的时候,如果导入错误,很可能导致数据拷贝失败,排查起来也不太好发现。
* <p>
* 我们通常使用的是Spring包中的拷贝方法
* <p>
* //org.springframework.beans.BeanUtils(源对象在左边,目标对象在右边)
* public static void copyProperties(Object source, Object target) throws BeansException
* <p>
* //org.apache.commons.beanutils.BeanUtils(源对象在右边,目标对象在左边)
* public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException
*/
@Test
public void testNullCopyToNotNull() throws InvocationTargetException, IllegalAccessException {
// 模拟 username为null
Source source = new Source();
source.setId("1");
// 模拟 username不为null
Target target = new Target();
target.setUsername("artisan");
// 属性copy
BeanUtils.copyProperties(source, target);
System.out.println("copied data: " + target);
//Assert.notNull(target.getUsername(), "username不应为空, 请检查");
System.out.println("============使用Apache Common 的BeanUtils============");
// Apache的BeanUtils属性copy --> 源对象在右边,目标对象在左边
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);
System.out.println("copied data: " + target);
Assert.notNull(target.getUsername(), "username不应为空, 请检查");
}
}
Performance - BeanUtils vs 原生set
/**
* BeanUtils.copyProperties底层是通过反射获取到对象的set和get方法,再通过get、set完成数据的拷贝,整体拷贝效率较低
*/
@SpringBootTest
class BootBeanUtilsApplicationTests6 {
@Test
public void testPerformance() {
PojoA sourcePojoA = new PojoA("artisan", new PojoB("pojoB"));
PojoA targetPojoA = new PojoA();
StopWatch stopWatch = new StopWatch("BeanUtils#copyProperties Vs Set");
stopWatch.start("copyProperties");
for (int i = 0; i < 50000; i++) {
BeanUtils.copyProperties(sourcePojoA, targetPojoA);
}
stopWatch.stop();
stopWatch.start("set");
for (int i = 0; i < 50000; i++) {
targetPojoA.setPojoB(sourcePojoA.getPojoB());
targetPojoA.setName(sourcePojoA.getName());
}
stopWatch.stop();
// 打印时间
System.out.println(stopWatch.prettyPrint());
}
}