add xxxCopier

This commit is contained in:
Looly 2022-03-19 21:00:36 +08:00
parent 858a0826f0
commit b848efb7b4
16 changed files with 552 additions and 548 deletions

View File

@ -2,7 +2,12 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.7.23 (2022-03-19)
# 5.8.0 (2022-03-19)
### ❌不兼容特性
* 【db 】 【不向下兼容】增加MongoDB4.x支持pr#568@Gitee
* 【json 】 【可能兼容问题】修改JSONObject结构继承自MapWrapper
* 【core 】 【可能兼容问题】BeanCopier重构新建XXXCopier删除XXXValueProvider
### 🐣新特性
* 【http 】 HttpRequest.form采用TableMap方式issue#I4W427@Gitee
@ -14,7 +19,6 @@
* 【core 】 阿拉伯数字转换成中文对发票票面金额转换的扩展pr#570@Gitee
* 【core 】 ArrayUtil增加replace方法pr#570@Gitee
* 【core 】 CsvReadConfig增加自定义标题行行号issue#2180@Github
* 【db 】 增加MongoDB4.x支持pr#568@Gitee
* 【core 】 FileAppender优化初始List大小pr#2197@Github
* 【core 】 Base32增加pad支持pr#2195@Github
* 【core 】 Dict增加setFields方法pr#578@Gitee
@ -22,7 +26,8 @@
* 【db 】 Oracle中Column#typeName后的长度去掉pr#563@Gitee
* 【poi 】 优化ExcelReader采用只读模式pr#2204@Gitee
* 【poi 】 优化ExcelBase将alias放入
*
* 【poi 】 优化ExcelBase将alias放入
### 🐞Bug修复
* 【core 】 修复ObjectUtil.hasNull传入null返回true的问题pr#555@Gitee
* 【core 】 修复NumberConverter对数字转换的问题issue#I4WPF4@Gitee
@ -31,6 +36,8 @@
* 【http 】 修复GET重定向时携带参数问题issue#2189@Github
* 【core 】 修复FileUtil、FileCopier相对路径获取父路径错误问题pr#2188@Github
* 【core 】 修复CopyOptions中fieldNameEditor无效问题issue#2202@Github
* 【json 】 修复JSON对Map.Entry的解析问题
* 【core 】 修复MapConverter中map与map转换兼容问题
-------------------------------------------------------------------------------------------------------------
# 5.7.22 (2022-03-01)

View File

@ -0,0 +1,28 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.lang.copier.Copier;
import cn.hutool.core.util.ObjectUtil;
/**
* 抽象的对象拷贝封装提供来源对象目标对象持有
*
* @param <S> 来源对象类型
* @param <T> 目标对象类型
* @author looly
* @since 5.8.0
*/
public abstract class AbsCopier<S, T> implements Copier<T> {
protected final S source;
protected final T target;
/**
* 拷贝选项
*/
protected final CopyOptions copyOptions;
public AbsCopier(S source, T target, CopyOptions copyOptions) {
this.source = source;
this.target = target;
this.copyOptions = ObjectUtil.defaultIfNull(copyOptions, CopyOptions::create);
}
}

View File

@ -1,19 +1,9 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.bean.BeanException;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.DynaBean;
import cn.hutool.core.bean.copier.provider.BeanValueProvider;
import cn.hutool.core.bean.copier.provider.DynaBeanValueProvider;
import cn.hutool.core.bean.copier.provider.MapValueProvider;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.copier.Copier;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Map;
/**
@ -34,27 +24,19 @@ import java.util.Map;
public class BeanCopier<T> implements Copier<T>, Serializable {
private static final long serialVersionUID = 1L;
/** 源对象 */
@SuppressWarnings("NonSerializableFieldInSerializableClass")
private final Object source;
/** 目标对象 */
private final T dest;
/** 目标的类型(用于泛型类注入) */
private final Type destType;
/** 拷贝选项 */
private final CopyOptions copyOptions;
private final Copier<T> copier;
/**
* 创建BeanCopier
*
* @param <T> 目标Bean类型
* @param source 来源对象可以是Bean或者Map
* @param dest 目标Bean对象
* @param target 目标Bean对象
* @param copyOptions 拷贝属性选项
* @return BeanCopier
*/
public static <T> BeanCopier<T> create(Object source, T dest, CopyOptions copyOptions) {
return create(source, dest, dest.getClass(), copyOptions);
public static <T> BeanCopier<T> create(Object source, T target, CopyOptions copyOptions) {
return create(source, target, target.getClass(), copyOptions);
}
/**
@ -62,253 +44,48 @@ public class BeanCopier<T> implements Copier<T>, Serializable {
*
* @param <T> 目标Bean类型
* @param source 来源对象可以是Bean或者Map
* @param dest 目标Bean对象
* @param target 目标Bean对象
* @param destType 目标的泛型类型用于标注有泛型参数的Bean对象
* @param copyOptions 拷贝属性选项
* @return BeanCopier
*/
public static <T> BeanCopier<T> create(Object source, T dest, Type destType, CopyOptions copyOptions) {
return new BeanCopier<>(source, dest, destType, copyOptions);
public static <T> BeanCopier<T> create(Object source, T target, Type destType, CopyOptions copyOptions) {
return new BeanCopier<>(source, target, destType, copyOptions);
}
/**
* 构造
*
* @param source 来源对象可以是Bean或者Map
* @param dest 目标Bean对象
* @param destType 目标的泛型类型用于标注有泛型参数的Bean对象
* @param target 目标Bean对象
* @param targetType 目标的泛型类型用于标注有泛型参数的Bean对象
* @param copyOptions 拷贝属性选项
*/
public BeanCopier(Object source, T dest, Type destType, CopyOptions copyOptions) {
this.source = source;
this.dest = dest;
this.destType = destType;
this.copyOptions = copyOptions;
public BeanCopier(Object source, T target, Type targetType, CopyOptions copyOptions) {
Copier<T> copier;
if (source instanceof Map) {
if (target instanceof Map) {
//noinspection unchecked
copier = (Copier<T>) new MapToMapCopier((Map<?, ?>) source, (Map<?, ?>) target, targetType, copyOptions);
} else {
copier = new MapToBeanCopier<>((Map<?, ?>) source, target, targetType, copyOptions);
}
}else if(source instanceof ValueProvider){
//noinspection unchecked
copier = new ValueProviderToBeanCopier<>((ValueProvider<String>) source, target, targetType, copyOptions);
} else {
if (target instanceof Map) {
//noinspection unchecked
copier = (Copier<T>) new BeanToMapCopier(source, (Map<?, ?>) target, targetType, copyOptions);
} else {
copier = new BeanToBeanCopier<>(source, target, targetType, copyOptions);
}
}
this.copier = copier;
}
@Override
@SuppressWarnings("unchecked")
public T copy() {
if (null != this.source) {
if (this.source instanceof ValueProvider) {
// 目标只支持Bean
valueProviderToBean((ValueProvider<String>) this.source, this.dest);
} else if (this.source instanceof DynaBean) {
// 目标只支持Bean
valueProviderToBean(new DynaBeanValueProvider((DynaBean) this.source, copyOptions.ignoreError), this.dest);
} else if (this.source instanceof Map) {
if (this.dest instanceof Map) {
mapToMap((Map<?, ?>) this.source, (Map<?, ?>) this.dest);
} else {
mapToBean((Map<?, ?>) this.source, this.dest);
}
} else {
if (this.dest instanceof Map) {
beanToMap(this.source, (Map<?, ?>) this.dest);
} else {
beanToBean(this.source, this.dest);
}
}
}
return this.dest;
}
/**
* Bean和Bean之间属性拷贝
*
* @param providerBean 来源Bean
* @param destBean 目标Bean
*/
private void beanToBean(Object providerBean, Object destBean) {
valueProviderToBean(
new BeanValueProvider(providerBean, this.copyOptions.ignoreCase, this.copyOptions.ignoreError, this.copyOptions.fieldNameEditor), destBean);
}
/**
* Map转Bean属性拷贝
*
* @param map Map
* @param bean Bean
*/
private void mapToBean(Map<?, ?> map, Object bean) {
valueProviderToBean(
new MapValueProvider(map, this.copyOptions.ignoreCase, this.copyOptions.ignoreError, this.copyOptions.fieldNameEditor),
bean
);
}
/**
* Map转Map
*
* @param source 源Map
* @param targetMap 目标Map
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void mapToMap(Map source, Map targetMap) {
source.forEach((key, value)->{
final CopyOptions copyOptions = this.copyOptions;
final HashSet<String> ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
// issue#I4JQ1N@Gitee
// 非覆盖模式下如果目标值存在则跳过
if(false == copyOptions.override && null != targetMap.get(key)){
return;
}
if(key instanceof CharSequence){
if (CollUtil.contains(ignoreSet, key)) {
// 目标属性值被忽略或值提供者无此key时跳过
return;
}
// 对源key做映射映射后为null的忽略之
key = copyOptions.editFieldName(copyOptions.getMappedFieldName(key.toString(), false));
if(null == key){
return;
}
value = copyOptions.editFieldValue(key.toString(), value);
}
if ((null == value && copyOptions.ignoreNullValue) || source == value) {
// 当允许跳过空时跳过
//值不能为bean本身防止循环引用此类也跳过
return;
}
targetMap.put(key, value);
});
}
/**
* 对象转Map
*
* @param bean bean对象
* @param targetMap 目标的Map
* @since 4.1.22
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void beanToMap(Object bean, Map targetMap) {
final CopyOptions copyOptions = this.copyOptions;
final HashSet<String> ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
BeanUtil.descForEach(bean.getClass(), (prop)->{
if(false == prop.isReadable(copyOptions.transientSupport)){
// 忽略的属性跳过之
return;
}
String key = prop.getFieldName();
if (CollUtil.contains(ignoreSet, key)) {
// 目标属性值被忽略或值提供者无此key时跳过
return;
}
// 对key做映射映射后为null的忽略之
key = copyOptions.editFieldName(copyOptions.getMappedFieldName(key, false));
if(null == key){
return;
}
// issue#I4JQ1N@Gitee
// 非覆盖模式下如果目标值存在则跳过
if(false == copyOptions.override && null != targetMap.get(key)){
return;
}
Object value;
try {
value = prop.getValue(bean);
} catch (Exception e) {
if (copyOptions.ignoreError) {
return;// 忽略反射失败
} else {
throw new BeanException(e, "Get value of [{}] error!", prop.getFieldName());
}
}
if(null != copyOptions.propertiesFilter && false == copyOptions.propertiesFilter.test(prop.getField(), value)) {
return;
}
// since 5.7.15
value = copyOptions.editFieldValue(key, value);
if ((null == value && copyOptions.ignoreNullValue) || bean == value) {
// 当允许跳过空时跳过
//值不能为bean本身防止循环引用此类也跳过
return;
}
targetMap.put(key, value);
});
}
/**
* 值提供器转Bean<br>
* 此方法通过遍历目标Bean的字段从ValueProvider查找对应值
*
* @param valueProvider 值提供器
* @param bean Bean
*/
private void valueProviderToBean(ValueProvider<String> valueProvider, Object bean) {
if (null == valueProvider) {
return;
}
final CopyOptions copyOptions = this.copyOptions;
Class<?> actualEditable = bean.getClass();
if (null != copyOptions.editable) {
// 检查限制类是否为target的父类或接口
if (false == copyOptions.editable.isInstance(bean)) {
throw new IllegalArgumentException(StrUtil.format("Target class [{}] not assignable to Editable class [{}]", bean.getClass().getName(), copyOptions.editable.getName()));
}
actualEditable = copyOptions.editable;
}
final HashSet<String> ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
// 遍历目标bean的所有属性
BeanUtil.descForEach(actualEditable, (prop)->{
if(false == prop.isWritable(this.copyOptions.transientSupport)){
// 字段不可写跳过之
return;
}
// 检查属性名
final String fieldName = prop.getFieldName();
if (CollUtil.contains(ignoreSet, fieldName)) {
// 目标属性值被忽略或值提供者无此key时跳过
return;
}
// 对key做映射映射后为null的忽略之
final String sourceKey = copyOptions.getMappedFieldName(fieldName, true);
if(null == sourceKey){
return;
}
if (false == valueProvider.containsKey(sourceKey)) {
// 无对应值可提供
return;
}
// 获取目标字段真实类型
final Type fieldType = TypeUtil.getActualType(this.destType ,prop.getFieldType());
// 获取属性值
Object value = valueProvider.value(sourceKey, fieldType);
if(null != copyOptions.propertiesFilter && false == copyOptions.propertiesFilter.test(prop.getField(), value)) {
return;
}
// since 5.7.15
value = copyOptions.editFieldValue(sourceKey, value);
if ((null == value && copyOptions.ignoreNullValue) || bean == value) {
// 当允许跳过空时跳过
// 值不能为bean本身防止循环引用
return;
}
prop.setValue(bean, value, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);
});
return copier.copy();
}
}

View File

@ -0,0 +1,86 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Bean属性拷贝到Bean中的拷贝器
*
* @param <S> 源Bean类型
* @param <T> 目标Bean类型
* @since 5.8.0
*/
public class BeanToBeanCopier<S, T> extends AbsCopier<S, T> {
/**
* 目标的类型用于泛型类注入
*/
private final Type targetType;
/**
* 构造
*
* @param source 来源Map
* @param target 目标Bean对象
* @param targetType 目标泛型类型
* @param copyOptions 拷贝选项
*/
public BeanToBeanCopier(S source, T target, Type targetType, CopyOptions copyOptions) {
super(source, target, copyOptions);
this.targetType = targetType;
}
@Override
public T copy() {
Class<?> actualEditable = target.getClass();
if (null != copyOptions.editable) {
// 检查限制类是否为target的父类或接口
Assert.isTrue(copyOptions.editable.isInstance(target),
"Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName());
actualEditable = copyOptions.editable;
}
final Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);
final Map<String, PropDesc> sourcePropDescMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(copyOptions.ignoreCase);
sourcePropDescMap.forEach((sFieldName, sDesc) -> {
if (null == sFieldName || false == sDesc.isReadable(copyOptions.transientSupport)) {
// 字段空或不可读跳过
return;
}
sFieldName = copyOptions.editFieldName(sFieldName);
// 对key做转换转换后为null的跳过
if (null == sFieldName) {
return;
}
// 检查目标字段可写性
final PropDesc tDesc = targetPropDescMap.get(sFieldName);
if (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {
// 字段不可写跳过之
return;
}
// 检查源对象属性是否过滤属性
Object sValue = sDesc.getValue(this.source);
if (false == copyOptions.testPropertyFilter(sDesc.getField(), sValue)) {
return;
}
// 获取目标字段真实类型并转换源值
final Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType());
sValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError);
sValue = copyOptions.editFieldValue(sFieldName, sValue);
// 目标赋值
tDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);
});
return this.target;
}
}

View File

@ -0,0 +1,82 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Bean属性拷贝到Map中的拷贝器
*
* @since 5.8.0
*/
@SuppressWarnings("rawtypes")
public class BeanToMapCopier extends AbsCopier<Object, Map> {
/**
* 目标的Map类型用于泛型类注入
*/
private final Type targetType;
/**
* 构造
*
* @param source 来源Map
* @param target 目标Bean对象
* @param targetType 目标泛型类型
* @param copyOptions 拷贝选项
*/
public BeanToMapCopier(Object source, Map target, Type targetType, CopyOptions copyOptions) {
super(source, target, copyOptions);
this.targetType = targetType;
}
@Override
public Map copy() {
Class<?> actualEditable = source.getClass();
if (null != copyOptions.editable) {
// 检查限制类是否为target的父类或接口
Assert.isTrue(copyOptions.editable.isInstance(source),
"Source class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName());
actualEditable = copyOptions.editable;
}
final Map<String, PropDesc> sourcePropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);
sourcePropDescMap.forEach((sFieldName, sDesc) -> {
if (null == sFieldName || false == sDesc.isReadable(copyOptions.transientSupport)) {
// 字段空或不可读跳过
return;
}
sFieldName = copyOptions.editFieldName(sFieldName);
// 对key做转换转换后为null的跳过
if (null == sFieldName) {
return;
}
// 检查源对象属性是否过滤属性
Object sValue = sDesc.getValue(this.source);
if (false == copyOptions.testPropertyFilter(sDesc.getField(), sValue)) {
return;
}
// 获取目标值真实类型并转换源值
final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType);
if(null != typeArguments){
sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);
sValue = copyOptions.editFieldValue(sFieldName, sValue);
}
// 目标赋值
if(null != sValue || false == copyOptions.ignoreNullValue){
//noinspection unchecked
target.put(sFieldName, sValue);
}
});
return this.target;
}
}

View File

@ -1,7 +1,7 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.map.BiMap;
import cn.hutool.core.util.ArrayUtil;
import java.io.Serializable;
import java.lang.reflect.Field;
@ -22,7 +22,8 @@ public class CopyOptions implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 限制的类或接口必须为目标对象的实现接口或父类用于限制拷贝的属性例如一个类我只想复制其父类的一些属性就可以将editable设置为父类
* 限制的类或接口必须为目标对象的实现接口或父类用于限制拷贝的属性例如一个类我只想复制其父类的一些属性就可以将editable设置为父类<br>
* 如果目标对象是Map源对象是Bean则作用于源对象上
*/
protected Class<?> editable;
/**
@ -30,13 +31,10 @@ public class CopyOptions implements Serializable {
*/
protected boolean ignoreNullValue;
/**
* 属性过滤器断言通过的属性才会被复制
* 属性过滤器断言通过的属性才会被复制<br>
* 断言参数中Field为源对象的字段对象,如果源对象为Map使用目标对象Object为源对象的对应值
*/
protected BiPredicate<Field, Object> propertiesFilter;
/**
* 忽略的目标对象中属性列表设置一个属性列表不拷贝这些属性值
*/
protected String[] ignoreProperties;
private BiPredicate<Field, Object> propertiesFilter;
/**
* 是否忽略字段注入错误
*/
@ -46,13 +44,10 @@ public class CopyOptions implements Serializable {
*/
protected boolean ignoreCase;
/**
* 拷贝属性的字段映射用于不同的属性之前拷贝做对应表用
* 字段属性编辑器用于自定义属性转换规则例如驼峰转下划线等<br>
* 规则为{@link Editor#edit(Object)}属性为源对象的字段名称或key返回值为目标对象的字段名称或key
*/
protected BiMap<String, String> fieldMapping;
/**
* 字段属性编辑器用于自定义属性转换规则例如驼峰转下划线等
*/
protected Editor<String> fieldNameEditor;
private Editor<String> fieldNameEditor;
/**
* 字段属性值编辑器用于自定义属性值转换规则例如null转""
*/
@ -66,6 +61,7 @@ public class CopyOptions implements Serializable {
*/
protected boolean override = true;
//region create
/**
* 创建拷贝选项
*
@ -86,6 +82,7 @@ public class CopyOptions implements Serializable {
public static CopyOptions create(Class<?> editable, boolean ignoreNullValue, String... ignoreProperties) {
return new CopyOptions(editable, ignoreNullValue, ignoreProperties);
}
//endregion
/**
* 构造拷贝选项
@ -104,7 +101,7 @@ public class CopyOptions implements Serializable {
this.propertiesFilter = (f, v) -> true;
this.editable = editable;
this.ignoreNullValue = ignoreNullValue;
this.ignoreProperties = ignoreProperties;
this.setIgnoreProperties(ignoreProperties);
}
/**
@ -140,7 +137,8 @@ public class CopyOptions implements Serializable {
}
/**
* 属性过滤器断言通过的属性才会被复制
* 属性过滤器断言通过的属性才会被复制<br>
* {@link BiPredicate#test(Object, Object)}返回{@code true}则属性通过{@code false}不通过抛弃之
*
* @param propertiesFilter 属性过滤器
* @return CopyOptions
@ -157,8 +155,7 @@ public class CopyOptions implements Serializable {
* @return CopyOptions
*/
public CopyOptions setIgnoreProperties(String... ignoreProperties) {
this.ignoreProperties = ignoreProperties;
return this;
return setPropertiesFilter((field, o) -> false == ArrayUtil.contains(ignoreProperties, field.getName()));
}
/**
@ -210,8 +207,7 @@ public class CopyOptions implements Serializable {
* @return CopyOptions
*/
public CopyOptions setFieldMapping(Map<String, String> fieldMapping) {
this.fieldMapping = new BiMap<>(fieldMapping);
return this;
return setFieldNameEditor((key-> fieldMapping.getOrDefault(key, key)));
}
/**
@ -253,18 +249,6 @@ public class CopyOptions implements Serializable {
this.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue;
}
/**
* 是否支持transient关键字修饰和@Transient注解如果支持被修饰的字段或方法对应的字段将被忽略
*
* @return 是否支持
* @since 5.4.2
* @deprecated 无需此方法内部使用直接调用属性
*/
@Deprecated
public boolean isTransientSupport() {
return this.transientSupport;
}
/**
* 设置是否支持transient关键字修饰和@Transient注解如果支持被修饰的字段或方法对应的字段将被忽略
*
@ -289,25 +273,6 @@ public class CopyOptions implements Serializable {
return this;
}
/**
* 获得映射后的字段名<br>
* 当非反向则根据源字段名获取目标字段名反之根据目标字段名获取源字段名
*
* @param fieldName 字段名
* @param reversed 是否反向映射
* @return 映射后的字段名
*/
protected String getMappedFieldName(String fieldName, boolean reversed) {
final BiMap<String, String> fieldMapping = this.fieldMapping;
if(null != fieldMapping){
final String mappingName = reversed ? fieldMapping.getKey(fieldName) : fieldMapping.get(fieldName);
if(null != mappingName){
return mappingName;
}
}
return fieldName;
}
/**
* 转换字段名为编辑后的字段名
*
@ -318,4 +283,15 @@ public class CopyOptions implements Serializable {
protected String editFieldName(String fieldName) {
return (null != this.fieldNameEditor) ? this.fieldNameEditor.edit(fieldName) : fieldName;
}
/**
* 测试是否保留字段{@code true}保留{@code false}不保留
*
* @param field 字段
* @param value
* @return 是否保留
*/
protected boolean testPropertyFilter(Field field, Object value) {
return null == this.propertiesFilter || this.propertiesFilter.test(field, value);
}
}

View File

@ -0,0 +1,125 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.CaseInsensitiveMap;
import cn.hutool.core.map.MapWrapper;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Map属性拷贝到Bean中的拷贝器
*
* @param <T> 目标Bean类型
* @since 5.8.0
*/
public class MapToBeanCopier<T> extends AbsCopier<Map<?, ?>, T> {
/**
* 目标的类型用于泛型类注入
*/
private final Type targetType;
/**
* 构造
*
* @param source 来源Map
* @param target 目标Bean对象
* @param targetType 目标泛型类型
* @param copyOptions 拷贝选项
*/
public MapToBeanCopier(Map<?, ?> source, T target, Type targetType, CopyOptions copyOptions) {
super(source, target, copyOptions);
// 针对MapWrapper特殊处理提供的Map包装了忽略大小写的Map则默认转Bean的时候也忽略大小写如JSONObject
if(source instanceof MapWrapper){
final Map<?, ?> raw = ((MapWrapper<?, ?>) source).getRaw();
if(raw instanceof CaseInsensitiveMap){
copyOptions.setIgnoreCase(true);
}
}
this.targetType = targetType;
}
@Override
public T copy() {
Class<?> actualEditable = target.getClass();
if (null != copyOptions.editable) {
// 检查限制类是否为target的父类或接口
Assert.isTrue(copyOptions.editable.isInstance(target),
"Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName());
actualEditable = copyOptions.editable;
}
final Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);
this.source.forEach((sKey, sValue) -> {
if (null == sKey) {
return;
}
String sKeyStr = copyOptions.editFieldName(sKey.toString());
// 对key做转换转换后为null的跳过
if (null == sKeyStr) {
return;
}
// 检查目标字段可写性
PropDesc tDesc = findPropDesc(targetPropDescMap, sKeyStr);
if (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {
// 字段不可写跳过之
return;
}
sKeyStr = tDesc.getFieldName();
// 检查目标是否过滤属性
if (false == copyOptions.testPropertyFilter(tDesc.getField(), sValue)) {
return;
}
// 获取目标字段真实类型并转换源值
final Type fieldType = TypeUtil.getActualType(this.targetType, tDesc.getFieldType());
Object newValue = Convert.convertWithCheck(fieldType, sValue, null, this.copyOptions.ignoreError);
newValue = copyOptions.editFieldValue(sKeyStr, newValue);
// 目标赋值
tDesc.setValue(this.target, newValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);
});
return this.target;
}
/**
* 查找Map对应Bean的名称<br>
* 尝试原名称转驼峰名称isXxx去掉is的名称
*
* @param targetPropDescMap 目标bean的属性描述Map
* @param sKeyStr 键或字段名
* @return {@link PropDesc}
*/
private PropDesc findPropDesc(Map<String, PropDesc> targetPropDescMap, String sKeyStr){
PropDesc propDesc = targetPropDescMap.get(sKeyStr);
if(null != propDesc){
return propDesc;
}
// 转驼峰尝试查找
sKeyStr = StrUtil.toCamelCase(sKeyStr);
propDesc = targetPropDescMap.get(sKeyStr);
if(null != propDesc){
return propDesc;
}
// boolean类型参数名转换尝试查找
if(sKeyStr.startsWith("is")){
sKeyStr = StrUtil.removePreAndLowerFirst(sKeyStr, 2);
propDesc = targetPropDescMap.get(sKeyStr);
return propDesc;
}
return null;
}
}

View File

@ -0,0 +1,65 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Map属性拷贝到Map中的拷贝器
*
* @since 5.8.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class MapToMapCopier extends AbsCopier<Map, Map> {
/**
* 目标的类型用于泛型类注入
*/
private final Type targetType;
/**
* 构造
*
* @param source 来源Map
* @param target 目标Bean对象
* @param targetType 目标泛型类型
* @param copyOptions 拷贝选项
*/
public MapToMapCopier(Map source, Map target, Type targetType, CopyOptions copyOptions) {
super(source, target, copyOptions);
this.targetType = targetType;
}
@Override
public Map copy() {
this.source.forEach((sKey, sValue) -> {
if (null == sKey) {
return;
}
final String sKeyStr = copyOptions.editFieldName(sKey.toString());
// 对key做转换转换后为null的跳过
if (null == sKeyStr) {
return;
}
final Object targetValue = target.get(sKeyStr);
// 非覆盖模式下如果目标值存在则跳过
if (false == copyOptions.override && null != targetValue) {
return;
}
// 获取目标值真实类型并转换源值
final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType);
if(null != typeArguments){
sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);
sValue = copyOptions.editFieldValue(sKeyStr, sValue);
}
// 目标赋值
target.put(sKeyStr, sValue);
});
return this.target;
}
}

View File

@ -0,0 +1,74 @@
package cn.hutool.core.bean.copier;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* {@link ValueProvider}属性拷贝到Bean中的拷贝器
*
* @param <T> 目标Bean类型
* @since 5.8.0
*/
public class ValueProviderToBeanCopier<T> extends AbsCopier<ValueProvider<String>, T> {
/**
* 目标的类型用于泛型类注入
*/
private final Type targetType;
/**
* 构造
*
* @param source 来源Map
* @param target 目标Bean对象
* @param targetType 目标泛型类型
* @param copyOptions 拷贝选项
*/
public ValueProviderToBeanCopier(ValueProvider<String> source, T target, Type targetType, CopyOptions copyOptions) {
super(source, target, copyOptions);
this.targetType = targetType;
}
@Override
public T copy() {
Class<?> actualEditable = target.getClass();
if (null != copyOptions.editable) {
// 检查限制类是否为target的父类或接口
Assert.isTrue(copyOptions.editable.isInstance(target),
"Target class [{}] not assignable to Editable class [{}]", actualEditable.getName(), copyOptions.editable.getName());
actualEditable = copyOptions.editable;
}
final Map<String, PropDesc> targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase);
targetPropDescMap.forEach((tFieldName, tDesc) -> {
if (null == tFieldName) {
return;
}
// 检查目标字段可写性
if (null == tDesc || false == tDesc.isWritable(this.copyOptions.transientSupport)) {
// 字段不可写跳过之
return;
}
// 获取目标字段真实类型
final Type fieldType = TypeUtil.getActualType(this.targetType ,tDesc.getFieldType());
// 检查目标对象属性是否过滤属性
Object sValue = source.value(tFieldName, fieldType);
if (false == copyOptions.testPropertyFilter(tDesc.getField(), sValue)) {
return;
}
sValue = copyOptions.editFieldValue(tFieldName, sValue);
// 目标赋值
tDesc.setValue(this.target, sValue, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);
});
return this.target;
}
}

View File

@ -1,96 +0,0 @@
package cn.hutool.core.bean.copier.provider;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.bean.copier.ValueProvider;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.map.FuncKeyMap;
import cn.hutool.core.util.StrUtil;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
/**
* Bean的值提供者
*
* @author looly
*/
public class BeanValueProvider implements ValueProvider<String> {
private final Object source;
private final boolean ignoreError;
final Map<String, PropDesc> sourcePdMap;
/**
* 构造
*
* @param bean Bean
* @param ignoreCase 是否忽略字段大小写
* @param ignoreError 是否忽略字段值读取错误
*/
public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError) {
this(bean, ignoreCase, ignoreError, null);
}
/**
* 构造
*
* @param bean Bean
* @param ignoreCase 是否忽略字段大小写
* @param ignoreError 是否忽略字段值读取错误
*/
public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError, Editor<String> keyEditor) {
this.source = bean;
this.ignoreError = ignoreError;
final Map<String, PropDesc> sourcePdMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(ignoreCase);
// issue#2202@Github
// 如果用户定义了键编辑器则提供的map中的数据必须全部转换key
this.sourcePdMap = new FuncKeyMap<>(new HashMap<>(sourcePdMap.size(), 1), (key) -> {
if (ignoreCase && key instanceof CharSequence) {
key = key.toString().toLowerCase();
}
if (null != keyEditor) {
key = keyEditor.edit(key.toString());
}
return key.toString();
});
this.sourcePdMap.putAll(sourcePdMap);
}
@Override
public Object value(String key, Type valueType) {
final PropDesc sourcePd = getPropDesc(key, valueType);
Object result = null;
if (null != sourcePd) {
result = sourcePd.getValue(this.source, valueType, this.ignoreError);
}
return result;
}
@Override
public boolean containsKey(String key) {
final PropDesc sourcePd = getPropDesc(key, null);
// 字段描述不存在或忽略读的情况下表示不存在
return null != sourcePd && sourcePd.isReadable(false);
}
/**
* 获得属性描述
*
* @param key 字段名
* @param valueType 值类型用于判断是否为Boolean可以为null
* @return 属性描述
*/
private PropDesc getPropDesc(String key, Type valueType) {
PropDesc sourcePd = sourcePdMap.get(key);
if (null == sourcePd && (null == valueType || Boolean.class == valueType || boolean.class == valueType)) {
//boolean类型字段字段名支持两种方式
sourcePd = sourcePdMap.get(StrUtil.upperFirstAndAddPre(key, "is"));
}
return sourcePd;
}
}

View File

@ -1,131 +0,0 @@
package cn.hutool.core.bean.copier.provider;
import cn.hutool.core.bean.copier.ValueProvider;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.map.FuncKeyMap;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Map值提供者支持驼峰和下划线的key兼容<br>
* 假设目标属性为firstName则Map中以下形式的值都可以对应
* <ul>
* <li>firstName</li>
* <li>first_name</li>
* <li>isFirstName如果为Boolean或boolean类型的值</li>
* <li>is_first_name如果为Boolean或boolean类型的值</li>
* </ul>
*
* @author looly
*/
public class MapValueProvider implements ValueProvider<String> {
@SuppressWarnings("rawtypes")
private final Map map;
private final boolean ignoreError;
/**
* 构造
*
* @param map Map
* @param ignoreCase 是否忽略key的大小写
*/
public MapValueProvider(Map<?, ?> map, boolean ignoreCase) {
this(map, ignoreCase, false);
}
/**
* 构造
*
* @param map Map
* @param ignoreCase 是否忽略key的大小写
* @param ignoreError 是否忽略错误
* @since 5.3.2
*/
public MapValueProvider(Map<?, ?> map, boolean ignoreCase, boolean ignoreError) {
this(map, ignoreCase, ignoreError, null);
}
/**
* 构造
*
* @param map Map
* @param ignoreCase 是否忽略key的大小写
* @param ignoreError 是否忽略错误
* @param keyEditor 自定义键编辑器
* @since 5.7.23
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public MapValueProvider(Map map, boolean ignoreCase, boolean ignoreError, Editor<String> keyEditor) {
// issue#2202@Github
// 如果用户定义了键编辑器则提供的map中的数据必须全部转换key
this.map = new FuncKeyMap(ObjectUtil.clone(map), (key)->{
if(ignoreCase && key instanceof CharSequence){
key = key.toString().toLowerCase();
}
if(null != keyEditor){
key = keyEditor.edit(key.toString());
}
return key;
});
this.map.clear();
this.map.putAll(map);
this.ignoreError = ignoreError;
}
@Override
public Object value(String key, Type valueType) {
final String key1 = getKey(key, valueType);
if (null == key1) {
return null;
}
return Convert.convertWithCheck(valueType, map.get(key1), null, this.ignoreError);
}
@Override
public boolean containsKey(String key) {
return null != getKey(key, null);
}
/**
* 获得map中可能包含的key,不包含返回null
*
* @param key map中可能包含的key
* @param valueType 值类型用于判断是否为Boolean可以为null
* @return map中可能包含的key
*/
private String getKey(String key, Type valueType) {
if (map.containsKey(key)) {
return key;
}
//检查下划线模式
String customKey = StrUtil.toUnderlineCase(key);
if (map.containsKey(customKey)) {
return customKey;
}
//检查boolean类型
if (null == valueType || Boolean.class == valueType || boolean.class == valueType) {
//boolean类型字段字段名支持两种方式
customKey = StrUtil.upperFirstAndAddPre(key, "is");
if (map.containsKey(customKey)) {
return customKey;
}
//检查下划线模式
customKey = StrUtil.toUnderlineCase(customKey);
if (map.containsKey(customKey)) {
return customKey;
}
}
return null;
}
}

View File

@ -9,7 +9,6 @@ import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
/**
@ -86,13 +85,11 @@ public class MapConverter extends AbstractConverter<Map<?, ?>> {
*/
private void convertMapToMap(Map<?, ?> srcMap, Map<Object, Object> targetMap) {
final ConverterRegistry convert = ConverterRegistry.getInstance();
Object key;
Object value;
for (Entry<?, ?> entry : srcMap.entrySet()) {
key = TypeUtil.isUnknown(this.keyType) ? entry.getKey() : convert.convert(this.keyType, entry.getKey());
value = TypeUtil.isUnknown(this.valueType) ? entry.getValue() : convert.convert(this.valueType, entry.getValue());
srcMap.forEach((key, value)->{
key = TypeUtil.isUnknown(this.keyType) ? key : convert.convert(this.keyType, key);
value = TypeUtil.isUnknown(this.valueType) ? value : convert.convert(this.valueType, value);
targetMap.put(key, value);
}
});
}
@Override

View File

@ -1278,7 +1278,7 @@ public class XmlUtil {
*
* @param bean Bean对象
* @param namespace 命名空间可以为null
* @param ignoreNull 时候忽略值为{@code null}的属性
* @param ignoreNull 忽略值为{@code null}的属性
* @return XML
* @see JAXBUtil#beanToXml(Object)
* @since 5.7.10

View File

@ -73,8 +73,8 @@ public class BeanUtilTest {
}, CopyOptions.create());
Assert.assertEquals(person.getName(), "张三");
Assert.assertEquals(person.getAge(), 18);
Assert.assertEquals("张三", person.getName());
Assert.assertEquals(18, person.getAge());
}
@Test

View File

@ -4,8 +4,25 @@ import lombok.Data;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
public class BeanCopierTest {
@Test
public void beanToMapIgnoreNullTest() {
final A a = new A();
HashMap<Object, Object> map = BeanCopier.create(a, new HashMap<>(), CopyOptions.create()).copy();
Assert.assertEquals(1, map.size());
Assert.assertTrue(map.containsKey("value"));
Assert.assertNull(map.get("value"));
// 忽略null的情况下空字段不写入map
map = BeanCopier.create(a, new HashMap<>(), CopyOptions.create().ignoreNullValue()).copy();
Assert.assertFalse(map.containsKey("value"));
Assert.assertEquals(0, map.size());
}
/**
* 测试在非覆盖模式下目标对象有值则不覆盖
*/

View File

@ -1,6 +1,5 @@
package cn.hutool.core.collection;
import cn.hutool.core.lang.Console;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.Assert;
@ -20,8 +19,6 @@ public class UniqueKeySetTest {
// 后两个ID重复
Assert.assertEquals(2, set.size());
set.forEach(Console::log);
}
@Data