很多項(xiàng)目中都使用了VO该溯、DTO、DO别惦、PO等模型設(shè)計(jì)狈茉,在每一層進(jìn)行參數(shù)傳遞之后,免不了會(huì)進(jìn)行大量的對(duì)象屬性之間的拷貝掸掸,此時(shí)我們會(huì)使用到BeanUtils這種工具類(lèi)氯庆,使用copyProperties進(jìn)行便捷的拷貝,代碼如下:
實(shí)體類(lèi)定義
package com.brianxia.reflection.instropector;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:28
*/
public class Source {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.brianxia.reflection.instropector;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:28
*/
public class Target {
private String name;
private long age;
public long getAge() {
return age;
}
public void setAge(long age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Target{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
引入相應(yīng)的依賴(lài)
這里我們選擇使用Spring和commons提供的BeanUtil
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
</dependencies>
方法調(diào)用測(cè)試
Source source = new Source();
source.setName("張三");
source.setAge(18);
Target target = new Target();
//Spring
BeanUtils.copyProperties(source,target);
//commons
BeanUtils.copyProperties(target,source);
源碼剖析
-
Spring實(shí)現(xiàn)
image.png
這是Spring的方法定義扰付,這里有四個(gè)參數(shù)堤撵,Spring對(duì)于復(fù)制屬性做了一些額外的處理:
source – 源bean
target – 目標(biāo)bean
editable – 可以限制只拷貝父類(lèi)或者接口中定義的屬性
ignoreProperties – 可變參數(shù)列表,排除某些特定的屬性
我們來(lái)看下Spring是如何實(shí)現(xiàn)的:
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;
}
如果傳遞了editable參數(shù)羽莺,那么就以editable的Class為準(zhǔn)实昨,獲取屬性。
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
通過(guò)內(nèi)省盐固,獲取屬性的信息荒给。內(nèi)省在后文中詳細(xì)描述。我們?cè)敿?xì)來(lái)看getPropertyDescriptors這個(gè)方法:
這里使用了一種
CachedIntrospectionResults
用來(lái)緩存曾經(jīng)使用過(guò)的內(nèi)省之后的數(shù)據(jù)刁卜,否則每次進(jìn)行copy都需要重新獲取屬性信息志电,性能太低,所以使用進(jìn)行了緩存優(yōu)化蛔趴。CachedIntrospectionResults
定義了兩種ConcurrentHashMap
存放屬性信息:Spring會(huì)先從
strongClassCache
中獲取挑辆,獲取不到再去softClassCache
中獲取,如果都沒(méi)有獲取到孝情,則進(jìn)行創(chuàng)建鱼蝉。創(chuàng)建是在CachedIntrospectionResults
的構(gòu)造方法中,其實(shí)創(chuàng)建的過(guò)程就是將目標(biāo)類(lèi)的所有屬性的PropertyDescriptor
進(jìn)行了緩存箫荡,注意: 如果有父類(lèi)的話(huà)蚀乔,父類(lèi)的屬性也會(huì)緩存起來(lái)。然后會(huì)將class作為key菲茬,將創(chuàng)建的CachedIntrospectionResults
作為value吉挣,默認(rèn)緩存到strongClassCache
屬性中(作為強(qiáng)引用)。源類(lèi)也一樣將PropertyDescriptor緩存到CachedIntrospectionResults
中婉弹。因此大大提升了性能睬魂。
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
return results;
}
results = softClassCache.get(beanClass);
if (results != null) {
return results;
}
results = new CachedIntrospectionResults(beanClass);
ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
isClassLoaderAccepted(beanClass.getClassLoader())) {
classCacheToUse = strongClassCache;
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
}
classCacheToUse = softClassCache;
}
CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
return (existing != null ? existing : results);
}
這里有一個(gè)方法需要注意:ClassUtils.isCacheSafe
,這個(gè)方法會(huì)檢查給定的beanClass是否由給定的classloader或者此classloader的祖先加載的(雙親委派的原理)镀赌。
所以第一次加載同一個(gè)類(lèi)的屬性會(huì)比較慢氯哮,后續(xù)使用緩存就不用重復(fù)加載了。
回到拷貝代碼部分商佛,接下來(lái)就是比較常規(guī)的使用屬性信息獲取Read和Write方法喉钢,其實(shí)也就是獲取setter和getter方法姆打,使用反射進(jìn)行調(diào)用:
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);
}
}
}
}
}
這里Spring對(duì)于非public的方法進(jìn)行了setAccessible
處理,使之有處理權(quán)限肠虽。但是Spring比較大的問(wèn)題是裝箱類(lèi)型Long
和Integer
等互相轉(zhuǎn)換無(wú)法做到:
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())
這個(gè)方法會(huì)判斷set和get的對(duì)象是否有繼承關(guān)系幔戏,如果沒(méi)有繼承關(guān)系,就直接返回税课。而方法內(nèi)部只是簡(jiǎn)簡(jiǎn)單單處理了基本數(shù)據(jù)類(lèi)型和裝箱類(lèi)型的關(guān)系闲延,并未對(duì)數(shù)據(jù)進(jìn)行特殊的處理。
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
Assert.notNull(lhsType, "Left-hand side type must not be null");
Assert.notNull(rhsType, "Right-hand side type must not be null");
if (lhsType.isAssignableFrom(rhsType)) {
return true;
} else {
Class resolvedWrapper;
if (lhsType.isPrimitive()) {
resolvedWrapper = (Class)primitiveWrapperTypeMap.get(rhsType);
return lhsType == resolvedWrapper;
} else {
resolvedWrapper = (Class)primitiveTypeToWrapperMap.get(rhsType);
return resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper);
}
}
}
整體來(lái)說(shuō)韩玩,Spring對(duì)于copyProperties的實(shí)現(xiàn)過(guò)于簡(jiǎn)單垒玲,僅僅增加了ignore忽略和editable限制父類(lèi)拷貝等基礎(chǔ)功能,但未對(duì)復(fù)雜的數(shù)據(jù)類(lèi)型轉(zhuǎn)換做出特殊處理找颓。接下來(lái)我們來(lái)看commons的實(shí)現(xiàn)合愈。
-
commons實(shí)現(xiàn)
從整體結(jié)構(gòu)上來(lái)說(shuō),commons的實(shí)現(xiàn)做了很多的特殊處理提升性能击狮。
image.png
它將數(shù)據(jù)類(lèi)型分成了三種佛析,DynaBean(姑且稱(chēng)之為萬(wàn)能Bean,自行查詢(xún)下資料)帘不、Map、JavaBean杨箭。
DynaBean使用較少寞焙,我們先說(shuō)Map的實(shí)現(xiàn):
@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());
}
}
直接從Map中遍歷Entry,然后將Entry的內(nèi)容寫(xiě)入到目標(biāo)對(duì)象中互婿。而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類(lèi)似捣郊,內(nèi)部維護(hù)了兩個(gè)WeakFastHashMap
緩存屬性信息,WeakFastHashMap
的設(shè)計(jì)很巧妙慈参,借鑒了CopyOnWrite的思想呛牲,查詢(xún)數(shù)據(jù)時(shí)無(wú)需加鎖,它會(huì)選擇clone一份新的數(shù)據(jù)進(jìn)行修改驮配,在clone出來(lái)的數(shù)據(jù)上進(jìn)行修改娘扩,然后再替換原來(lái)的數(shù)據(jù)。比如如下代碼:
@Override
public V get(final Object key) {
if (fast) {
return (map.get(key));
} else {
synchronized (map) {
return (map.get(key));
}
}
}
@Override
public void putAll(final Map<? extends K, ? extends V> in) {
if (fast) {
synchronized (this) {
final Map<K, V> temp = cloneMap(map);
temp.putAll(in);
map = temp;
}
} else {
synchronized (map) {
map.putAll(in);
}
}
}
commons對(duì)于數(shù)據(jù)類(lèi)型轉(zhuǎn)換壮锻,有專(zhuān)門(mén)的函數(shù)convertForCopy
來(lái)進(jìn)行處理琐旁。
protected Object convert(final Object value, final Class<?> type) {
final Converter converter = getConvertUtils().lookup(type);
if (converter != null) {
log.trace(" USING CONVERTER " + converter);
return converter.convert(type, value);
} else {
return value;
}
}
首先根據(jù)數(shù)據(jù)類(lèi)型找到對(duì)應(yīng)的converter,比如IntegerConverter[UseDefault=true, UseLocaleFormat=false]
猜绣,使用裝飾者模式進(jìn)行增強(qiáng)灰殴。根據(jù)具體轉(zhuǎn)換出的類(lèi)型,使用Number進(jìn)行處理掰邢。
@Override
protected <T> T convertToType(final Class<T> targetType, final Object value) throws Throwable {
final Class<?> sourceType = value.getClass();
// Handle Number
if (value instanceof Number) {
return toNumber(sourceType, targetType, (Number)value);
}
// Handle Boolean
if (value instanceof Boolean) {
return toNumber(sourceType, targetType, ((Boolean)value).booleanValue() ? ONE : ZERO);
}
// Handle Date --> Long
if (value instanceof Date && Long.class.equals(targetType)) {
return targetType.cast(new Long(((Date)value).getTime()));
}
// Handle Calendar --> Long
if (value instanceof Calendar && Long.class.equals(targetType)) {
return targetType.cast(new Long(((Calendar)value).getTime().getTime()));
}
// Convert all other types to String & handle
final String stringValue = value.toString().trim();
if (stringValue.length() == 0) {
return handleMissing(targetType);
}
// Convert/Parse a String
Number number = null;
if (useLocaleFormat) {
final NumberFormat format = getFormat();
number = parse(sourceType, targetType, stringValue, format);
} else {
if (log().isDebugEnabled()) {
log().debug(" No NumberFormat, using default conversion");
}
number = toNumber(sourceType, targetType, stringValue);
}
// Ensure the correct number type is returned
return toNumber(sourceType, targetType, number);
}
比如此例中Long
轉(zhuǎn)換成Integer
類(lèi)型牺陶,只需要調(diào)用toNumber
即可伟阔。針對(duì)Long
型進(jìn)行特殊的處理。
// Long
if (targetType.equals(Long.class)) {
return targetType.cast(new Long(value.longValue()));
}
總結(jié):commons的實(shí)現(xiàn)要比Spring功能更加強(qiáng)大掰伸,不僅使用了具備COW技術(shù)的緩存大大增強(qiáng)并發(fā)讀取能力皱炉,同時(shí)對(duì)數(shù)據(jù)轉(zhuǎn)換做了嚴(yán)格的處理。
內(nèi)省
最后我們來(lái)說(shuō)一下Java中的內(nèi)省(Introspector)機(jī)制碱工。Introspector與反射類(lèi)似娃承,主要是對(duì)Java Bean屬性、方法等的一種處理方法怕篷。
PropertyDescriptor類(lèi):
PropertyDescriptor類(lèi)表示JavaBean類(lèi)通過(guò)存儲(chǔ)器導(dǎo)出一個(gè)屬性历筝。主要方法:
1. getPropertyType(),獲得屬性的Class對(duì)象;
2. getReadMethod()廊谓,獲得用于讀取屬性值的方法梳猪;getWriteMethod(),獲得用于寫(xiě)入屬性值的方法;
3. hashCode()蒸痹,獲取對(duì)象的哈希值;
4. setReadMethod(Method readMethod)春弥,設(shè)置用于讀取屬性值的方法;
5. setWriteMethod(Method writeMethod),設(shè)置用于寫(xiě)入屬性值的方法叠荠。Introspector類(lèi):
將JavaBean中的屬性封裝起來(lái)進(jìn)行操作匿沛。在程序把一個(gè)類(lèi)當(dāng)做JavaBean來(lái)看,就是調(diào)用Introspector.getBeanInfo()方法榛鼎,得到的BeanInfo對(duì)象封裝了把這個(gè)類(lèi)當(dāng)做JavaBean看的結(jié)果信息逃呼,即屬性的信息。
getPropertyDescriptors()者娱,獲得屬性的描述抡笼,可以采用遍歷BeanInfo的方法,來(lái)查找黄鳍、設(shè)置類(lèi)的屬性推姻。
手寫(xiě)一個(gè)copyProperties
package com.brianxia.reflection.instropector;
import org.springframework.beans.BeanUtils;
import javax.xml.ws.spi.Invoker;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:22
*/
public class InstropectorDemo {
public static Map<Class,Map<String,PropertyDescriptor>> sourcePd = new ConcurrentHashMap<>();
public synchronized static void createPd(Object source) throws IntrospectionException {
Class clazz = source.getClass();
if(!sourcePd.containsKey(clazz)){
sourcePd.put(clazz,new HashMap<>());
}
Map putData = sourcePd.get(clazz);
//獲取BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(source.getClass());
//獲取屬性描述信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
putData.put(propertyDescriptor.getName(),propertyDescriptor);
}
}
public static PropertyDescriptor getPd(String name,Class clazz) {
if(!sourcePd.containsKey(clazz)){
return null;
}
return sourcePd.get(clazz).get(name);
}
/**
*
* @param source the source bean
* @param target the target bean
*/
public static void copyProperties(Object source,Object target) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
//獲取BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(target.getClass());
//獲取屬性描述信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
Class<?> aClass = source.getClass();
//創(chuàng)建source的描述信息map
createPd(source);
//遍歷屬性描述信息,進(jìn)行copy
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String name = propertyDescriptor.getName();
PropertyDescriptor sourcePd = getPd(name,aClass);
//如果source沒(méi)有對(duì)應(yīng)屬性框沟,直接continue
if(sourcePd == null){
continue;
}
//獲取getter和setter方法
Method writeMethod = propertyDescriptor.getWriteMethod();
Method readMethod = sourcePd.getReadMethod();
//授予權(quán)限 private也可以訪問(wèn)
if(writeMethod != null && readMethod != null){
if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())){
writeMethod.setAccessible(true);
}
if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())){
readMethod.setAccessible(true);
}
//復(fù)制屬性
Object invoke = readMethod.invoke(source);
writeMethod.invoke(target,invoke);
}
}
}
public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
Source source = new Source();
source.setName("張三");
source.setAge(18L);
Target target = new Target();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
//target.setAge(source.getAge());
// target.setName(source.getName());
copyProperties(source,target);
//BeanUtils.copyProperties(source,target);
//org.apache.commons.beanutils.BeanUtils.copyProperties(target,source);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(target);
}
}
此案例中類(lèi)型轉(zhuǎn)換并未特別處理藏古,大家可以根據(jù)commons的實(shí)現(xiàn)自行處理簡(jiǎn)單的轉(zhuǎn)換。