背景
在我們實際項目開發過程中,我們經常需要將不同的兩個對象實例進行屬性復制,從而基於源對象的屬性信息進行后續操作,而不改變源對象的屬性信息,比如DTO數據傳輸對象和數據對象DO,我們需要將DO對象進行屬性復制到DTO,但是對象格式又不一樣,所以我們需要編寫映射代碼將對象中的屬性值從一種類型轉換成另一種類型。
這種轉換最原始的方式就是手動編寫大量的 get/set代碼,當然這是我們開發過程不願意去做的,因為它確實顯得很繁瑣。為了解決這一痛點,就誕生了一些方便的類庫,常用的有 apache的 BeanUtils,spring的 BeanUtils, Dozer,Orika等拷貝工具。這篇文章主要介紹 Apache的BeanUtils 與 Spring 的BeanUtils,其他框架后續文章再做介紹
對象拷貝
在具體介紹兩種 BeanUtils之前,先來補充一些基礎知識。它們兩種工具本質上就是對象拷貝工具,而對象拷貝又分為深拷貝和淺拷貝,下面進行詳細解釋。
什么是淺拷貝和深拷貝
在Java中,除了 基本數據類型之外,還存在 類的實例對象這個引用數據類型,而一般使用 “=”號做賦值操作的時候,對於基本數據類型,實際上是拷貝的它的值,但是對於對象而言,其實賦值的只是這個對象的引用,將原對象的引用傳遞過去,他們實際還是指向的同一個對象。
而淺拷貝和深拷貝就是在這個基礎上做的區分,如果在拷貝這個對象的時候,只對基本數據類型進行了拷貝,而對引用數據類型只是進行引用的傳遞,而沒有真實的創建一個新的對象,則認為是淺拷貝。反之,在對引用數據類型進行拷貝的時候,創建了一個新的對象,並且復制其內的成員變量,則認為是深拷貝。
簡單來說:
淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此為淺拷貝
深拷貝: 對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並復制其內容,此為深拷貝。
BeanUtils
前面簡單講了一下對象拷貝的一些知識,下面就來具體看下兩種BeanUtils工具
apache 的 BeanUtils
首先來看一個非常簡單的BeanUtils的例子
public class PersonSource {
private Integer id;
private String username;
private String password;
private Integer age;
// getters/setters omiited
}
public class PersonDest {
private Integer id;
private String username;
private Integer age;
// getters/setters omiited
}
public class TestApacheBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
//下面只是用於單獨測試
PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personDest,personSource);
System.out.println("persondest: "+personDest);
}
}
persondest: PersonDest{id=1, username='pjmike', age=21}
從上面的例子可以看出,對象拷貝非常簡單,BeanUtils最常用的方法就是:
//將源對象中的值拷貝到目標對象
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 對於對象拷貝加了很多的檢驗,包括類型的轉換,甚至還會檢驗對象所屬的類的可訪問性,可謂相當復雜,這也造就了它的差勁的性能,具體實現代碼如下:
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進行對象拷貝:
public class TestSpringBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
//下面只是用於單獨測試
PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personSource,personDest);
System.out.println("persondest: "+personDest);
}
}
spring下的BeanUtils也是使用 copyProperties方法進行拷貝,只不過它的實現方式非常簡單,就是對兩個對象中相同名字的屬性進行簡單的get/set,僅檢查屬性的可訪問性。具體實現如下:
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);
for (PropertyDescriptor targetPd : targetPds) {
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 ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
可以看到,成員變量賦值是基於目標對象的成員列表,並且會跳過ignore的以及在源對象中不存在,所以這個方法是安全的,不會因為兩個對象之間的結構差異導致錯誤,但是必須保證同名的兩個成員變量類型相同
小結
以上簡要的分析兩種BeanUtils,因為Apache下的BeanUtils性能較差,不建議使用,可以使用 Spring的BeanUtils,或者使用其他拷貝框架,比如 cglib BeanCopier,基於javassist的Orika等,這些也是非常優秀的類庫,值得去嘗試,並且也有人去評測過這些Bean映射工具,具體分析請參閱: 常見Bean映射工具分析評測及Orika介紹
參考資料
談談 Java 開發中的對象拷貝
細說 Java 的深拷貝和淺拷貝:https://segmentfault.com/a/1190000010648514
Bean映射工具:https://pjmike.github.io/2018/11/03/Bean映射工具之Apache-BeanUtils-VS-Spring-BeanUtils/