From b848efb7b4671e0a5c56b1e6806de8f491af4197 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 19 Mar 2022 21:00:36 +0800 Subject: [PATCH] add xxxCopier --- CHANGELOG.md | 13 +- .../cn/hutool/core/bean/copier/AbsCopier.java | 28 ++ .../hutool/core/bean/copier/BeanCopier.java | 285 ++---------------- .../core/bean/copier/BeanToBeanCopier.java | 86 ++++++ .../core/bean/copier/BeanToMapCopier.java | 82 +++++ .../hutool/core/bean/copier/CopyOptions.java | 78 ++--- .../core/bean/copier/MapToBeanCopier.java | 125 ++++++++ .../core/bean/copier/MapToMapCopier.java | 65 ++++ .../copier/ValueProviderToBeanCopier.java | 74 +++++ .../copier/provider/BeanValueProvider.java | 96 ------ .../copier/provider/MapValueProvider.java | 131 -------- .../core/convert/impl/MapConverter.java | 11 +- .../java/cn/hutool/core/util/XmlUtil.java | 2 +- .../cn/hutool/core/bean/BeanUtilTest.java | 4 +- .../core/bean/copier/BeanCopierTest.java | 17 ++ .../core/collection/UniqueKeySetTest.java | 3 - 16 files changed, 552 insertions(+), 548 deletions(-) create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java create mode 100755 hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java delete mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java delete mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d713b2d0f..1d1e55cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java new file mode 100755 index 000000000..19549100f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/AbsCopier.java @@ -0,0 +1,28 @@ +package cn.hutool.core.bean.copier; + +import cn.hutool.core.lang.copier.Copier; +import cn.hutool.core.util.ObjectUtil; + +/** + * 抽象的对象拷贝封装,提供来源对象、目标对象持有 + * + * @param 来源对象类型 + * @param 目标对象类型 + * @author looly + * @since 5.8.0 + */ +public abstract class AbsCopier implements Copier { + + 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); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java index 6e026f856..8d3fd418f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java @@ -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 implements Copier, 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 copier; /** * 创建BeanCopier * * @param 目标Bean类型 * @param source 来源对象,可以是Bean或者Map - * @param dest 目标Bean对象 + * @param target 目标Bean对象 * @param copyOptions 拷贝属性选项 * @return BeanCopier */ - public static BeanCopier create(Object source, T dest, CopyOptions copyOptions) { - return create(source, dest, dest.getClass(), copyOptions); + public static BeanCopier create(Object source, T target, CopyOptions copyOptions) { + return create(source, target, target.getClass(), copyOptions); } /** @@ -62,253 +44,48 @@ public class BeanCopier implements Copier, Serializable { * * @param 目标Bean类型 * @param source 来源对象,可以是Bean或者Map - * @param dest 目标Bean对象 + * @param target 目标Bean对象 * @param destType 目标的泛型类型,用于标注有泛型参数的Bean对象 * @param copyOptions 拷贝属性选项 * @return BeanCopier */ - public static BeanCopier create(Object source, T dest, Type destType, CopyOptions copyOptions) { - return new BeanCopier<>(source, dest, destType, copyOptions); + public static BeanCopier 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 copier; + if (source instanceof Map) { + if (target instanceof Map) { + //noinspection unchecked + copier = (Copier) 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) source, target, targetType, copyOptions); + } else { + if (target instanceof Map) { + //noinspection unchecked + copier = (Copier) 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) 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 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 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
- * 此方法通过遍历目标Bean的字段,从ValueProvider查找对应值 - * - * @param valueProvider 值提供器 - * @param bean Bean - */ - private void valueProviderToBean(ValueProvider 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 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(); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java new file mode 100755 index 000000000..c6abd1f9b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToBeanCopier.java @@ -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 源Bean类型 + * @param 目标Bean类型 + * @since 5.8.0 + */ +public class BeanToBeanCopier extends AbsCopier { + + /** + * 目标的类型(用于泛型类注入) + */ + 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 targetPropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(copyOptions.ignoreCase); + + final Map 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java new file mode 100755 index 000000000..52a4bf8a3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanToMapCopier.java @@ -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 { + + /** + * 目标的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 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java index e6a25eb92..198e1fdcf 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java @@ -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设置为父类
+ * 如果目标对象是Map,源对象是Bean,则作用于源对象上 */ protected Class editable; /** @@ -30,13 +31,10 @@ public class CopyOptions implements Serializable { */ protected boolean ignoreNullValue; /** - * 属性过滤器,断言通过的属性才会被复制 + * 属性过滤器,断言通过的属性才会被复制
+ * 断言参数中Field为源对象的字段对象,如果源对象为Map,使用目标对象,Object为源对象的对应值 */ - protected BiPredicate propertiesFilter; - /** - * 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 - */ - protected String[] ignoreProperties; + private BiPredicate propertiesFilter; /** * 是否忽略字段注入错误 */ @@ -46,13 +44,10 @@ public class CopyOptions implements Serializable { */ protected boolean ignoreCase; /** - * 拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 + * 字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等
+ * 规则为,{@link Editor#edit(Object)}属性为源对象的字段名称或key,返回值为目标对象的字段名称或key */ - protected BiMap fieldMapping; - /** - * 字段属性编辑器,用于自定义属性转换规则,例如驼峰转下划线等 - */ - protected Editor fieldNameEditor; + private Editor 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 { } /** - * 属性过滤器,断言通过的属性才会被复制 + * 属性过滤器,断言通过的属性才会被复制
+ * {@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 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; } - /** - * 获得映射后的字段名
- * 当非反向,则根据源字段名获取目标字段名,反之根据目标字段名获取源字段名。 - * - * @param fieldName 字段名 - * @param reversed 是否反向映射 - * @return 映射后的字段名 - */ - protected String getMappedFieldName(String fieldName, boolean reversed) { - final BiMap 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); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java new file mode 100755 index 000000000..43da5311a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToBeanCopier.java @@ -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 目标Bean类型 + * @since 5.8.0 + */ +public class MapToBeanCopier extends AbsCopier, 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 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的名称
+ * 尝试原名称、转驼峰名称、isXxx去掉is的名称 + * + * @param targetPropDescMap 目标bean的属性描述Map + * @param sKeyStr 键或字段名 + * @return {@link PropDesc} + */ + private PropDesc findPropDesc(Map 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java new file mode 100755 index 000000000..e5bb62bf9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/MapToMapCopier.java @@ -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 { + + /** + * 目标的类型(用于泛型类注入) + */ + 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java new file mode 100755 index 000000000..990a95bec --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProviderToBeanCopier.java @@ -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 目标Bean类型 + * @since 5.8.0 + */ +public class ValueProviderToBeanCopier extends AbsCopier, T> { + + /** + * 目标的类型(用于泛型类注入) + */ + private final Type targetType; + + /** + * 构造 + * + * @param source 来源Map + * @param target 目标Bean对象 + * @param targetType 目标泛型类型 + * @param copyOptions 拷贝选项 + */ + public ValueProviderToBeanCopier(ValueProvider 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 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java deleted file mode 100644 index a4fc982db..000000000 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java +++ /dev/null @@ -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 { - - private final Object source; - private final boolean ignoreError; - final Map 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 keyEditor) { - this.source = bean; - this.ignoreError = ignoreError; - final Map 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; - } -} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java deleted file mode 100644 index 775ab77fe..000000000 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java +++ /dev/null @@ -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兼容。
- * 假设目标属性为firstName,则Map中以下形式的值都可以对应: - *
    - *
  • firstName
  • - *
  • first_name
  • - *
  • isFirstName(如果为Boolean或boolean类型的值)
  • - *
  • is_first_name(如果为Boolean或boolean类型的值)
  • - *
- * - * @author looly - */ -public class MapValueProvider implements ValueProvider { - - @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 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; - } - -} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java index edb914ed4..b99a5d6b9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java @@ -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> { */ private void convertMapToMap(Map srcMap, Map 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 diff --git a/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java index 55f3130bb..a6ccc3e06 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java @@ -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 diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java index 147056670..5a51a5d18 100644 --- a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java @@ -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 diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java index 0c314d18b..369631104 100644 --- a/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java @@ -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 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()); + } + /** * 测试在非覆盖模式下,目标对象有值则不覆盖 */ diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java index 9dd45058f..04c198716 100644 --- a/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java @@ -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