This commit is contained in:
Looly 2023-12-30 01:00:01 +08:00
parent f628e975bc
commit 752a8ece52
8 changed files with 239 additions and 272 deletions

View File

@ -54,109 +54,6 @@ import java.util.stream.Collectors;
*/
public class BeanUtil {
/**
* 判断是否为可读的Bean对象判定方法是
*
* <pre>
* 1是否存在只有无参数的getXXX方法或者isXXX方法
* 2是否存在public类型的字段
* </pre>
*
* @param clazz 待测试类
* @return 是否为可读的Bean对象
* @see #hasGetter(Class)
* @see #hasPublicField(Class)
*/
public static boolean isReadableBean(final Class<?> clazz) {
if (null == clazz) {
return false;
}
return hasGetter(clazz) || hasPublicField(clazz);
}
/**
* 判断是否为可写Bean对象判定方法是
*
* <pre>
* 1是否存在只有一个参数的setXXX方法
* 2是否存在public类型的字段
* </pre>
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @see #hasSetter(Class)
* @see #hasPublicField(Class)
*/
public static boolean isWritableBean(final Class<?> clazz) {
if (null == clazz) {
return false;
}
return hasSetter(clazz) || hasPublicField(clazz);
}
/**
* 判断是否有Setter方法<br>
* 判定方法是否存在只有一个参数的setXXX方法
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @since 4.2.2
*/
public static boolean hasSetter(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Method method : clazz.getMethods()) {
if (method.getParameterCount() == 1 && method.getName().startsWith("set")) {
// 检测包含标准的setXXX方法即视为标准的JavaBean
return true;
}
}
}
return false;
}
/**
* 判断是否为Bean对象<br>
* 判定方法是否存在只有无参数的getXXX方法或者isXXX方法
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @since 4.2.2
*/
public static boolean hasGetter(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Method method : clazz.getMethods()) {
if (method.getParameterCount() == 0) {
final String name = method.getName();
if (name.startsWith("get") || name.startsWith("is")) {
if (!"getClass".equals(name)) {
return true;
}
}
}
}
}
return false;
}
/**
* 指定类中是否有public类型字段(static字段除外)
*
* @param clazz 待测试类
* @return 是否有public类型字段
* @since 5.1.0
*/
public static boolean hasPublicField(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Field field : clazz.getFields()) {
if (ModifierUtil.isPublic(field) && !ModifierUtil.isStatic(field)) {
//非static的public字段
return true;
}
}
}
return false;
}
/**
* 创建动态Bean
*
@ -200,8 +97,7 @@ public class BeanUtil {
getBeanDesc(clazz).getProps().forEach(action);
}
// --------------------------------------------------------------------------------------------------------- PropertyDescriptor
// region ----- getPropertyDescriptor
/**
* 获得Bean字段描述数组
*
@ -278,6 +174,7 @@ public class BeanUtil {
final Map<String, PropertyDescriptor> map = getPropertyDescriptorMap(clazz, ignoreCase);
return (null == map) ? null : map.get(fieldName);
}
// endregion
/**
* 获得字段值通过反射直接获得字段值并不调用getXXX方法<br>
@ -380,101 +277,7 @@ public class BeanUtil {
BeanPath.of(expression).setValue(bean, value);
}
// --------------------------------------------------------------------------------------------- mapToBean
/**
* Map转换为Bean对象
*
* @param <T> Bean类型
* @param map {@link Map}
* @param beanClass Bean Class
* @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格
* @param copyOptions 转Bean选项
* @return Bean
*/
public static <T> T mapToBean(final Map<?, ?> map, final Class<T> beanClass, final boolean isToCamelCase, final CopyOptions copyOptions) {
return fillBeanWithMap(map, ConstructorUtil.newInstanceIfPossible(beanClass), isToCamelCase, copyOptions);
}
// --------------------------------------------------------------------------------------------- fillBeanWithMap
/**
* 使用Map填充Bean对象
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param isIgnoreError 是否忽略注入错误
* @return Bean
*/
public static <T> T fillBeanWithMap(final Map<?, ?> map, final T bean, final boolean isIgnoreError) {
return fillBeanWithMap(map, bean, false, isIgnoreError);
}
/**
* 使用Map填充Bean对象可配置将下划线转换为驼峰
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param isToCamelCase 是否将下划线模式转换为驼峰模式
* @param isIgnoreError 是否忽略注入错误
* @return Bean
*/
public static <T> T fillBeanWithMap(final Map<?, ?> map, final T bean, final boolean isToCamelCase, final boolean isIgnoreError) {
return fillBeanWithMap(map, bean, isToCamelCase, CopyOptions.of().setIgnoreError(isIgnoreError));
}
/**
* 使用Map填充Bean对象忽略大小写
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param isIgnoreError 是否忽略注入错误
* @return Bean
*/
public static <T> T fillBeanWithMapIgnoreCase(final Map<?, ?> map, final T bean, final boolean isIgnoreError) {
return fillBeanWithMap(map, bean, CopyOptions.of().setIgnoreCase(true).setIgnoreError(isIgnoreError));
}
/**
* 使用Map填充Bean对象
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param copyOptions 属性复制选项 {@link CopyOptions}
* @return Bean
*/
public static <T> T fillBeanWithMap(final Map<?, ?> map, final T bean, final CopyOptions copyOptions) {
return fillBeanWithMap(map, bean, false, copyOptions);
}
/**
* 使用Map填充Bean对象
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param isToCamelCase 是否将Map中的下划线风格key转换为驼峰风格
* @param copyOptions 属性复制选项 {@link CopyOptions}
* @return Bean
* @since 3.3.1
*/
public static <T> T fillBeanWithMap(Map<?, ?> map, final T bean, final boolean isToCamelCase, final CopyOptions copyOptions) {
if (MapUtil.isEmpty(map)) {
return bean;
}
if (isToCamelCase) {
map = MapUtil.toCamelCaseMap(map);
}
copyProperties(map, bean, copyOptions);
return bean;
}
// --------------------------------------------------------------------------------------------- fillBean
// region ----- toBean
/**
* 对象或Map转Bean
*
@ -520,7 +323,9 @@ public class BeanUtil {
copyProperties(source, target, options);
return target;
}
// endregion
// region ----- fillBean
/**
* 填充Bean的核心方法
*
@ -537,8 +342,25 @@ public class BeanUtil {
return BeanCopier.of(valueProvider, bean, copyOptions).copy();
}
// --------------------------------------------------------------------------------------------- beanToMap
/**
* 使用Map填充Bean对象
*
* @param <T> Bean类型
* @param map Map
* @param bean Bean
* @param copyOptions 属性复制选项 {@link CopyOptions}
* @return Bean
*/
public static <T> T fillBeanWithMap(final Map<?, ?> map, final T bean, final CopyOptions copyOptions) {
if (MapUtil.isEmpty(map)) {
return bean;
}
return copyProperties(map, bean, copyOptions);
}
// endregion
// region ----- beanToMap
/**
* 将bean的部分属性转换成map<br>
* 可选拷贝哪些属性值默认是不忽略值为{@code null}的值的
@ -657,8 +479,9 @@ public class BeanUtil {
return BeanCopier.of(bean, targetMap, copyOptions).copy();
}
// endregion
// --------------------------------------------------------------------------------------------- copyProperties
// region ----- copyProperties
/**
* 按照Bean对象属性创建对应的Class对象并忽略某些属性
@ -727,6 +550,20 @@ public class BeanUtil {
return BeanCopier.of(source, target, ObjUtil.defaultIfNull(copyOptions, CopyOptions::of)).copy();
}
/**
* 复制集合中的Bean属性<br>
* 此方法遍历集合中每个Bean复制其属性后加入一个新的{@link List}
*
* @param collection 原Bean集合
* @param targetType 目标Bean类型
* @param <T> Bean类型
* @return 复制后的List
* @since 5.6.6
*/
public static <T> List<T> copyToList(final Collection<?> collection, final Class<T> targetType) {
return copyToList(collection, targetType, CopyOptions.of());
}
/**
* 复制集合中的Bean属性<br>
* 此方法遍历集合中每个Bean复制其属性后加入一个新的{@link List}
@ -758,20 +595,7 @@ public class BeanUtil {
return target;
}).collect(Collectors.toList());
}
/**
* 复制集合中的Bean属性<br>
* 此方法遍历集合中每个Bean复制其属性后加入一个新的{@link List}
*
* @param collection 原Bean集合
* @param targetType 目标Bean类型
* @param <T> Bean类型
* @return 复制后的List
* @since 5.6.6
*/
public static <T> List<T> copyToList(final Collection<?> collection, final Class<T> targetType) {
return copyToList(collection, targetType, CopyOptions.of());
}
// endregion
/**
* 给定的Bean的类名是否匹配指定类名字符串<br>
@ -881,6 +705,110 @@ public class BeanUtil {
);
}
/**
* 判断是否为可读的Bean对象判定方法是
*
* <pre>
* 1是否存在只有无参数的getXXX方法或者isXXX方法
* 2是否存在public类型的字段
* </pre>
*
* @param clazz 待测试类
* @return 是否为可读的Bean对象
* @see #hasGetter(Class)
* @see #hasPublicField(Class)
*/
public static boolean isReadableBean(final Class<?> clazz) {
if (null == clazz) {
return false;
}
return hasGetter(clazz) || hasPublicField(clazz);
}
/**
* 判断是否为可写Bean对象判定方法是
*
* <pre>
* 1是否存在只有一个参数的setXXX方法
* 2是否存在public类型的字段
* </pre>
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @see #hasSetter(Class)
* @see #hasPublicField(Class)
*/
public static boolean isWritableBean(final Class<?> clazz) {
if (null == clazz) {
return false;
}
return hasSetter(clazz) || hasPublicField(clazz);
}
// region ----- hasXXX
/**
* 判断是否有Setter方法<br>
* 判定方法是否存在只有一个参数的setXXX方法
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @since 4.2.2
*/
public static boolean hasSetter(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Method method : clazz.getMethods()) {
if (method.getParameterCount() == 1 && method.getName().startsWith("set")) {
// 检测包含标准的setXXX方法即视为标准的JavaBean
return true;
}
}
}
return false;
}
/**
* 判断是否为Bean对象<br>
* 判定方法是否存在只有无参数的getXXX方法或者isXXX方法
*
* @param clazz 待测试类
* @return 是否为Bean对象
* @since 4.2.2
*/
public static boolean hasGetter(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Method method : clazz.getMethods()) {
if (method.getParameterCount() == 0) {
final String name = method.getName();
if (name.startsWith("get") || name.startsWith("is")) {
if (!"getClass".equals(name)) {
return true;
}
}
}
}
}
return false;
}
/**
* 指定类中是否有public类型字段(static字段除外)
*
* @param clazz 待测试类
* @return 是否有public类型字段
* @since 5.1.0
*/
public static boolean hasPublicField(final Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
for (final Field field : clazz.getFields()) {
if (ModifierUtil.isPublic(field) && !ModifierUtil.isStatic(field)) {
//非static的public字段
return true;
}
}
}
return false;
}
/**
* 判断Bean是否包含值为{@code null}的属性<br>
* 对象本身为{@code null}也返回true
@ -912,6 +840,7 @@ public class BeanUtil {
&& StrUtil.isEmptyIfStr(FieldUtil.getFieldValue(bean, field))
);
}
// endregion
/**
* 检查Bean<br>

View File

@ -86,7 +86,7 @@ public class BeanToBeanCopier<S, T> extends AbsCopier<S, T> {
// 检查目标字段可写性
// 目标字段检查放在键值对编辑之后因为键可能被编辑修改
final PropDesc tDesc = targetPropDescMap.get(sFieldName);
final PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sFieldName);
if (null == tDesc || !tDesc.isWritable(this.copyOptions.transientSupport)) {
// 字段不可写跳过之
return;

View File

@ -12,12 +12,14 @@
package org.dromara.hutool.core.bean.copier;
import org.dromara.hutool.core.bean.PropDesc;
import org.dromara.hutool.core.convert.Convert;
import org.dromara.hutool.core.convert.Converter;
import org.dromara.hutool.core.func.LambdaUtil;
import org.dromara.hutool.core.func.SerFunction;
import org.dromara.hutool.core.lang.mutable.MutableEntry;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.io.Serializable;
import java.lang.reflect.Field;
@ -76,11 +78,16 @@ public class CopyOptions implements Serializable {
*/
protected boolean override = true;
/**
* 是否自动转换为驼峰方式
*/
protected boolean autoTransCamelCase = true;
/**
* 自定义类型转换器默认使用全局万能转换器转换
*/
protected Converter converter = (type, value) ->
Convert.convertWithCheck(type, value, null, ignoreError);
Convert.convertWithCheck(type, value, null, ignoreError);
//region create
@ -178,7 +185,7 @@ public class CopyOptions implements Serializable {
*/
public CopyOptions setIgnoreProperties(final String... ignoreProperties) {
return setPropertiesFilter((field, o) -> {
if(ignoreCase){
if (ignoreCase) {
// issue#I80FP4
return !ArrayUtil.containsIgnoreCase(ignoreProperties, field.getName());
}
@ -284,7 +291,7 @@ public class CopyOptions implements Serializable {
protected MutableEntry<String, Object> editField(final String fieldName, final Object fieldValue) {
final MutableEntry<String, Object> entry = new MutableEntry<>(fieldName, fieldValue);
return (null != this.fieldEditor) ?
this.fieldEditor.apply(entry) : entry;
this.fieldEditor.apply(entry) : entry;
}
/**
@ -311,6 +318,19 @@ public class CopyOptions implements Serializable {
return this;
}
/**
* 设置是否自动转换为驼峰方式<br>
* 一般用于map转bean和bean转bean出现非驼峰格式时在尝试转换失败的情况下是否二次检查转为驼峰匹配
*
* @param autoTransCamelCase 是否自动转换为驼峰方式
* @return this
* @since 6.0.0
*/
public CopyOptions setAutoTransCamelCase(final boolean autoTransCamelCase) {
this.autoTransCamelCase = autoTransCamelCase;
return this;
}
/**
* 设置自定义类型转换器默认使用全局万能转换器转换
*
@ -334,7 +354,7 @@ public class CopyOptions implements Serializable {
*/
protected Object convertField(final Type targetType, final Object fieldValue) {
return (null != this.converter) ?
this.converter.convert(targetType, fieldValue) : fieldValue;
this.converter.convert(targetType, fieldValue) : fieldValue;
}
/**
@ -348,4 +368,25 @@ public class CopyOptions implements Serializable {
protected boolean testPropertyFilter(final Field field, final Object value) {
return null == this.propertiesFilter || this.propertiesFilter.test(field, value);
}
/**
* 查找Map对应Bean的名称<br>
* 尝试原名称转驼峰名称isXxx去掉is的名称
*
* @param targetPropDescMap 目标bean的属性描述Map
* @param sKeyStr 键或字段名
* @return {@link PropDesc}
*/
protected PropDesc findPropDesc(final Map<String, PropDesc> targetPropDescMap, final String sKeyStr) {
PropDesc propDesc = targetPropDescMap.get(sKeyStr);
// 转驼峰尝试查找
if (null == propDesc && this.autoTransCamelCase) {
final String camelCaseKey = StrUtil.toCamelCase(sKeyStr);
if (!StrUtil.equals(sKeyStr, camelCaseKey)) {
// 只有转换为驼峰后与原key不同才重复查询相同说明本身就是驼峰不需要二次查询
propDesc = targetPropDescMap.get(camelCaseKey);
}
}
return propDesc;
}
}

View File

@ -88,7 +88,7 @@ public class MapToBeanCopier<T> extends AbsCopier<Map<?, ?>, T> {
// 检查目标字段可写性
// 目标字段检查放在键值对编辑之后因为键可能被编辑修改
final PropDesc tDesc = findPropDesc(targetPropDescMap, sFieldName);
final PropDesc tDesc = this.copyOptions.findPropDesc(targetPropDescMap, sFieldName);
if (null == tDesc || !tDesc.isWritable(this.copyOptions.transientSupport)) {
// 字段不可写跳过之
return;
@ -109,24 +109,4 @@ public class MapToBeanCopier<T> extends AbsCopier<Map<?, ?>, T> {
});
return this.target;
}
/**
* 查找Map对应Bean的名称<br>
* 尝试原名称转驼峰名称isXxx去掉is的名称
*
* @param targetPropDescMap 目标bean的属性描述Map
* @param sKeyStr 键或字段名
* @return {@link PropDesc}
*/
private PropDesc findPropDesc(final Map<String, PropDesc> targetPropDescMap, String sKeyStr){
PropDesc propDesc = targetPropDescMap.get(sKeyStr);
if(null != propDesc){
return propDesc;
}
// 转驼峰尝试查找
sKeyStr = StrUtil.toCamelCase(sKeyStr);
propDesc = targetPropDescMap.get(sKeyStr);
return propDesc;
}
}

View File

@ -194,7 +194,7 @@ public class Dict extends CustomKeyMap<String, Object> implements TypeGetter<Str
* @return Bean
*/
public <T> T toBean(final T bean) {
return toBean(bean, false);
return BeanUtil.fillBeanWithMap(this, bean, CopyOptions.of());
}
/**
@ -206,33 +206,7 @@ public class Dict extends CustomKeyMap<String, Object> implements TypeGetter<Str
* @since 3.3.1
*/
public <T> T toBeanIgnoreCase(final T bean) {
BeanUtil.fillBeanWithMapIgnoreCase(this, bean, false);
return bean;
}
/**
* 转换为Bean对象
*
* @param <T> Bean类型
* @param bean Bean
* @param isToCamelCase 是否转换为驼峰模式
* @return Bean
*/
public <T> T toBean(final T bean, final boolean isToCamelCase) {
BeanUtil.fillBeanWithMap(this, bean, isToCamelCase, false);
return bean;
}
/**
* 转换为Bean对象,并使用驼峰法模式转换
*
* @param <T> Bean类型
* @param bean Bean
* @return Bean
*/
public <T> T toBeanWithCamelCase(final T bean) {
BeanUtil.fillBeanWithMap(this, bean, true, false);
return bean;
return BeanUtil.fillBeanWithMap(this, bean, CopyOptions.of().setIgnoreCase(true));
}
/**

View File

@ -90,7 +90,8 @@ public class BeanUtilTest {
.put("aGe", 12)
.put("openId", "DFDFSDFWERWER")
.build();
final SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false);
final SubPerson person = BeanUtil.fillBeanWithMap(
map, new SubPerson(), CopyOptions.of().setIgnoreCase(true));
Assertions.assertEquals("Joe", person.getName());
Assertions.assertEquals(12, person.getAge());
Assertions.assertEquals("DFDFSDFWERWER", person.getOpenid());

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023. looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* https://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.bean;
import lombok.Data;
import org.dromara.hutool.core.bean.copier.CopyOptions;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
public class Issue3452Test {
@Test
void fillBeanWithMapTest() {
final Map<String, Object> properties = new HashMap<>();
properties.put("name", "JohnDoe");
properties.put("user_age", 25);
final User user = BeanUtil.fillBeanWithMap(
properties, new User(), CopyOptions.of());
Assertions.assertEquals("JohnDoe", user.getName());
Assertions.assertEquals(25, user.getUserAge());
}
@Data
static class User {
private String name;
private int userAge;
}
}

View File

@ -14,6 +14,7 @@ package org.dromara.hutool.core.xml;
import lombok.Data;
import org.dromara.hutool.core.bean.BeanUtil;
import org.dromara.hutool.core.bean.copier.CopyOptions;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.collection.set.SetUtil;
import org.dromara.hutool.core.io.file.FileUtil;
@ -282,7 +283,7 @@ public class XmlUtilTest {
// 标准方式
final Map<String, Object> map = XmlUtil.xmlToMap(doc.getFirstChild());
final SmsRes res = new SmsRes();
BeanUtil.fillBeanWithMap(map, res, true);
BeanUtil.fillBeanWithMap(map, res, CopyOptions.of().setIgnoreError(true));
// toBean方式
final SmsRes res1 = XmlUtil.xmlToBean(doc.getFirstChild(), SmsRes.class);