perf(bean): 优化FastBeanCopier复制性能

This commit is contained in:
zhouhao
2026-05-28 09:20:17 +08:00
parent 8c668750c8
commit c4452f1d7f
9 changed files with 1628 additions and 63 deletions

View File

@@ -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()) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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` 在整体真实场景仍是首选。
### 定向 JMHsimple / 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`

View File

@@ -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;
}
}
}

View File

@@ -23,6 +23,7 @@ public class FastBeanCopierJmhRunner {
Options options = new OptionsBuilder()
.include(FastBeanCopierJmhBenchmark.class.getSimpleName())
.forks(0)
.result(resultFile.toString())
.resultFormat(ResultFormatType.JSON)
.build();

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}