From 95854cddb126efc2a80ea7c2c709f05c3493d230 Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 26 Nov 2021 21:31:55 +0800 Subject: [PATCH] add options --- CHANGELOG.md | 1 + .../java/cn/hutool/core/bean/PropDesc.java | 39 ++++++++++--- .../hutool/core/bean/copier/BeanCopier.java | 55 ++++++++++++++++--- .../hutool/core/bean/copier/CopyOptions.java | 20 ++++++- .../core/bean/copier/BeanCopierTest.java | 50 +++++++++++++++++ 5 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9ad47d9..4887c7fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * 【core 】 增加CharSequenceUtil.subPreGbk重载(issue#I4JO2E@Gitee) * 【core 】 ReflectUtil.getMethod排除桥接方法(pr#1965@Github) * 【http 】 completeFileNameFromHeader在使用path为路径时,自动解码(issue#I4K0FS@Gitee) +* 【core 】 CopyOptions增加override配置(issue#I4JQ1N@Gitee) * ### 🐞Bug修复 * 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java index 8fc238457..c9ea67472 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java @@ -121,18 +121,19 @@ public class PropDesc { /** * 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值) + * * @param checkTransient 是否检查Transient关键字或注解 * @return 是否可读 * @since 5.4.2 */ - public boolean isReadable(boolean checkTransient){ + public boolean isReadable(boolean checkTransient) { // 检查是否有getter方法或是否为public修饰 - if(null == this.getter && false == ModifierUtil.isPublic(this.field)){ + if (null == this.getter && false == ModifierUtil.isPublic(this.field)) { return false; } // 检查transient关键字和@Transient注解 - if(checkTransient && isTransientForGet()){ + if (checkTransient && isTransientForGet()) { return false; } @@ -164,7 +165,7 @@ public class PropDesc { * 首先调用字段对应的Getter方法获取值,如果Getter方法不存在,则判断字段如果为public,则直接获取字段值 * * @param bean Bean对象 - * @param targetType 返回属性值需要转换的类型,null表示不转换 + * @param targetType 返回属性值需要转换的类型,null表示不转换 * @param ignoreError 是否忽略错误,包括转换错误和注入错误 * @return this * @since 5.4.2 @@ -190,18 +191,19 @@ public class PropDesc { /** * 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值) + * * @param checkTransient 是否检查Transient关键字或注解 * @return 是否可读 * @since 5.4.2 */ - public boolean isWritable(boolean checkTransient){ + public boolean isWritable(boolean checkTransient) { // 检查是否有getter方法或是否为public修饰 - if(null == this.setter && false == ModifierUtil.isPublic(this.field)){ + if (null == this.setter && false == ModifierUtil.isPublic(this.field)) { return false; } // 检查transient关键字和@Transient注解 - if(checkTransient && isTransientForSet()){ + if (checkTransient && isTransientForSet()) { return false; } @@ -239,7 +241,28 @@ public class PropDesc { * @since 5.4.2 */ public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError) { - if (ignoreNull && null == value) { + return setValue(bean, value, ignoreNull, ignoreError, true); + } + + /** + * 设置属性值,可以自动转换字段类型为目标类型 + * + * @param bean Bean对象 + * @param value 属性值,可以为任意类型 + * @param ignoreNull 是否忽略{@code null}值,true表示忽略 + * @param ignoreError 是否忽略错误,包括转换错误和注入错误 + * @param override 是否覆盖目标值,如果不覆盖,会先读取bean的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + * @return this + * @since 5.7.17 + */ + public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError, boolean override) { + if (null == value && ignoreNull) { + return this; + } + + // issue#I4JQ1N@Gitee + // 非覆盖模式下,如果目标值存在,则跳过 + if (false == override && null != getValue(bean)) { return this; } 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 bcfae58a1..ec0a01d22 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 @@ -140,13 +140,44 @@ public class BeanCopier implements Copier, Serializable { * Map转Map * * @param source 源Map - * @param dest 目标Map + * @param targetMap 目标Map */ @SuppressWarnings({ "unchecked", "rawtypes" }) - private void mapToMap(Map source, Map dest) { - if (null != dest && null != source) { - dest.putAll(source); - } + 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); + }); } /** @@ -158,11 +189,11 @@ public class BeanCopier implements Copier, Serializable { */ @SuppressWarnings({ "rawtypes", "unchecked" }) private void beanToMap(Object bean, Map targetMap) { - final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null; 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.isTransientSupport())){ + if(false == prop.isReadable(copyOptions.transientSupport)){ // 忽略的属性跳过之 return; } @@ -178,6 +209,12 @@ public class BeanCopier implements Copier, Serializable { return; } + // issue#I4JQ1N@Gitee + // 非覆盖模式下,如果目标值存在,则跳过 + if(false == copyOptions.override && null != targetMap.get(key)){ + return; + } + Object value; try { value = prop.getValue(bean); @@ -230,7 +267,7 @@ public class BeanCopier implements Copier, Serializable { // 遍历目标bean的所有属性 BeanUtil.descForEach(actualEditable, (prop)->{ - if(false == prop.isWritable(this.copyOptions.isTransientSupport())){ + if(false == prop.isWritable(this.copyOptions.transientSupport)){ // 字段不可写,跳过之 return; } @@ -270,7 +307,7 @@ public class BeanCopier implements Copier, Serializable { return; } - prop.setValue(bean, value, copyOptions.ignoreNullValue, copyOptions.ignoreError); + prop.setValue(bean, value, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override); }); } } 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 b0e35315f..92ab2937a 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 @@ -65,7 +65,11 @@ public class CopyOptions implements Serializable { /** * 是否支持transient关键字修饰和@Transient注解,如果支持,被修饰的字段或方法对应的字段将被忽略。 */ - private boolean transientSupport = true; + protected boolean transientSupport = true; + /** + * 是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + */ + protected boolean override = true; /** * 创建拷贝选项 @@ -259,7 +263,9 @@ public class CopyOptions implements Serializable { * * @return 是否支持 * @since 5.4.2 + * @deprecated 无需此方法,内部使用直接调用属性 */ + @Deprecated public boolean isTransientSupport() { return this.transientSupport; } @@ -276,6 +282,18 @@ public class CopyOptions implements Serializable { return this; } + /** + * 设置是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写 + * + * @param override 是否覆盖目标值 + * @return this + * @since 5.7.17 + */ + public CopyOptions setOverride(boolean override) { + this.override = override; + return this; + } + /** * 获得映射后的字段名
* 当非反向,则根据源字段名获取目标字段名,反之根据目标字段名获取源字段名。 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 new file mode 100644 index 000000000..0c314d18b --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java @@ -0,0 +1,50 @@ +package cn.hutool.core.bean.copier; + +import lombok.Data; +import org.junit.Assert; +import org.junit.Test; + +public class BeanCopierTest { + + /** + * 测试在非覆盖模式下,目标对象有值则不覆盖 + */ + @Test + public void beanToBeanNotOverrideTest() { + final A a = new A(); + a.setValue("123"); + final B b = new B(); + b.setValue("abc"); + + final BeanCopier copier = BeanCopier.create(a, b, CopyOptions.create().setOverride(false)); + copier.copy(); + + Assert.assertEquals("abc", b.getValue()); + } + + /** + * 测试在覆盖模式下,目标对象值被覆盖 + */ + @Test + public void beanToBeanOverrideTest() { + final A a = new A(); + a.setValue("123"); + final B b = new B(); + b.setValue("abc"); + + final BeanCopier copier = BeanCopier.create(a, b, CopyOptions.create()); + copier.copy(); + + Assert.assertEquals("123", b.getValue()); + } + + @Data + private static class A { + private String value; + } + + @Data + private static class B { + private String value; + } +}