mirror of
https://github.com/hs-web/hsweb-framework.git
synced 2026-09-03 06:35:25 +08:00
perf(bean): 优化FastBeanCopier复制性能
This commit is contained in:
@@ -63,6 +63,10 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
static final class AccessorCopier implements Copier {
|
||||
private final NamedPropertyTransfer[] transfers;
|
||||
private final ExtensionTransfer[] extensionTransfers;
|
||||
private final Map<String, NamedPropertyTransfer> sourceMapTransfers;
|
||||
private final boolean sourceIsMap;
|
||||
private final PropertyTransfer[] fastAccessorTransfers;
|
||||
private final boolean fastAccessorMode;
|
||||
|
||||
AccessorCopier(Class<?> source, Class<?> target, PropertyAccessor propertyAccessor) {
|
||||
Map<String, FastBeanCopierPropertySupport.ClassProperty> sourceProperties = null;
|
||||
@@ -90,6 +94,8 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
|
||||
List<NamedPropertyTransfer> mappings = new ArrayList<>(sourceProperties.size());
|
||||
List<ExtensionTransfer> extensionMappings = new ArrayList<>();
|
||||
List<PropertyTransfer> fastTransfers = new ArrayList<>(sourceProperties.size());
|
||||
boolean accessorOnly = !sourceIsMap && !targetIsMap && !targetIsExtendable;
|
||||
for (FastBeanCopierPropertySupport.ClassProperty sourceProperty : sourceProperties.values()) {
|
||||
FastBeanCopierPropertySupport.ClassProperty targetProperty = targetProperties.get(sourceProperty.getName());
|
||||
if (targetProperty == null) {
|
||||
@@ -100,15 +106,40 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
}
|
||||
NamedPropertyTransfer transfer = NamedPropertyTransfer.of(target, sourceProperty, targetProperty, propertyAccessor);
|
||||
mappings.add(transfer);
|
||||
if (accessorOnly && transfer instanceof AccessorPropertyTransfer) {
|
||||
fastTransfers.add(((AccessorPropertyTransfer) transfer).transfer);
|
||||
} else {
|
||||
accessorOnly = false;
|
||||
}
|
||||
}
|
||||
this.transfers = mappings.toArray(new NamedPropertyTransfer[0]);
|
||||
this.extensionTransfers = extensionMappings.toArray(new ExtensionTransfer[0]);
|
||||
this.sourceIsMap = sourceIsMap;
|
||||
this.fastAccessorMode = accessorOnly && !fastTransfers.isEmpty() && fastTransfers.size() == mappings.size();
|
||||
this.fastAccessorTransfers = fastAccessorMode
|
||||
? fastTransfers.toArray(new PropertyTransfer[0])
|
||||
: null;
|
||||
if (sourceIsMap) {
|
||||
Map<String, NamedPropertyTransfer> transferIndex = new HashMap<>(mappings.size());
|
||||
for (NamedPropertyTransfer transfer : mappings) {
|
||||
transferIndex.put(transfer.name, transfer);
|
||||
}
|
||||
this.sourceMapTransfers = transferIndex;
|
||||
} else {
|
||||
this.sourceMapTransfers = Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(Object source, Object target, Set<String> ignore, Converter converter) {
|
||||
try {
|
||||
if (ignore.isEmpty()) {
|
||||
if (sourceIsMap) {
|
||||
if (ignore.isEmpty()) {
|
||||
copyMapWithoutIgnore((Map<?, ?>) source, target, converter);
|
||||
} else {
|
||||
copyMapWithIgnore((Map<?, ?>) source, target, ignore, converter);
|
||||
}
|
||||
} else if (ignore.isEmpty()) {
|
||||
copyWithoutIgnore(source, target, converter);
|
||||
} else {
|
||||
copyWithIgnore(source, target, ignore, converter);
|
||||
@@ -121,6 +152,12 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
}
|
||||
|
||||
private void copyWithoutIgnore(Object source, Object target, Converter converter) throws Throwable {
|
||||
if (fastAccessorMode) {
|
||||
for (PropertyTransfer transfer : fastAccessorTransfers) {
|
||||
transfer.transfer(source, target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (NamedPropertyTransfer transfer : transfers) {
|
||||
transfer.transfer(source, target, converter);
|
||||
}
|
||||
@@ -143,6 +180,28 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyMapWithoutIgnore(Map<?, ?> source, Object target, Converter converter) throws Throwable {
|
||||
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||
NamedPropertyTransfer transfer = sourceMapTransfers.get(String.valueOf(entry.getKey()));
|
||||
if (transfer != null) {
|
||||
transfer.transferValue(entry.getValue(), target, converter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyMapWithIgnore(Map<?, ?> source, Object target, Set<String> ignore, Converter converter) throws Throwable {
|
||||
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||
String name = String.valueOf(entry.getKey());
|
||||
if (ignore.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
NamedPropertyTransfer transfer = sourceMapTransfers.get(name);
|
||||
if (transfer != null) {
|
||||
transfer.transferValue(entry.getValue(), target, converter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract static class NamedPropertyTransfer {
|
||||
@@ -160,6 +219,10 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
|
||||
abstract void transfer(Object source, Object target, Converter converter) throws Throwable;
|
||||
|
||||
void transferValue(Object value, Object target, Converter converter) throws Throwable {
|
||||
throw new UnsupportedOperationException("transferValue is only supported for map source transfers");
|
||||
}
|
||||
|
||||
static NamedPropertyTransfer of(Class<?> target,
|
||||
FastBeanCopierPropertySupport.ClassProperty sourceProperty,
|
||||
FastBeanCopierPropertySupport.ClassProperty targetProperty,
|
||||
@@ -170,6 +233,29 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
boolean targetPrimitive = targetProperty.isPrimitive();
|
||||
Class<?>[] genericTypes = resolveGenericTypes(target, targetProperty);
|
||||
|
||||
if (Map.class.isAssignableFrom(sourceProperty.getBeanType())) {
|
||||
ValueWriter writer = createWriter(targetProperty, propertyAccessor);
|
||||
if (targetType == Object.class) {
|
||||
return new MapDirectPropertyTransfer(sourceProperty.getName(), writer, targetPrimitive);
|
||||
}
|
||||
if (requiresGenericConversion(targetType, genericTypes)) {
|
||||
return new MapConvertingPropertyTransfer(sourceProperty.getName(),
|
||||
writer,
|
||||
targetPrimitive,
|
||||
targetType,
|
||||
genericTypes,
|
||||
isNumberType(targetType),
|
||||
true);
|
||||
}
|
||||
return new MapConvertingPropertyTransfer(sourceProperty.getName(),
|
||||
writer,
|
||||
targetPrimitive,
|
||||
targetType,
|
||||
genericTypes,
|
||||
isNumberType(targetType),
|
||||
true);
|
||||
}
|
||||
|
||||
PropertyTransfer accessorTransfer = createAccessorTransfer(sourceProperty,
|
||||
targetProperty,
|
||||
propertyAccessor,
|
||||
@@ -394,6 +480,93 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
}
|
||||
}
|
||||
|
||||
static final class MapDirectPropertyTransfer extends NamedPropertyTransfer {
|
||||
private final ValueWriter writer;
|
||||
private final boolean targetPrimitive;
|
||||
|
||||
MapDirectPropertyTransfer(String name, ValueWriter writer, boolean targetPrimitive) {
|
||||
super(name);
|
||||
this.writer = writer;
|
||||
this.targetPrimitive = targetPrimitive;
|
||||
}
|
||||
|
||||
@Override
|
||||
void transfer(Object source, Object target, Converter converter) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
void transferValue(Object value, Object target, Converter converter) throws Throwable {
|
||||
if (value == null && !targetPrimitive) {
|
||||
return;
|
||||
}
|
||||
writer.write(target, value);
|
||||
}
|
||||
}
|
||||
|
||||
static final class MapConvertingPropertyTransfer extends NamedPropertyTransfer {
|
||||
private final ValueWriter writer;
|
||||
private final boolean targetPrimitive;
|
||||
private final Class<?> targetType;
|
||||
private final Class<?>[] genericTypes;
|
||||
private final boolean unwrapEnumDictNumber;
|
||||
private final boolean allowDirectAssignable;
|
||||
private final boolean beanLikeTarget;
|
||||
|
||||
MapConvertingPropertyTransfer(String name,
|
||||
ValueWriter writer,
|
||||
boolean targetPrimitive,
|
||||
Class<?> targetType,
|
||||
Class<?>[] genericTypes,
|
||||
boolean unwrapEnumDictNumber,
|
||||
boolean allowDirectAssignable) {
|
||||
super(name);
|
||||
this.writer = writer;
|
||||
this.targetPrimitive = targetPrimitive;
|
||||
this.targetType = targetType;
|
||||
this.genericTypes = genericTypes;
|
||||
this.unwrapEnumDictNumber = unwrapEnumDictNumber;
|
||||
this.allowDirectAssignable = allowDirectAssignable;
|
||||
this.beanLikeTarget = isBeanLikeTarget(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
void transfer(Object source, Object target, Converter converter) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
void transferValue(Object value, Object target, Converter converter) throws Throwable {
|
||||
if (value == null && !targetPrimitive) {
|
||||
return;
|
||||
}
|
||||
if (unwrapEnumDictNumber && value instanceof EnumDict) {
|
||||
Object enumValue = ((EnumDict<?>) value).getValue();
|
||||
if (enumValue != null) {
|
||||
value = enumValue;
|
||||
}
|
||||
}
|
||||
if (allowDirectAssignable && isDirectAssignable(targetType, value)) {
|
||||
writer.write(target, value);
|
||||
return;
|
||||
}
|
||||
if (beanLikeTarget && value instanceof Map) {
|
||||
Object nested = FastBeanCopierSupport.copy(value,
|
||||
FastBeanCopierSupport.getBeanFactory().newInstance(targetType),
|
||||
converter,
|
||||
Collections.emptySet());
|
||||
writer.write(target, nested);
|
||||
return;
|
||||
}
|
||||
Object converted = converter.convert(value, (Class) targetType, genericTypes);
|
||||
if (converted == null && !targetPrimitive) {
|
||||
return;
|
||||
}
|
||||
writer.write(target, converted);
|
||||
}
|
||||
}
|
||||
|
||||
static final class CloneablePropertyTransfer extends ReaderWriterPropertyTransfer {
|
||||
private final Class<?> targetType;
|
||||
private final Class<?>[] genericTypes;
|
||||
@@ -438,6 +611,7 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
private final Class<?>[] genericTypes;
|
||||
private final boolean unwrapEnumDictNumber;
|
||||
private final boolean allowDirectAssignable;
|
||||
private final boolean beanLikeTarget;
|
||||
|
||||
ConvertingPropertyTransfer(String name,
|
||||
ValueReader reader,
|
||||
@@ -454,6 +628,7 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
this.genericTypes = genericTypes;
|
||||
this.unwrapEnumDictNumber = unwrapEnumDictNumber;
|
||||
this.allowDirectAssignable = allowDirectAssignable;
|
||||
this.beanLikeTarget = isBeanLikeTarget(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -473,6 +648,14 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
writer.write(target, value);
|
||||
return;
|
||||
}
|
||||
if (beanLikeTarget && value instanceof Map) {
|
||||
Object nested = FastBeanCopierSupport.copy(value,
|
||||
FastBeanCopierSupport.getBeanFactory().newInstance(targetType),
|
||||
converter,
|
||||
Collections.emptySet());
|
||||
writer.write(target, nested);
|
||||
return;
|
||||
}
|
||||
Object converted = converter.convert(value, (Class) targetType, genericTypes);
|
||||
if (converted == null && !targetPrimitive) {
|
||||
return;
|
||||
@@ -544,6 +727,22 @@ abstract class AccessorFastBeanCopierBackend implements FastBeanCopierBackend {
|
||||
&& targetType != char.class;
|
||||
}
|
||||
|
||||
private static boolean isBeanLikeTarget(Class<?> targetType) {
|
||||
return targetType != Object.class
|
||||
&& targetType != String.class
|
||||
&& targetType != CharSequence.class
|
||||
&& targetType != Date.class
|
||||
&& targetType != Boolean.class
|
||||
&& targetType != Character.class
|
||||
&& targetType != boolean.class
|
||||
&& targetType != char.class
|
||||
&& !targetType.isEnum()
|
||||
&& !targetType.isArray()
|
||||
&& !Collection.class.isAssignableFrom(targetType)
|
||||
&& !Map.class.isAssignableFrom(targetType)
|
||||
&& !isNumberType(targetType);
|
||||
}
|
||||
|
||||
private static Object tryClone(Object value) {
|
||||
try {
|
||||
if (value.getClass().isArray()) {
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
package org.hswebframework.web.bean;
|
||||
|
||||
import org.hswebframework.ezorm.core.Extendable;
|
||||
import org.hswebframework.web.bean.accessor.AsmBeanAccessor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
final class AsmAccessorFastBeanCopierBackend extends AccessorFastBeanCopierBackend {
|
||||
private final AsmBeanAccessor asmBeanAccessor;
|
||||
|
||||
AsmAccessorFastBeanCopierBackend() {
|
||||
super(new AsmBeanAccessor());
|
||||
this(new AsmBeanAccessor());
|
||||
}
|
||||
|
||||
private AsmAccessorFastBeanCopierBackend(AsmBeanAccessor accessor) {
|
||||
super(accessor);
|
||||
this.asmBeanAccessor = accessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Copier createCopier(Class<?> source, Class<?> target) {
|
||||
if (!Map.class.isAssignableFrom(source)
|
||||
&& !Map.class.isAssignableFrom(target)
|
||||
&& !Extendable.class.isAssignableFrom(source)
|
||||
&& !Extendable.class.isAssignableFrom(target)) {
|
||||
Copier copier = asmBeanAccessor.createDirectCopier(source, target);
|
||||
if (copier != null) {
|
||||
return copier;
|
||||
}
|
||||
}
|
||||
return super.createCopier(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,26 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Slf4j
|
||||
final class FastBeanCopierConverterSupport implements Converter {
|
||||
private static final ConvertUtilsBean CONVERT_UTILS = BeanUtilsBean.getInstance().getConvertUtils();
|
||||
private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPERS = new HashMap<>();
|
||||
private static final org.apache.commons.beanutils.Converter NO_CONVERTER = new org.apache.commons.beanutils.Converter() {
|
||||
@Override
|
||||
public <T> T convert(Class<T> type, Object value) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
private static final Map<Class<?>, org.apache.commons.beanutils.Converter> APACHE_CONVERTER_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<Class<?>, Map<String, Object>> ENUM_LOOKUP_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
PRIMITIVE_WRAPPERS.put(byte.class, Byte.class);
|
||||
PRIMITIVE_WRAPPERS.put(short.class, Short.class);
|
||||
PRIMITIVE_WRAPPERS.put(int.class, Integer.class);
|
||||
PRIMITIVE_WRAPPERS.put(float.class, Float.class);
|
||||
PRIMITIVE_WRAPPERS.put(double.class, Double.class);
|
||||
PRIMITIVE_WRAPPERS.put(char.class, Character.class);
|
||||
PRIMITIVE_WRAPPERS.put(boolean.class, Boolean.class);
|
||||
PRIMITIVE_WRAPPERS.put(long.class, Long.class);
|
||||
}
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
@@ -51,14 +71,25 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
}
|
||||
ClassDescription target = ClassDescriptions.getDescription(targetClass);
|
||||
|
||||
if (isDirectScalarAssignable(targetClass, source)) {
|
||||
return (T) source;
|
||||
}
|
||||
|
||||
if (target.isEnumType() && source instanceof EnumDict) {
|
||||
Object val = ((EnumDict) source).getValue();
|
||||
if (targetClass.isInstance(val)) {
|
||||
return ((T) val);
|
||||
}
|
||||
Object matched = lookupEnum(targetClass, val);
|
||||
if (matched == null) {
|
||||
matched = lookupEnum(targetClass, ((EnumDict<?>) source).getText());
|
||||
}
|
||||
if (matched != null) {
|
||||
return (T) matched;
|
||||
}
|
||||
return convert(val, targetClass, genericType);
|
||||
}
|
||||
if (targetClass == String.class) {
|
||||
if (targetClass == String.class || targetClass == CharSequence.class) {
|
||||
if (source instanceof Date) {
|
||||
return (T) DateFormatter.toString(((Date) source), "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
@@ -82,23 +113,23 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
return (T) new Date(((Date) source).getTime());
|
||||
}
|
||||
}
|
||||
if (targetClass == boolean.class || targetClass == Boolean.class) {
|
||||
return (T) convertToBoolean(source);
|
||||
}
|
||||
if (targetClass == char.class || targetClass == Character.class) {
|
||||
return (T) convertToCharacter(source);
|
||||
}
|
||||
if (target.isCollectionType()) {
|
||||
Collection collection = newCollection(targetClass);
|
||||
Collection sourceCollection;
|
||||
if (source instanceof Collection) {
|
||||
sourceCollection = (Collection) source;
|
||||
} else if (source.getClass().isArray()) {
|
||||
sourceCollection = new DynamicArrayList(source);
|
||||
} else if (source instanceof Map) {
|
||||
sourceCollection = ((Map<?, ?>) source).values();
|
||||
} else if (source instanceof String) {
|
||||
sourceCollection = Arrays.asList(((String) source).split("[,]"));
|
||||
} else {
|
||||
sourceCollection = Arrays.asList(source);
|
||||
}
|
||||
Collection sourceCollection = asCollection(source);
|
||||
if (genericType != null && genericType.length > 0 && genericType[0] != Object.class) {
|
||||
Class<?> elementType = genericType[0];
|
||||
for (Object sourceObj : sourceCollection) {
|
||||
collection.add(convert(sourceObj, genericType[0], null));
|
||||
if (sourceObj == null || isDirectAssignable(elementType, sourceObj)) {
|
||||
collection.add(sourceObj);
|
||||
} else {
|
||||
collection.add(convert(sourceObj, elementType, null));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collection.addAll(sourceCollection);
|
||||
@@ -106,34 +137,13 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
return (T) collection;
|
||||
}
|
||||
if (target.isEnumType()) {
|
||||
if (target.isEnumDict()) {
|
||||
String strVal = String.valueOf(source);
|
||||
Object val = null;
|
||||
for (Object anEnum : target.getEnums()) {
|
||||
EnumDict dic = ((EnumDict) anEnum);
|
||||
Enum e = ((Enum<?>) anEnum);
|
||||
if (dic.eq(source) || e.name().equalsIgnoreCase(strVal)) {
|
||||
val = anEnum;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
Object val = lookupEnum(targetClass, source);
|
||||
if (val != null) {
|
||||
if (targetClass.isInstance(val)) {
|
||||
return ((T) val);
|
||||
return (T) val;
|
||||
}
|
||||
return convert(val, targetClass, genericType);
|
||||
}
|
||||
String strSource = String.valueOf(source);
|
||||
for (Object e : target.getEnums()) {
|
||||
Enum t = ((Enum<?>) e);
|
||||
if (t.name().equalsIgnoreCase(strSource)
|
||||
|| Objects.equals(String.valueOf(t.ordinal()), strSource)) {
|
||||
return (T) e;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("无法将:{}转为枚举:{}",
|
||||
source,
|
||||
targetClass,
|
||||
@@ -141,26 +151,24 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
return null;
|
||||
}
|
||||
if (target.isArrayType()) {
|
||||
Class<?> componentType = targetClass.getComponentType();
|
||||
List<?> val = convert(source, List.class, new Class[]{componentType});
|
||||
int size = val.size();
|
||||
|
||||
Object array = Array.newInstance(componentType, size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
Array.set(array, i, val.get(i));
|
||||
}
|
||||
return (T) array;
|
||||
return (T) convertToArray(source, targetClass.getComponentType());
|
||||
}
|
||||
if (target.isNumber()) {
|
||||
if (source instanceof Number) {
|
||||
return (T) convertNumber((Number) source, targetClass);
|
||||
}
|
||||
if (source instanceof String) {
|
||||
return (T) NumberUtils.parseNumber(String.valueOf(source), (Class) targetClass);
|
||||
return (T) convertStringToNumber((String) source, targetClass);
|
||||
}
|
||||
if (source instanceof Date) {
|
||||
source = ((Date) source).getTime();
|
||||
}
|
||||
}
|
||||
if (source instanceof Map && isBeanLikeTarget(target, targetClass)) {
|
||||
return FastBeanCopierSupport.copy(source, beanFactory.newInstance(targetClass), this);
|
||||
}
|
||||
try {
|
||||
org.apache.commons.beanutils.Converter converter = CONVERT_UTILS.lookup(targetClass);
|
||||
org.apache.commons.beanutils.Converter converter = getApacheConverter(targetClass);
|
||||
if (converter != null) {
|
||||
return converter.convert(targetClass, source);
|
||||
}
|
||||
@@ -170,10 +178,11 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
return (T) copyMap(((Map<?, ?>) source));
|
||||
}
|
||||
if (source instanceof Collection) {
|
||||
Map<Object, Object> map = new LinkedHashMap<>();
|
||||
Collection<?> sourceCollection = (Collection<?>) source;
|
||||
Map<Object, Object> map = new LinkedHashMap<>(Math.max((int) (sourceCollection.size() / 0.75F) + 1, 16));
|
||||
int i = 0;
|
||||
for (Object o : ((Collection<?>) source)) {
|
||||
if (genericType.length >= 2) {
|
||||
for (Object o : sourceCollection) {
|
||||
if (genericType != null && genericType.length >= 2) {
|
||||
map.put(convert(i++, genericType[0], FastBeanCopierSupport.EMPTY_CLASS_ARRAY),
|
||||
convert(o, genericType[1], FastBeanCopierSupport.EMPTY_CLASS_ARRAY));
|
||||
} else {
|
||||
@@ -193,6 +202,42 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<?> asCollection(Object source) {
|
||||
if (source instanceof Collection) {
|
||||
return (Collection<?>) source;
|
||||
}
|
||||
if (source.getClass().isArray()) {
|
||||
return new DynamicArrayList(source);
|
||||
}
|
||||
if (source instanceof Map) {
|
||||
return ((Map<?, ?>) source).values();
|
||||
}
|
||||
if (source instanceof String) {
|
||||
return Arrays.asList(((String) source).split("[,]"));
|
||||
}
|
||||
return Collections.singletonList(source);
|
||||
}
|
||||
|
||||
private Object convertToArray(Object source, Class<?> componentType) {
|
||||
if (source.getClass().isArray() && source.getClass().getComponentType() == componentType) {
|
||||
int length = Array.getLength(source);
|
||||
Object array = Array.newInstance(componentType, length);
|
||||
System.arraycopy(source, 0, array, 0, length);
|
||||
return array;
|
||||
}
|
||||
Collection<?> sourceCollection = asCollection(source);
|
||||
Object array = Array.newInstance(componentType, sourceCollection.size());
|
||||
int index = 0;
|
||||
for (Object value : sourceCollection) {
|
||||
Array.set(array,
|
||||
index++,
|
||||
value == null || isDirectAssignable(componentType, value)
|
||||
? value
|
||||
: convert(value, componentType, FastBeanCopierSupport.EMPTY_CLASS_ARRAY));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private Map<?, ?> copyMap(Map<?, ?> map) {
|
||||
if (map instanceof TreeMap) {
|
||||
return new TreeMap<>(map);
|
||||
@@ -207,10 +252,165 @@ final class FastBeanCopierConverterSupport implements Converter {
|
||||
}
|
||||
|
||||
private Object converterByApache(Class<?> targetClass, Object source) {
|
||||
org.apache.commons.beanutils.Converter converter = CONVERT_UTILS.lookup(targetClass);
|
||||
org.apache.commons.beanutils.Converter converter = getApacheConverter(targetClass);
|
||||
if (converter != null) {
|
||||
return converter.convert(targetClass, source);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private org.apache.commons.beanutils.Converter getApacheConverter(Class<?> targetClass) {
|
||||
org.apache.commons.beanutils.Converter converter = APACHE_CONVERTER_CACHE.computeIfAbsent(targetClass,
|
||||
type -> {
|
||||
org.apache.commons.beanutils.Converter found = CONVERT_UTILS.lookup(type);
|
||||
return found == null ? NO_CONVERTER : found;
|
||||
});
|
||||
return converter == NO_CONVERTER ? null : converter;
|
||||
}
|
||||
|
||||
private boolean isDirectScalarAssignable(Class<?> targetClass, Object source) {
|
||||
if (source instanceof Number || source instanceof Boolean || source instanceof Character || source instanceof Enum) {
|
||||
return isDirectAssignable(targetClass, source);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Object lookupEnum(Class<?> targetClass, Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (targetClass.isInstance(source)) {
|
||||
return source;
|
||||
}
|
||||
if (source instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) source;
|
||||
Object value = map.get("value");
|
||||
Object matched = lookupEnum(targetClass, value);
|
||||
return matched != null ? matched : lookupEnum(targetClass, map.get("text"));
|
||||
}
|
||||
if (source instanceof EnumDict) {
|
||||
EnumDict<?> dict = (EnumDict<?>) source;
|
||||
Object matched = lookupEnum(targetClass, dict.getValue());
|
||||
return matched != null ? matched : lookupEnum(targetClass, dict.getText());
|
||||
}
|
||||
if (source instanceof Number) {
|
||||
source = ((Number) source).intValue();
|
||||
}
|
||||
String key = normalizeEnumKey(source);
|
||||
return ENUM_LOOKUP_CACHE
|
||||
.computeIfAbsent(targetClass, this::createEnumLookup)
|
||||
.get(key);
|
||||
}
|
||||
|
||||
private Map<String, Object> createEnumLookup(Class<?> targetClass) {
|
||||
ClassDescription description = ClassDescriptions.getDescription(targetClass);
|
||||
Map<String, Object> lookup = new HashMap<>();
|
||||
for (Object obj : description.getEnums()) {
|
||||
Enum<?> value = (Enum<?>) obj;
|
||||
lookup.putIfAbsent(normalizeEnumKey(value.name()), obj);
|
||||
lookup.putIfAbsent(normalizeEnumKey(value.ordinal()), obj);
|
||||
if (obj instanceof EnumDict) {
|
||||
EnumDict<?> dict = (EnumDict<?>) obj;
|
||||
lookup.putIfAbsent(normalizeEnumKey(dict.getValue()), obj);
|
||||
lookup.putIfAbsent(normalizeEnumKey(dict.getText()), obj);
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private String normalizeEnumKey(Object value) {
|
||||
return String.valueOf(value).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private boolean isDirectAssignable(Class<?> targetClass, Object source) {
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
if (targetClass.isInstance(source)) {
|
||||
return true;
|
||||
}
|
||||
if (targetClass.isPrimitive()) {
|
||||
Class<?> wrapper = PRIMITIVE_WRAPPERS.get(targetClass);
|
||||
return wrapper != null && wrapper.isInstance(source);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isBeanLikeTarget(ClassDescription target, Class<?> targetClass) {
|
||||
return targetClass != Object.class
|
||||
&& targetClass != String.class
|
||||
&& targetClass != CharSequence.class
|
||||
&& targetClass != Date.class
|
||||
&& targetClass != Boolean.class
|
||||
&& targetClass != Character.class
|
||||
&& targetClass != boolean.class
|
||||
&& targetClass != char.class
|
||||
&& !target.isEnumType()
|
||||
&& !target.isArrayType()
|
||||
&& !target.isCollectionType()
|
||||
&& !target.isNumber()
|
||||
&& !Map.class.isAssignableFrom(targetClass);
|
||||
}
|
||||
|
||||
private Number convertNumber(Number source, Class<?> targetClass) {
|
||||
Class<?> wrapper = targetClass.isPrimitive() ? PRIMITIVE_WRAPPERS.get(targetClass) : targetClass;
|
||||
return NumberUtils.convertNumberToTargetClass(source, (Class<? extends Number>) wrapper);
|
||||
}
|
||||
|
||||
private Number convertStringToNumber(String source, Class<?> targetClass) {
|
||||
String value = source.trim();
|
||||
Class<?> wrapper = targetClass.isPrimitive() ? PRIMITIVE_WRAPPERS.get(targetClass) : targetClass;
|
||||
try {
|
||||
if (wrapper == Integer.class) {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
if (wrapper == Long.class) {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
if (wrapper == Short.class) {
|
||||
return Short.parseShort(value);
|
||||
}
|
||||
if (wrapper == Byte.class) {
|
||||
return Byte.parseByte(value);
|
||||
}
|
||||
if (wrapper == Double.class) {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
if (wrapper == Float.class) {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
} catch (NumberFormatException ignore) {
|
||||
// fallback to Spring NumberUtils for extended formats
|
||||
}
|
||||
return NumberUtils.parseNumber(value, (Class) targetClass);
|
||||
}
|
||||
|
||||
private Boolean convertToBoolean(Object source) {
|
||||
if (source instanceof Boolean) {
|
||||
return (Boolean) source;
|
||||
}
|
||||
if (source instanceof Number) {
|
||||
return ((Number) source).intValue() != 0;
|
||||
}
|
||||
String value = String.valueOf(source).trim();
|
||||
if (value.isEmpty()) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return "true".equalsIgnoreCase(value)
|
||||
|| "1".equals(value)
|
||||
|| "y".equalsIgnoreCase(value)
|
||||
|| "yes".equalsIgnoreCase(value)
|
||||
|| "on".equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
private Character convertToCharacter(Object source) {
|
||||
if (source instanceof Character) {
|
||||
return (Character) source;
|
||||
}
|
||||
if (source instanceof Number) {
|
||||
return (char) ((Number) source).intValue();
|
||||
}
|
||||
String value = String.valueOf(source);
|
||||
return value.isEmpty() ? null : value.charAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.hswebframework.web.bean.accessor;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.hswebframework.web.bean.Copier;
|
||||
import org.hswebframework.web.bean.FastBeanCopierSupport;
|
||||
import org.objectweb.asm.*;
|
||||
import org.springframework.core.ResolvableType;
|
||||
@@ -10,6 +11,9 @@ import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AsmBeanAccessor implements PropertyAccessor {
|
||||
@@ -18,6 +22,7 @@ public class AsmBeanAccessor implements PropertyAccessor {
|
||||
private static final String CONVERTER_PROPERTY_TRANSFER_INTERNAL_NAME = "org/hswebframework/web/bean/accessor/ConverterPropertyTransfer";
|
||||
private static final String PROPERTY_TRANSFER_INTERNAL_NAME = "org/hswebframework/web/bean/accessor/PropertyTransfer";
|
||||
private static final String PROPERTY_WRITER_INTERNAL_NAME = "org/hswebframework/web/bean/accessor/PropertyWriter";
|
||||
private static final String COPIER_INTERNAL_NAME = "org/hswebframework/web/bean/Copier";
|
||||
private static final String BEAN_CONVERTER_INTERNAL_NAME = "org/hswebframework/web/bean/Converter";
|
||||
private static final String TYPE_CONVERTER_INTERNAL_NAME = "org/hswebframework/web/bean/accessor/TypeConverter";
|
||||
private static final String RESOLVABLE_TYPE_INTERNAL_NAME = "org/springframework/core/ResolvableType";
|
||||
@@ -26,6 +31,38 @@ public class AsmBeanAccessor implements PropertyAccessor {
|
||||
private static final AtomicInteger COUNTER = new AtomicInteger();
|
||||
private final PropertyAccessor fallback = new ReflectionBeanAccessor();
|
||||
|
||||
public Copier createDirectCopier(Class<?> sourceClass, Class<?> targetClass) {
|
||||
if (shouldUseFallback(sourceClass, targetClass)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Map<String, PropertyDescriptor> sourceProperties = findWritableReadableProperties(sourceClass, true);
|
||||
Map<String, PropertyDescriptor> targetProperties = findWritableReadableProperties(targetClass, false);
|
||||
if (sourceProperties.isEmpty() || targetProperties.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<PropertyDescriptor, PropertyDescriptor> mappings = new LinkedHashMap<>();
|
||||
for (PropertyDescriptor sourceProperty : sourceProperties.values()) {
|
||||
PropertyDescriptor targetProperty = targetProperties.get(sourceProperty.getName());
|
||||
if (targetProperty == null) {
|
||||
continue;
|
||||
}
|
||||
if (!canUseDirectCopier(sourceProperty, targetProperty)) {
|
||||
return null;
|
||||
}
|
||||
mappings.put(sourceProperty, targetProperty);
|
||||
}
|
||||
if (mappings.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytecode = createDirectCopierCode(sourceClass, targetClass, mappings);
|
||||
Class<?> generatedClass = defineClass(bytecode);
|
||||
return (Copier) generatedClass.getDeclaredConstructor().newInstance();
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public byte[] createReaderCode(Class<?> clazz, String name) {
|
||||
PropertyDescriptor descriptor = findPropertyDescriptor(clazz, name);
|
||||
@@ -420,6 +457,67 @@ public class AsmBeanAccessor implements PropertyAccessor {
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] createDirectCopierCode(Class<?> sourceClass,
|
||||
Class<?> targetClass,
|
||||
Map<PropertyDescriptor, PropertyDescriptor> mappings) {
|
||||
String className = generateClassName(sourceClass.getSimpleName()
|
||||
+ "$To$"
|
||||
+ targetClass.getSimpleName()
|
||||
+ "$DirectCopier");
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", new String[]{COPIER_INTERNAL_NAME});
|
||||
|
||||
MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
constructor.visitCode();
|
||||
constructor.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
constructor.visitInsn(Opcodes.RETURN);
|
||||
constructor.visitMaxs(1, 1);
|
||||
constructor.visitEnd();
|
||||
|
||||
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC,
|
||||
"copy",
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Set;L" + BEAN_CONVERTER_INTERNAL_NAME + ";)V",
|
||||
null,
|
||||
null);
|
||||
mv.visitCode();
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(sourceClass));
|
||||
mv.visitVarInsn(Opcodes.ASTORE, 5);
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 2);
|
||||
mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(targetClass));
|
||||
mv.visitVarInsn(Opcodes.ASTORE, 6);
|
||||
|
||||
Label withIgnore = new Label();
|
||||
Label end = new Label();
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 3);
|
||||
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Set", "isEmpty", "()Z", true);
|
||||
mv.visitJumpInsn(Opcodes.IFEQ, withIgnore);
|
||||
for (Map.Entry<PropertyDescriptor, PropertyDescriptor> entry : mappings.entrySet()) {
|
||||
generateDirectCopyStatement(mv, sourceClass, targetClass, entry.getKey(), entry.getValue(), false);
|
||||
}
|
||||
mv.visitJumpInsn(Opcodes.GOTO, end);
|
||||
|
||||
mv.visitLabel(withIgnore);
|
||||
for (Map.Entry<PropertyDescriptor, PropertyDescriptor> entry : mappings.entrySet()) {
|
||||
String name = entry.getKey().getName();
|
||||
Label next = new Label();
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 3);
|
||||
mv.visitLdcInsn(name);
|
||||
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Set", "contains", "(Ljava/lang/Object;)Z", true);
|
||||
mv.visitJumpInsn(Opcodes.IFNE, next);
|
||||
generateDirectCopyStatement(mv, sourceClass, targetClass, entry.getKey(), entry.getValue(), true);
|
||||
mv.visitLabel(next);
|
||||
}
|
||||
mv.visitLabel(end);
|
||||
mv.visitInsn(Opcodes.RETURN);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
|
||||
cw.visitEnd();
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] createConverterTransferCode(Class<?> sourceClass,
|
||||
PropertyDescriptor sourceDescriptor,
|
||||
boolean sourcePrimitive,
|
||||
@@ -546,10 +644,123 @@ public class AsmBeanAccessor implements PropertyAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, PropertyDescriptor> findWritableReadableProperties(Class<?> clazz, boolean source) throws Exception {
|
||||
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
|
||||
Map<String, PropertyDescriptor> properties = new LinkedHashMap<>();
|
||||
for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
|
||||
if ("class".equals(property.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (source) {
|
||||
if (property.getReadMethod() == null) {
|
||||
continue;
|
||||
}
|
||||
} else if (property.getWriteMethod() == null) {
|
||||
continue;
|
||||
}
|
||||
properties.put(property.getName(), property);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private String generateClassName(String prefix) {
|
||||
return "org/hswebframework/web/bean/accessor/" + prefix + "$" + COUNTER.incrementAndGet();
|
||||
}
|
||||
|
||||
private boolean canUseDirectCopier(PropertyDescriptor sourceProperty, PropertyDescriptor targetProperty) {
|
||||
Method readMethod = sourceProperty.getReadMethod();
|
||||
Method writeMethod = targetProperty.getWriteMethod();
|
||||
if (readMethod == null || writeMethod == null) {
|
||||
return false;
|
||||
}
|
||||
if (!Modifier.isPublic(readMethod.getModifiers()) || !Modifier.isPublic(writeMethod.getModifiers())) {
|
||||
return false;
|
||||
}
|
||||
Class<?> sourceType = readMethod.getReturnType();
|
||||
Class<?> targetType = writeMethod.getParameterTypes()[0];
|
||||
return isDirectCompatible(sourceType, targetType);
|
||||
}
|
||||
|
||||
private boolean isDirectCompatible(Class<?> sourceType, Class<?> targetType) {
|
||||
if (sourceType == targetType) {
|
||||
return true;
|
||||
}
|
||||
if (sourceType.isPrimitive()) {
|
||||
return getWrapperType(sourceType) == targetType || targetType == Object.class;
|
||||
}
|
||||
if (targetType.isPrimitive()) {
|
||||
return getWrapperType(targetType) == sourceType;
|
||||
}
|
||||
return targetType.isAssignableFrom(sourceType);
|
||||
}
|
||||
|
||||
private boolean shouldClone(Class<?> sourceType, Class<?> targetType) {
|
||||
return sourceType == targetType
|
||||
&& sourceType.isArray();
|
||||
}
|
||||
|
||||
private void generateDirectCopyStatement(MethodVisitor mv,
|
||||
Class<?> sourceClass,
|
||||
Class<?> targetClass,
|
||||
PropertyDescriptor sourceDescriptor,
|
||||
PropertyDescriptor targetDescriptor,
|
||||
boolean ignoreBranch) {
|
||||
Method readMethod = sourceDescriptor.getReadMethod();
|
||||
Method writeMethod = targetDescriptor.getWriteMethod();
|
||||
Class<?> sourceType = readMethod.getReturnType();
|
||||
Class<?> targetType = writeMethod.getParameterTypes()[0];
|
||||
Label skip = new Label();
|
||||
|
||||
if (sourceType.isPrimitive()) {
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 6);
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 5);
|
||||
mv.visitMethodInsn(readMethod.getDeclaringClass().isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
|
||||
Type.getInternalName(readMethod.getDeclaringClass()),
|
||||
readMethod.getName(),
|
||||
Type.getMethodDescriptor(readMethod),
|
||||
readMethod.getDeclaringClass().isInterface());
|
||||
adaptPrimitiveValueForTarget(mv, sourceType, targetType);
|
||||
mv.visitMethodInsn(writeMethod.getDeclaringClass().isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
|
||||
Type.getInternalName(writeMethod.getDeclaringClass()),
|
||||
writeMethod.getName(),
|
||||
Type.getMethodDescriptor(writeMethod),
|
||||
writeMethod.getDeclaringClass().isInterface());
|
||||
return;
|
||||
}
|
||||
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 5);
|
||||
mv.visitMethodInsn(readMethod.getDeclaringClass().isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
|
||||
Type.getInternalName(readMethod.getDeclaringClass()),
|
||||
readMethod.getName(),
|
||||
Type.getMethodDescriptor(readMethod),
|
||||
readMethod.getDeclaringClass().isInterface());
|
||||
mv.visitVarInsn(Opcodes.ASTORE, 7);
|
||||
Label continueLabel = new Label();
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 7);
|
||||
mv.visitJumpInsn(Opcodes.IFNONNULL, continueLabel);
|
||||
mv.visitJumpInsn(Opcodes.GOTO, skip);
|
||||
mv.visitLabel(continueLabel);
|
||||
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 6);
|
||||
if (shouldClone(sourceType, targetType)) {
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 7);
|
||||
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
|
||||
Type.getInternalName(AsmBeanAccessor.class),
|
||||
"cloneValue",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;",
|
||||
false);
|
||||
} else {
|
||||
mv.visitVarInsn(Opcodes.ALOAD, 7);
|
||||
}
|
||||
adaptReferenceValueForTarget(mv, targetType);
|
||||
mv.visitMethodInsn(writeMethod.getDeclaringClass().isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
|
||||
Type.getInternalName(writeMethod.getDeclaringClass()),
|
||||
writeMethod.getName(),
|
||||
Type.getMethodDescriptor(writeMethod),
|
||||
writeMethod.getDeclaringClass().isInterface());
|
||||
mv.visitLabel(skip);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private Class<?> defineClass(byte[] bytes) {
|
||||
// 使用 MethodHandles.Lookup 来定义类,这是 Java 17 推荐的方式
|
||||
@@ -682,6 +893,25 @@ public class AsmBeanAccessor implements PropertyAccessor {
|
||||
return primitiveType;
|
||||
}
|
||||
|
||||
public static Object cloneValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (value.getClass().isArray()) {
|
||||
int length = java.lang.reflect.Array.getLength(value);
|
||||
Object clone = java.lang.reflect.Array.newInstance(value.getClass().getComponentType(), length);
|
||||
System.arraycopy(value, 0, clone, 0, length);
|
||||
return clone;
|
||||
}
|
||||
Method clone = value.getClass().getMethod("clone");
|
||||
clone.setAccessible(true);
|
||||
return clone.invoke(value);
|
||||
} catch (Throwable ignore) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private void generateTransferConvertMethod(ClassWriter cw,
|
||||
String className,
|
||||
Class<?> paramType,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# FastBeanCopier Benchmark Summary
|
||||
|
||||
> 更新时间:2026-05-28
|
||||
|
||||
## 本轮优化摘要
|
||||
|
||||
本轮围绕 `asm-accessor` 做了收口优化,重点包括:
|
||||
|
||||
- `Map -> Bean` 按 `entrySet()` 驱动,减少目标属性逐个 `Map.get`
|
||||
- nested bean-like `Map` 值直接走 `FastBeanCopierSupport.copy(...)`
|
||||
- converter 增加常用 `String -> Number` fast path
|
||||
- accessor backend 增加 direct accessor 快路径
|
||||
- `asm-accessor` 增加 **simple bean -> bean 专用 Copier 生成**
|
||||
- `asm-accessor` direct copier 收紧 clone 策略:**仅数组 clone**
|
||||
|
||||
## 覆盖场景
|
||||
|
||||
- simple bean -> bean
|
||||
- complex bean -> bean
|
||||
- bean -> map
|
||||
- bean -> extendable
|
||||
- extendable -> map
|
||||
- heterogeneous map -> bean
|
||||
- conversion-heavy map -> bean
|
||||
- collection-heavy map -> bean
|
||||
- nested-heavy map -> bean
|
||||
- nested-only map -> bean
|
||||
|
||||
## 当前结论
|
||||
|
||||
### 1. `asm-accessor` 已成为默认首选 backend
|
||||
|
||||
在以下真实场景中,`asm-accessor` 整体最优:
|
||||
|
||||
- complex bean -> bean
|
||||
- heterogeneous map -> bean
|
||||
- conversion-heavy map -> bean
|
||||
- collection-heavy map -> bean
|
||||
- nested-heavy / nested-only map -> bean
|
||||
- bean -> map
|
||||
- bean -> extendable / extendable -> map
|
||||
|
||||
### 2. simple bean -> bean 已基本打平甚至部分反超 `javassist`
|
||||
|
||||
经过 direct copier 与 clone 策略优化后:
|
||||
|
||||
- 场景对比中,`asm-accessor` 已优于 `javassist`
|
||||
- 定向 JMH 中,`asm-accessor` 与 `javassist` 已基本同一量级
|
||||
|
||||
### 3. `reflection-accessor` 仍是稳定兜底
|
||||
|
||||
当需要:
|
||||
|
||||
- 更低的冷启动生成成本
|
||||
- 更保守的运行时生成策略
|
||||
- future native-image 降级方案
|
||||
|
||||
`reflection-accessor` 是当前最稳妥的 fallback。
|
||||
|
||||
## backend 推荐策略
|
||||
|
||||
### 默认推荐
|
||||
|
||||
- **默认 backend:`asm-accessor`**
|
||||
|
||||
原因:
|
||||
|
||||
- 复杂场景整体最佳
|
||||
- simple 场景已不再明显落后
|
||||
- 更符合后续替代 `javassist` 的方向
|
||||
|
||||
### 保守 fallback
|
||||
|
||||
- **fallback backend:`reflection-accessor`**
|
||||
|
||||
适用于:
|
||||
|
||||
- ASM 生成失败
|
||||
- 跨 classloader / 可见性不满足
|
||||
- native-image 环境不适合运行时字节码生成
|
||||
|
||||
### `javassist` 的当前定位
|
||||
|
||||
`javassist` 仍可保留作为兼容 backend,但从当前结果看:
|
||||
|
||||
- 已不再是默认最优解
|
||||
- 可逐步退化为兼容实现
|
||||
|
||||
## 代表性结果
|
||||
|
||||
### 场景对比(FastBeanCopierSupportTest)
|
||||
|
||||
#### simple-bean -> bean
|
||||
|
||||
- javassist: `2.84ms`
|
||||
- reflection-accessor: `29.76ms`
|
||||
- asm-accessor: `1.87ms`
|
||||
|
||||
#### complex-bean -> bean
|
||||
|
||||
- javassist: `266.68ms`
|
||||
- reflection-accessor: `251.38ms`
|
||||
- asm-accessor: `198.02ms`
|
||||
|
||||
#### conversion-heavy-map -> bean
|
||||
|
||||
- javassist: `466.38ms`
|
||||
- reflection-accessor: `265.01ms`
|
||||
- asm-accessor: `261.07ms`
|
||||
|
||||
#### collection-heavy-map -> bean
|
||||
|
||||
- javassist: `93.13ms`
|
||||
- reflection-accessor: `36.84ms`
|
||||
- asm-accessor: `25.37ms`
|
||||
|
||||
#### nested-heavy-map -> bean
|
||||
|
||||
- javassist: `101.66ms`
|
||||
- reflection-accessor: `32.50ms`
|
||||
- asm-accessor: `39.62ms`
|
||||
|
||||
> 注:nested-heavy 在场景对比里受运行波动影响较大;结合多轮 JMH 与多次场景跑数,`asm-accessor` 与 `reflection-accessor` 同属第一梯队,`asm-accessor` 在整体真实场景仍是首选。
|
||||
|
||||
### 定向 JMH(simple / complex)
|
||||
|
||||
#### copySimpleBean
|
||||
|
||||
- javassist: `0.075 us/op`
|
||||
- reflection-accessor: `0.379 us/op`
|
||||
- asm-accessor: `0.080 us/op`
|
||||
|
||||
#### copyComplexBean
|
||||
|
||||
- javassist: `13.269 us/op` *(本轮波动偏大,仅供趋势参考)*
|
||||
- reflection-accessor: `8.552 us/op`
|
||||
- asm-accessor: `7.095 us/op`
|
||||
|
||||
## native-image 建议
|
||||
|
||||
当前建议策略:
|
||||
|
||||
1. 常规 JVM 环境:
|
||||
- 默认 `asm-accessor`
|
||||
2. native-image / 受限运行时:
|
||||
- 自动降级 `reflection-accessor`
|
||||
3. 若需彻底去除运行时字节码生成:
|
||||
- 后续可增加显式配置开关
|
||||
|
||||
## 剩余优化空间
|
||||
|
||||
当前优先级已下降,后续更适合做收尾而不是大改:
|
||||
|
||||
1. native-image 环境自动探测与 backend 降级
|
||||
2. backend 选择策略外置配置
|
||||
3. conversion plan cache(若继续追求极限 map -> bean)
|
||||
4. 清理 / 退役 `javassist` 的迁移路径评估
|
||||
|
||||
## 结果来源
|
||||
|
||||
- `FastBeanCopierSupportTest`
|
||||
- `FastBeanCopierJmhBenchmark`
|
||||
- `hsweb-core/target/jmh-results/fast-bean-copier.json`
|
||||
@@ -54,6 +54,26 @@ public class FastBeanCopierJmhBenchmark {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.heterogeneousMap, new Target()));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void copyConversionHeavyMapToBean(BenchmarkState state, Blackhole blackhole) {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.conversionHeavyMap, new Target()));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void copyCollectionHeavyMapToBean(BenchmarkState state, Blackhole blackhole) {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.collectionHeavyMap, new Target()));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void copyNestedHeavyMapToBean(BenchmarkState state, Blackhole blackhole) {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.nestedHeavyMap, new Target()));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void copyNestedHeavyMapToBeanOnly(BenchmarkState state, Blackhole blackhole) {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.nestedOnlyMap, new NestedOnlyTarget()));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void copyBeanToExtendable(BenchmarkState state, Blackhole blackhole) {
|
||||
blackhole.consume(FastBeanCopierSupport.copy(state.complexSource, new BenchmarkExtendableEntity()));
|
||||
@@ -73,6 +93,10 @@ public class FastBeanCopierJmhBenchmark {
|
||||
private SimpleSource simpleSource;
|
||||
private Source complexSource;
|
||||
private Map<String, Object> heterogeneousMap;
|
||||
private Map<String, Object> conversionHeavyMap;
|
||||
private Map<String, Object> collectionHeavyMap;
|
||||
private Map<String, Object> nestedHeavyMap;
|
||||
private Map<String, Object> nestedOnlyMap;
|
||||
private BenchmarkExtendableEntity extendableSource;
|
||||
|
||||
@Setup(Level.Trial)
|
||||
@@ -84,6 +108,10 @@ public class FastBeanCopierJmhBenchmark {
|
||||
simpleSource = createSimpleSource();
|
||||
complexSource = createComplexSource();
|
||||
heterogeneousMap = createHeterogeneousMap();
|
||||
conversionHeavyMap = createConversionHeavyMap();
|
||||
collectionHeavyMap = createCollectionHeavyMap();
|
||||
nestedHeavyMap = createNestedHeavyMap();
|
||||
nestedOnlyMap = createNestedOnlyMap();
|
||||
extendableSource = FastBeanCopierSupport.copy(complexSource, new BenchmarkExtendableEntity());
|
||||
|
||||
warmupCache();
|
||||
@@ -99,6 +127,10 @@ public class FastBeanCopierJmhBenchmark {
|
||||
FastBeanCopierSupport.copy(complexSource, new Target());
|
||||
FastBeanCopierSupport.copy(complexSource, new HashMap<>());
|
||||
FastBeanCopierSupport.copy(heterogeneousMap, new Target());
|
||||
FastBeanCopierSupport.copy(conversionHeavyMap, new Target());
|
||||
FastBeanCopierSupport.copy(collectionHeavyMap, new Target());
|
||||
FastBeanCopierSupport.copy(nestedHeavyMap, new Target());
|
||||
FastBeanCopierSupport.copy(nestedOnlyMap, new NestedOnlyTarget());
|
||||
FastBeanCopierSupport.copy(complexSource, new BenchmarkExtendableEntity());
|
||||
FastBeanCopierSupport.copy(extendableSource, new HashMap<>());
|
||||
}
|
||||
@@ -217,6 +249,147 @@ public class FastBeanCopierJmhBenchmark {
|
||||
map.put("colors", Arrays.asList("BLUE", "RED"));
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createConversionHeavyMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", 123456);
|
||||
map.put("ids", "alpha,beta,gamma");
|
||||
map.put("boy", "true");
|
||||
map.put("boy2", 1);
|
||||
map.put("boy3", false);
|
||||
map.put("age", "2048");
|
||||
map.put("age2", 4096L);
|
||||
map.put("age3", 8192);
|
||||
map.put("deleteTime", "2024-05-07 10:20:30");
|
||||
map.put("createTime", new Date(1715043600000L));
|
||||
map.put("updateTime", "2024-05-08 11:30:40");
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", 1001);
|
||||
put("age", "28");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-2");
|
||||
put("age", "29");
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-3");
|
||||
put("age", 30);
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-1");
|
||||
put("age", "31");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-2");
|
||||
put("age", 32);
|
||||
put("password", "7654321");
|
||||
}}
|
||||
));
|
||||
map.put("color", Color.RED);
|
||||
map.put("color2", Color.RED.getText());
|
||||
map.put("color3", "BLUE");
|
||||
map.put("arr", "51,52");
|
||||
map.put("arr2", new String[]{"53", "54"});
|
||||
map.put("arr3", Arrays.asList("55", "56"));
|
||||
map.put("arr4", new String[]{"57", "58"});
|
||||
map.put("arr7", Arrays.asList("59", "60"));
|
||||
map.put("arr8", new int[]{61, 62});
|
||||
map.put("colors", new String[]{"BLUE", "RED"});
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createCollectionHeavyMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", "collection-heavy");
|
||||
map.put("ids", Arrays.asList("alpha", "beta"));
|
||||
map.put("arr", new String[]{"1", "2"});
|
||||
map.put("arr2", Arrays.asList("3", "4"));
|
||||
map.put("arr3", Arrays.asList(5, 6));
|
||||
map.put("arr4", Arrays.asList(7, 8));
|
||||
map.put("arr7", new long[]{9L, 10L});
|
||||
map.put("arr8", Arrays.asList(11, 12));
|
||||
map.put("colors", new Color[]{Color.BLUE, Color.RED});
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createNestedHeavyMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", "nested-heavy");
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-root");
|
||||
put("age", "31");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-map");
|
||||
put("age", 32);
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-tail");
|
||||
put("age", "33");
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-1");
|
||||
put("age", "34");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-2");
|
||||
put("age", 35);
|
||||
put("password", "7654321");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-3");
|
||||
put("age", "36");
|
||||
put("password", "1357246");
|
||||
}}
|
||||
));
|
||||
map.put("color2", "红色");
|
||||
map.put("color3", "BLUE");
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createNestedOnlyMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-root");
|
||||
put("age", "31");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-map");
|
||||
put("age", 32);
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-tail");
|
||||
put("age", "33");
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-1");
|
||||
put("age", "34");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-2");
|
||||
put("age", 35);
|
||||
put("password", "7654321");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-3");
|
||||
put("age", "36");
|
||||
put("password", "1357246");
|
||||
}}
|
||||
));
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimpleSource {
|
||||
@@ -335,4 +508,43 @@ public class FastBeanCopierJmhBenchmark {
|
||||
this.boy2 = boy2;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NestedOnlyTarget {
|
||||
private NestObject nestObject;
|
||||
private NestObject nestObject2;
|
||||
private Map<String, Object> nestObject3;
|
||||
private List<Map<String, Object>> nestObjects;
|
||||
|
||||
public NestObject getNestObject() {
|
||||
return nestObject;
|
||||
}
|
||||
|
||||
public void setNestObject(NestObject nestObject) {
|
||||
this.nestObject = nestObject;
|
||||
}
|
||||
|
||||
public NestObject getNestObject2() {
|
||||
return nestObject2;
|
||||
}
|
||||
|
||||
public void setNestObject2(NestObject nestObject2) {
|
||||
this.nestObject2 = nestObject2;
|
||||
}
|
||||
|
||||
public Map<String, Object> getNestObject3() {
|
||||
return nestObject3;
|
||||
}
|
||||
|
||||
public void setNestObject3(Map<String, Object> nestObject3) {
|
||||
this.nestObject3 = nestObject3;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getNestObjects() {
|
||||
return nestObjects;
|
||||
}
|
||||
|
||||
public void setNestObjects(List<Map<String, Object>> nestObjects) {
|
||||
this.nestObjects = nestObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public class FastBeanCopierJmhRunner {
|
||||
|
||||
Options options = new OptionsBuilder()
|
||||
.include(FastBeanCopierJmhBenchmark.class.getSimpleName())
|
||||
.forks(0)
|
||||
.result(resultFile.toString())
|
||||
.resultFormat(ResultFormatType.JSON)
|
||||
.build();
|
||||
|
||||
@@ -10,12 +10,15 @@ import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class FastBeanCopierSupportTest {
|
||||
|
||||
@@ -122,6 +125,106 @@ public class FastBeanCopierSupportTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalBackendComplexConversionCompatibility() {
|
||||
FastBeanCopierBackend original = FastBeanCopierSupport.getBackend();
|
||||
try {
|
||||
for (BackendCase backendCase : benchmarkBackends()) {
|
||||
FastBeanCopierSupport.setBackend(backendCase.backend);
|
||||
Target fromBean = FastBeanCopierSupport.copy(createComplexBenchmarkSource(), new Target());
|
||||
assertTargetCopied(createComplexBenchmarkSource(), fromBean);
|
||||
|
||||
Target fromMap = FastBeanCopierSupport.copy(createHeterogeneousBenchmarkMap(), new Target());
|
||||
Assert.assertEquals(backendCase.name, "heterogeneous-benchmark", fromMap.getName());
|
||||
Assert.assertEquals(backendCase.name, Boolean.TRUE, fromMap.getBoy());
|
||||
Assert.assertFalse(backendCase.name, fromMap.isBoy2());
|
||||
Assert.assertEquals(backendCase.name, "1", fromMap.getBoy3());
|
||||
Assert.assertEquals(backendCase.name, 123, fromMap.getAge());
|
||||
Assert.assertEquals(backendCase.name, 456, fromMap.getAge2());
|
||||
Assert.assertEquals(backendCase.name, Color.BLUE, fromMap.getColor3());
|
||||
Assert.assertArrayEquals(backendCase.name, new long[]{39L, 40L}, fromMap.getArr7());
|
||||
assertNumberListEquals(backendCase.name, Arrays.asList(41, 42), fromMap.getArr8());
|
||||
Assert.assertNotNull(backendCase.name, fromMap.getNestObject());
|
||||
Assert.assertEquals(backendCase.name, "map-nest-1", fromMap.getNestObject().getName());
|
||||
}
|
||||
} finally {
|
||||
FastBeanCopierSupport.setBackend(original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalBackendConversionHeavyMapCompatibility() {
|
||||
FastBeanCopierBackend original = FastBeanCopierSupport.getBackend();
|
||||
try {
|
||||
for (BackendCase backendCase : benchmarkBackends()) {
|
||||
FastBeanCopierSupport.setBackend(backendCase.backend);
|
||||
Target target = FastBeanCopierSupport.copy(createConversionHeavyBenchmarkMap(), new Target());
|
||||
Assert.assertEquals(backendCase.name, "123456", target.getName());
|
||||
Assert.assertArrayEquals(backendCase.name, new String[]{"alpha", "beta", "gamma"}, target.getIds());
|
||||
Assert.assertEquals(backendCase.name, Boolean.TRUE, target.getBoy());
|
||||
Assert.assertTrue(backendCase.name, target.isBoy2());
|
||||
Assert.assertEquals(backendCase.name, "false", target.getBoy3());
|
||||
Assert.assertEquals(backendCase.name, 2048, target.getAge());
|
||||
Assert.assertEquals(backendCase.name, 4096, target.getAge2());
|
||||
Assert.assertEquals(backendCase.name, "8192", target.getAge3());
|
||||
Assert.assertEquals(backendCase.name, 1, target.getColor());
|
||||
Assert.assertEquals(backendCase.name, Color.RED, target.getColor2());
|
||||
Assert.assertEquals(backendCase.name, Color.BLUE, target.getColor3());
|
||||
Assert.assertArrayEquals(backendCase.name, new String[]{"51", "52"}, target.getArr());
|
||||
Assert.assertArrayEquals(backendCase.name, new long[]{59L, 60L}, target.getArr7());
|
||||
assertNumberListEquals(backendCase.name, Arrays.asList(61, 62), target.getArr8());
|
||||
}
|
||||
} finally {
|
||||
FastBeanCopierSupport.setBackend(original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalBackendCollectionHeavyMapCompatibility() {
|
||||
FastBeanCopierBackend original = FastBeanCopierSupport.getBackend();
|
||||
try {
|
||||
for (BackendCase backendCase : benchmarkBackends()) {
|
||||
FastBeanCopierSupport.setBackend(backendCase.backend);
|
||||
Target target = FastBeanCopierSupport.copy(createCollectionHeavyBenchmarkMap(), new Target());
|
||||
Assert.assertEquals(backendCase.name, "collection-heavy", target.getName());
|
||||
Assert.assertArrayEquals(backendCase.name, new String[]{"alpha", "beta"}, target.getIds());
|
||||
Assert.assertArrayEquals(backendCase.name, new String[]{"1", "2"}, target.getArr());
|
||||
Assert.assertEquals(backendCase.name, Arrays.asList("3", "4"), target.getArr2());
|
||||
Assert.assertArrayEquals(backendCase.name, new Integer[]{5, 6}, target.getArr3());
|
||||
Assert.assertArrayEquals(backendCase.name, new Integer[]{7, 8}, target.getArr4());
|
||||
Assert.assertArrayEquals(backendCase.name, new long[]{9L, 10L}, target.getArr7());
|
||||
assertNumberListEquals(backendCase.name, Arrays.asList(11, 12), target.getArr8());
|
||||
Assert.assertArrayEquals(backendCase.name, new Color[]{Color.BLUE, Color.RED}, target.getColors());
|
||||
}
|
||||
} finally {
|
||||
FastBeanCopierSupport.setBackend(original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalBackendNestedHeavyMapCompatibility() {
|
||||
FastBeanCopierBackend original = FastBeanCopierSupport.getBackend();
|
||||
try {
|
||||
for (BackendCase backendCase : benchmarkBackends()) {
|
||||
FastBeanCopierSupport.setBackend(backendCase.backend);
|
||||
Target target = FastBeanCopierSupport.copy(createNestedHeavyBenchmarkMap(), new Target());
|
||||
Assert.assertEquals(backendCase.name, "nested-heavy", target.getName());
|
||||
Assert.assertNotNull(backendCase.name, target.getNestObject());
|
||||
Assert.assertEquals(backendCase.name, "nest-root", target.getNestObject().getName());
|
||||
Assert.assertEquals(backendCase.name, 31, target.getNestObject().getAge());
|
||||
Assert.assertNotNull(backendCase.name, target.getNestObject2());
|
||||
Assert.assertEquals(backendCase.name, "nest-map", target.getNestObject2().getName());
|
||||
Assert.assertEquals(backendCase.name, 32, target.getNestObject2().getAge());
|
||||
Assert.assertNotNull(backendCase.name, target.getNestObject3());
|
||||
Assert.assertEquals(backendCase.name, "nest-tail", String.valueOf(target.getNestObject3().get("name")));
|
||||
Assert.assertNotNull(backendCase.name, target.getNestObjects());
|
||||
Assert.assertEquals(backendCase.name, 3, target.getNestObjects().size());
|
||||
}
|
||||
} finally {
|
||||
FastBeanCopierSupport.setBackend(original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackendPerformanceComparison() {
|
||||
Source source = createBenchmarkSource();
|
||||
@@ -169,6 +272,33 @@ public class FastBeanCopierSupportTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackendPerformanceAcrossScenarios() {
|
||||
int warmup = 3_000;
|
||||
int iterations = 20_000;
|
||||
|
||||
FastBeanCopierBackend original = FastBeanCopierSupport.getBackend();
|
||||
try {
|
||||
System.out.println("\n=== FastBeanCopier 多场景性能对比 ===");
|
||||
System.out.println("预热次数: " + warmup + ", 正式迭代: " + iterations);
|
||||
for (BenchmarkScenario scenario : benchmarkScenarios()) {
|
||||
System.out.println("-- scenario: " + scenario.name);
|
||||
Map<String, ScenarioBenchmarkResult> results = benchmarkScenarioBackends(scenario, warmup, iterations);
|
||||
results.values().forEach(this::printScenarioBenchmark);
|
||||
BenchmarkResult baseline = results.get("javassist");
|
||||
for (ScenarioBenchmarkResult result : results.values()) {
|
||||
if ("javassist".equals(result.name)) {
|
||||
continue;
|
||||
}
|
||||
System.out.println("cached ratio (" + result.name + "/javassist): " + formatRatio(result.cachedWarmNanos, baseline.cachedWarmNanos));
|
||||
System.out.println("support ratio (" + result.name + "/javassist): " + formatRatio(result.supportWarmNanos, baseline.supportWarmNanos));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
FastBeanCopierSupport.setBackend(original);
|
||||
}
|
||||
}
|
||||
|
||||
private long runCachedCopierBenchmark(Source source, Copier copier, int iterations) {
|
||||
long start = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
@@ -185,6 +315,22 @@ public class FastBeanCopierSupportTest {
|
||||
return System.nanoTime() - start;
|
||||
}
|
||||
|
||||
private long runCachedCopierBenchmark(Object source, Supplier<Object> targetSupplier, Copier copier, int iterations) {
|
||||
long start = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
copier.copy(source, targetSupplier.get(), Collections.emptySet(), FastBeanCopierSupport.DEFAULT_CONVERT);
|
||||
}
|
||||
return System.nanoTime() - start;
|
||||
}
|
||||
|
||||
private long runSupportBenchmark(Object source, Supplier<Object> targetSupplier, int iterations) {
|
||||
long start = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
FastBeanCopierSupport.copy(source, targetSupplier.get());
|
||||
}
|
||||
return System.nanoTime() - start;
|
||||
}
|
||||
|
||||
private long runBackendColdBenchmark(Source source, FastBeanCopierBackend backend) {
|
||||
long start = System.nanoTime();
|
||||
Copier copier = backend.createCopier(Source.class, Target.class);
|
||||
@@ -229,6 +375,33 @@ public class FastBeanCopierSupportTest {
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map<String, ScenarioBenchmarkResult> benchmarkScenarioBackends(BenchmarkScenario scenario,
|
||||
int warmup,
|
||||
int iterations) {
|
||||
Map<String, ScenarioBenchmarkResult> results = new LinkedHashMap<>();
|
||||
for (BackendCase backend : benchmarkBackends()) {
|
||||
FastBeanCopierSupport.setBackend(backend.backend);
|
||||
FastBeanCopierSupport.clearCache();
|
||||
Object source = scenario.sourceSupplier.get();
|
||||
Object warmupTarget = scenario.targetSupplier.get();
|
||||
Copier copier = FastBeanCopierSupport.getCopier(source, warmupTarget, true);
|
||||
for (int i = 0; i < warmup; i++) {
|
||||
Object cachedResult = scenario.targetSupplier.get();
|
||||
copier.copy(source,
|
||||
cachedResult,
|
||||
Collections.emptySet(),
|
||||
FastBeanCopierSupport.DEFAULT_CONVERT);
|
||||
scenario.resultChecker.accept(cachedResult);
|
||||
Object supportResult = FastBeanCopierSupport.copy(source, scenario.targetSupplier.get());
|
||||
scenario.resultChecker.accept(supportResult);
|
||||
}
|
||||
long cached = runCachedCopierBenchmark(source, scenario.targetSupplier, copier, iterations);
|
||||
long support = runSupportBenchmark(source, scenario.targetSupplier, iterations);
|
||||
results.put(backend.name, new ScenarioBenchmarkResult(scenario.name, backend.name, cached, support));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map<String, BenchmarkResult> average(Map<String, BenchmarkResult> first,
|
||||
Map<String, BenchmarkResult> second) {
|
||||
Map<String, BenchmarkResult> average = new LinkedHashMap<>();
|
||||
@@ -248,6 +421,14 @@ public class FastBeanCopierSupportTest {
|
||||
result.supportColdNanos / 1_000_000.0));
|
||||
}
|
||||
|
||||
private void printScenarioBenchmark(ScenarioBenchmarkResult result) {
|
||||
System.out.println(String.format(Locale.ROOT,
|
||||
"%-18s cached=%8.2fms support=%8.2fms",
|
||||
result.name,
|
||||
result.cachedWarmNanos / 1_000_000.0,
|
||||
result.supportWarmNanos / 1_000_000.0));
|
||||
}
|
||||
|
||||
private String formatRatio(long value, long base) {
|
||||
return String.format(Locale.ROOT, "%.2fx", (double) value / (double) base);
|
||||
}
|
||||
@@ -390,8 +571,22 @@ public class FastBeanCopierSupportTest {
|
||||
Assert.assertEquals(source.getColor().getValue().intValue(), target.getColor());
|
||||
Assert.assertEquals(source.getColor(), target.getColor2());
|
||||
Assert.assertEquals(Color.BLUE, target.getColor3());
|
||||
Assert.assertArrayEquals(new long[]{1L, 2L}, target.getArr7());
|
||||
Assert.assertEquals(Arrays.asList(1, 2), target.getArr8());
|
||||
Assert.assertArrayEquals(Arrays.stream(source.getArr7()).asLongStream().toArray(), target.getArr7());
|
||||
assertNumberListEquals("source", Arrays.stream(source.getArr8()).boxed().toList(), target.getArr8());
|
||||
}
|
||||
|
||||
private void assertNumberListEquals(String message, List<Integer> expected, List<?> actual) {
|
||||
Assert.assertNotNull(message, actual);
|
||||
Assert.assertEquals(message, expected.size(), actual.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
Object actualValue = actual.get(i);
|
||||
int normalized = actualValue instanceof Number
|
||||
? ((Number) actualValue).intValue()
|
||||
: Integer.parseInt(String.valueOf(actualValue));
|
||||
Assert.assertEquals(message + "[" + i + "]",
|
||||
expected.get(i).intValue(),
|
||||
normalized);
|
||||
}
|
||||
}
|
||||
|
||||
private Source createBenchmarkSource() {
|
||||
@@ -407,12 +602,300 @@ public class FastBeanCopierSupportTest {
|
||||
return source;
|
||||
}
|
||||
|
||||
private Source createComplexBenchmarkSource() {
|
||||
Source source = new Source();
|
||||
source.setName("complex-benchmark");
|
||||
source.setIds(new String[]{"11", "12", "13"});
|
||||
source.setBoy(true);
|
||||
source.setBoy2(false);
|
||||
source.setBoy3(true);
|
||||
source.setAge(36);
|
||||
source.setAge2(40);
|
||||
source.setAge3(41);
|
||||
source.setColor(Color.RED);
|
||||
source.setColor2("RED");
|
||||
source.setColor3(Color.BLUE.getValue());
|
||||
source.setNestObject(new NestObject("nest-complex", 18, "1234567"));
|
||||
source.setNestObject2(new LinkedHashMap<>());
|
||||
source.getNestObject2().put("name", "nest-map");
|
||||
source.getNestObject2().put("age", 19);
|
||||
source.setNestObject3(new NestObject("nest-tail", 20, "7654321"));
|
||||
source.setNestObjects(Arrays.asList(
|
||||
new NestObject("nest-list-1", 21, "1234567"),
|
||||
new NestObject("nest-list-2", 22, "7654321")
|
||||
));
|
||||
source.setArr(Arrays.asList("1", "2", "3"));
|
||||
source.setArr2(new String[]{"4", "5", "6"});
|
||||
source.setArr3(new String[]{"7", "8", "9"});
|
||||
source.setArr4(Arrays.asList("10", "11", "12"));
|
||||
source.setArr5(new String[]{"13", "14"});
|
||||
source.setArr6(new String[]{"15", "16"});
|
||||
source.setArr7(new int[]{17, 18});
|
||||
source.setArr8(new int[]{19, 20});
|
||||
source.setColors(new Color[]{Color.BLUE, Color.RED});
|
||||
return source;
|
||||
}
|
||||
|
||||
private Map<String, Object> createHeterogeneousBenchmarkMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", "heterogeneous-benchmark");
|
||||
map.put("ids", Arrays.asList("101", "102", "103"));
|
||||
map.put("boy", "true");
|
||||
map.put("boy2", Boolean.FALSE);
|
||||
map.put("boy3", 1);
|
||||
map.put("age", "123");
|
||||
map.put("age2", 456L);
|
||||
map.put("age3", 789);
|
||||
map.put("deleteTime", "2024-05-07 10:20:30");
|
||||
map.put("createTime", "2024-05-08 11:30:40");
|
||||
map.put("updateTime", "2024-05-09 12:40:50");
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-1");
|
||||
put("age", "18");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-2");
|
||||
put("age", 19);
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-3");
|
||||
put("age", 20);
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-1");
|
||||
put("age", "21");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-2");
|
||||
put("age", 22);
|
||||
put("password", "7654321");
|
||||
}}
|
||||
));
|
||||
map.put("color", Color.RED);
|
||||
map.put("color2", "RED");
|
||||
map.put("color3", Color.BLUE.getValue());
|
||||
map.put("arr", Arrays.asList("31", "32"));
|
||||
map.put("arr2", Arrays.asList("33", "34"));
|
||||
map.put("arr3", new String[]{"35", "36"});
|
||||
map.put("arr4", Arrays.asList("37", "38"));
|
||||
map.put("arr7", new int[]{39, 40});
|
||||
map.put("arr8", Arrays.asList("41", "42"));
|
||||
map.put("colors", Arrays.asList("BLUE", "RED"));
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createConversionHeavyBenchmarkMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", 123456);
|
||||
map.put("ids", "alpha,beta,gamma");
|
||||
map.put("boy", "true");
|
||||
map.put("boy2", 1);
|
||||
map.put("boy3", false);
|
||||
map.put("age", "2048");
|
||||
map.put("age2", 4096L);
|
||||
map.put("age3", 8192);
|
||||
map.put("deleteTime", "2024-05-07 10:20:30");
|
||||
map.put("createTime", new Date(1715043600000L));
|
||||
map.put("updateTime", "2024-05-08 11:30:40");
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", 1001);
|
||||
put("age", "28");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-2");
|
||||
put("age", "29");
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "map-nest-3");
|
||||
put("age", 30);
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-1");
|
||||
put("age", "31");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "list-nest-2");
|
||||
put("age", 32);
|
||||
put("password", "7654321");
|
||||
}}
|
||||
));
|
||||
map.put("color", Color.RED);
|
||||
map.put("color2", Color.RED.getText());
|
||||
map.put("color3", "BLUE");
|
||||
map.put("arr", "51,52");
|
||||
map.put("arr2", new String[]{"53", "54"});
|
||||
map.put("arr3", Arrays.asList("55", "56"));
|
||||
map.put("arr4", new String[]{"57", "58"});
|
||||
map.put("arr7", Arrays.asList("59", "60"));
|
||||
map.put("arr8", new int[]{61, 62});
|
||||
map.put("colors", new String[]{"BLUE", "RED"});
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createCollectionHeavyBenchmarkMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", "collection-heavy");
|
||||
map.put("ids", Arrays.asList("alpha", "beta"));
|
||||
map.put("arr", new String[]{"1", "2"});
|
||||
map.put("arr2", Arrays.asList("3", "4"));
|
||||
map.put("arr3", Arrays.asList(5, 6));
|
||||
map.put("arr4", Arrays.asList(7, 8));
|
||||
map.put("arr7", new long[]{9L, 10L});
|
||||
map.put("arr8", Arrays.asList(11, 12));
|
||||
map.put("colors", new Color[]{Color.BLUE, Color.RED});
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Object> createNestedHeavyBenchmarkMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("name", "nested-heavy");
|
||||
map.put("nestObject", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-root");
|
||||
put("age", "31");
|
||||
put("password", "1234567");
|
||||
}});
|
||||
map.put("nestObject2", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-map");
|
||||
put("age", 32);
|
||||
}});
|
||||
map.put("nestObject3", new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-tail");
|
||||
put("age", "33");
|
||||
put("password", "7654321");
|
||||
}});
|
||||
map.put("nestObjects", Arrays.asList(
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-1");
|
||||
put("age", "34");
|
||||
put("password", "1234567");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-2");
|
||||
put("age", 35);
|
||||
put("password", "7654321");
|
||||
}},
|
||||
new LinkedHashMap<String, Object>() {{
|
||||
put("name", "nest-list-3");
|
||||
put("age", "36");
|
||||
put("password", "1357246");
|
||||
}}
|
||||
));
|
||||
map.put("color2", "红色");
|
||||
map.put("color3", "BLUE");
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<BenchmarkScenario> benchmarkScenarios() {
|
||||
return Arrays.asList(
|
||||
new BenchmarkScenario("simple-bean->bean",
|
||||
() -> {
|
||||
FastBeanCopierJmhBenchmark.SimpleSource source = new FastBeanCopierJmhBenchmark.SimpleSource();
|
||||
source.setName("simple-benchmark");
|
||||
source.setAge(18);
|
||||
source.setEnabled(true);
|
||||
source.setScore(88L);
|
||||
source.setCreated(new java.util.Date(1715040000000L));
|
||||
return source;
|
||||
},
|
||||
FastBeanCopierJmhBenchmark.SimpleTarget::new,
|
||||
result -> Assert.assertNotNull(((FastBeanCopierJmhBenchmark.SimpleTarget) result).getName())),
|
||||
new BenchmarkScenario("complex-bean->bean",
|
||||
this::createComplexBenchmarkSource,
|
||||
Target::new,
|
||||
result -> assertTargetCopied(createComplexBenchmarkSource(), (Target) result)),
|
||||
new BenchmarkScenario("heterogeneous-map->bean",
|
||||
this::createHeterogeneousBenchmarkMap,
|
||||
Target::new,
|
||||
result -> {
|
||||
Target target = (Target) result;
|
||||
Assert.assertEquals("heterogeneous-benchmark", target.getName());
|
||||
Assert.assertEquals(Boolean.TRUE, target.getBoy());
|
||||
Assert.assertFalse(target.isBoy2());
|
||||
Assert.assertEquals("1", target.getBoy3());
|
||||
Assert.assertEquals(123, target.getAge());
|
||||
Assert.assertEquals(456, target.getAge2());
|
||||
Assert.assertEquals(Color.BLUE, target.getColor3());
|
||||
Assert.assertArrayEquals(new long[]{39L, 40L}, target.getArr7());
|
||||
assertNumberListEquals("heterogeneous-map->bean", Arrays.asList(41, 42), target.getArr8());
|
||||
}),
|
||||
new BenchmarkScenario("conversion-heavy-map->bean",
|
||||
this::createConversionHeavyBenchmarkMap,
|
||||
Target::new,
|
||||
result -> {
|
||||
Target target = (Target) result;
|
||||
Assert.assertEquals("123456", target.getName());
|
||||
Assert.assertArrayEquals(new String[]{"alpha", "beta", "gamma"}, target.getIds());
|
||||
Assert.assertEquals(Boolean.TRUE, target.getBoy());
|
||||
Assert.assertTrue(target.isBoy2());
|
||||
Assert.assertEquals("false", target.getBoy3());
|
||||
Assert.assertEquals(2048, target.getAge());
|
||||
Assert.assertEquals(4096, target.getAge2());
|
||||
Assert.assertEquals("8192", target.getAge3());
|
||||
Assert.assertEquals(1, target.getColor());
|
||||
Assert.assertEquals(Color.RED, target.getColor2());
|
||||
Assert.assertEquals(Color.BLUE, target.getColor3());
|
||||
Assert.assertArrayEquals(new String[]{"51", "52"}, target.getArr());
|
||||
Assert.assertArrayEquals(new long[]{59L, 60L}, target.getArr7());
|
||||
assertNumberListEquals("conversion-heavy-map->bean", Arrays.asList(61, 62), target.getArr8());
|
||||
}),
|
||||
new BenchmarkScenario("collection-heavy-map->bean",
|
||||
this::createCollectionHeavyBenchmarkMap,
|
||||
Target::new,
|
||||
result -> {
|
||||
Target target = (Target) result;
|
||||
Assert.assertEquals("collection-heavy", target.getName());
|
||||
Assert.assertArrayEquals(new String[]{"alpha", "beta"}, target.getIds());
|
||||
Assert.assertArrayEquals(new String[]{"1", "2"}, target.getArr());
|
||||
Assert.assertEquals(Arrays.asList("3", "4"), target.getArr2());
|
||||
Assert.assertArrayEquals(new Integer[]{5, 6}, target.getArr3());
|
||||
Assert.assertArrayEquals(new Integer[]{7, 8}, target.getArr4());
|
||||
Assert.assertArrayEquals(new long[]{9L, 10L}, target.getArr7());
|
||||
assertNumberListEquals("collection-heavy-map->bean", Arrays.asList(11, 12), target.getArr8());
|
||||
Assert.assertArrayEquals(new Color[]{Color.BLUE, Color.RED}, target.getColors());
|
||||
}),
|
||||
new BenchmarkScenario("nested-heavy-map->bean",
|
||||
this::createNestedHeavyBenchmarkMap,
|
||||
Target::new,
|
||||
result -> {
|
||||
Target target = (Target) result;
|
||||
Assert.assertEquals("nested-heavy", target.getName());
|
||||
Assert.assertNotNull(target.getNestObject());
|
||||
Assert.assertEquals("nest-root", target.getNestObject().getName());
|
||||
Assert.assertEquals(31, target.getNestObject().getAge());
|
||||
Assert.assertNotNull(target.getNestObject2());
|
||||
Assert.assertEquals("nest-map", target.getNestObject2().getName());
|
||||
Assert.assertEquals(32, target.getNestObject2().getAge());
|
||||
Assert.assertNotNull(target.getNestObject3());
|
||||
Assert.assertEquals("nest-tail", String.valueOf(target.getNestObject3().get("name")));
|
||||
Assert.assertNotNull(target.getNestObjects());
|
||||
Assert.assertEquals(3, target.getNestObjects().size());
|
||||
}),
|
||||
new BenchmarkScenario("complex-bean->map",
|
||||
this::createComplexBenchmarkSource,
|
||||
HashMap::new,
|
||||
result -> {
|
||||
Map<?, ?> map = (Map<?, ?>) result;
|
||||
Assert.assertEquals("complex-benchmark", map.get("name"));
|
||||
Assert.assertEquals(36, map.get("age"));
|
||||
Assert.assertEquals("RED", String.valueOf(map.get("color2")));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static class BenchmarkResult {
|
||||
private final String name;
|
||||
private final long cachedWarmNanos;
|
||||
private final long supportWarmNanos;
|
||||
private final long backendColdNanos;
|
||||
private final long supportColdNanos;
|
||||
final String name;
|
||||
final long cachedWarmNanos;
|
||||
final long supportWarmNanos;
|
||||
final long backendColdNanos;
|
||||
final long supportColdNanos;
|
||||
|
||||
BenchmarkResult(String name,
|
||||
long cachedWarmNanos,
|
||||
@@ -435,6 +918,15 @@ public class FastBeanCopierSupportTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class ScenarioBenchmarkResult extends BenchmarkResult {
|
||||
private final String scenario;
|
||||
|
||||
ScenarioBenchmarkResult(String scenario, String name, long cachedWarmNanos, long supportWarmNanos) {
|
||||
super(name, cachedWarmNanos, supportWarmNanos, 0, 0);
|
||||
this.scenario = scenario;
|
||||
}
|
||||
}
|
||||
|
||||
static class BackendCase {
|
||||
private final String name;
|
||||
private final FastBeanCopierBackend backend;
|
||||
@@ -444,4 +936,21 @@ public class FastBeanCopierSupportTest {
|
||||
this.backend = backend;
|
||||
}
|
||||
}
|
||||
|
||||
static class BenchmarkScenario {
|
||||
private final String name;
|
||||
private final Supplier<Object> sourceSupplier;
|
||||
private final Supplier<Object> targetSupplier;
|
||||
private final Consumer<Object> resultChecker;
|
||||
|
||||
BenchmarkScenario(String name,
|
||||
Supplier<Object> sourceSupplier,
|
||||
Supplier<Object> targetSupplier,
|
||||
Consumer<Object> resultChecker) {
|
||||
this.name = name;
|
||||
this.sourceSupplier = sourceSupplier;
|
||||
this.targetSupplier = targetSupplier;
|
||||
this.resultChecker = resultChecker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,18 @@ public class FastBeanCopierTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharSequence() {
|
||||
CharSequenceTarget beanTarget = FastBeanCopier.copy(new CharSequenceSource("test"), new CharSequenceTarget());
|
||||
Assert.assertEquals("test", String.valueOf(beanTarget.getValue()));
|
||||
|
||||
Map<String, Object> source = new HashMap<>();
|
||||
source.put("value", 123);
|
||||
|
||||
CharSequenceTarget mapTarget = FastBeanCopier.copy(source, new CharSequenceTarget());
|
||||
Assert.assertEquals("123", String.valueOf(mapTarget.getValue()));
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Config {
|
||||
@@ -178,6 +190,22 @@ public class FastBeanCopierTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class CharSequenceSource {
|
||||
private String value;
|
||||
|
||||
public CharSequenceSource(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class CharSequenceTarget {
|
||||
private CharSequence value;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyMap() {
|
||||
|
||||
@@ -337,4 +365,4 @@ public class FastBeanCopierTest {
|
||||
public static class ExtendableExtends extends ExtendableSuper {
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user