hutool的BeanUtil.copyProperties复制枚举类属性大坑

在项目中使用Hutool的BeanUtil.copyProperties方法复制对象属性时,遇到一个转换异常:CannotconvertORDER_INVALIDtoclasscom.xxx。问题源于新增了一个枚举类的静态方法getEnumByCode,此方法影响了Hutool的枚举转换规则,导致转换失败。解决方案是将该方法抽取到枚举类操作工具类中。
摘要由CSDN通过智能技术生成

现象

        项目中需要使用到对象属性复制,于是使用hutool的BeanUtil.copyProperties方法。这个方法线上一直用着都没问题,然而最近修改代码后却突然报错:Can not convert XXX to XXX。结合代码得知,该报错为把Map中的字符串复制到Bean的枚举类属性,并为该属性设置对应对象时出现的。

报错截图如下:

 报错内容如下:

cn.hutool.core.convert.ConvertException: Can not convert ORDER_INVALID to class com.xxx
	at cn.hutool.core.convert.impl.EnumConverter.convertInternal(EnumConverter.java:53) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.convert.AbstractConverter.convert(AbstractConverter.java:58) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.convert.ConverterRegistry.convertSpecial(ConverterRegistry.java:357) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.convert.ConverterRegistry.convert(ConverterRegistry.java:271) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.convert.ConverterRegistry.convert(ConverterRegistry.java:297) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.convert.Convert.convertWithCheck(Convert.java:745) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.provider.MapValueProvider.value(MapValueProvider.java:65) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.provider.MapValueProvider.value(MapValueProvider.java:24) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.BeanCopier.lambda$valueProviderToBean$1(BeanCopier.java:259) ~[hutool-all-5.7.16.jar!/:na]
	at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608) ~[na:1.8.0_322]
	at cn.hutool.core.bean.BeanUtil.descForEach(BeanUtil.java:182) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.BeanCopier.valueProviderToBean(BeanCopier.java:232) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.BeanCopier.mapToBean(BeanCopier.java:133) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.copier.BeanCopier.copy(BeanCopier.java:102) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.BeanUtil.copyProperties(BeanUtil.java:742) ~[hutool-all-5.7.16.jar!/:na]
	at cn.hutool.core.bean.BeanUtil.copyProperties(BeanUtil.java:706) ~[hutool-all-5.7.16.jar!/:na]
	at 

排查

        之前一直用都没问题,突然间出现报错着实奇怪,首先想到的就是改动的代码导致的bug。

        通过Git查看新提交的代码,发现本次改动只是在枚举类中增加一个通过code获取枚举类对象的静态方法getEnumByCode,代码如下:

package com.xxx;

import lombok.Getter;


@Getter
public enum XxxTypeEnum {

    XX("1", "类型1"),
    ORDER_INVALID("8", "订单失效"),
    /*...*/
    /*此处省略一堆枚举类对象*/

    ;

    private final String code;
    private final String desc;

    private static final Map<String, XxxTypeEnum> ENUM_MAP = Arrays.stream(XxxTypeEnum.values()).collect(Collectors.toMap(XxxTypeEnum::getCode, Function.identity(), (v1, v2) -> v1));

    XxxTypeEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getFullDesc() {
        return "[" + this.getCode() + "-" + this.getDesc() + "]";
    }

    // TODO: 2023-2-7 hutool的BeanUtil.copyProperties巨坑: 枚举类中有类似下面的方法会导致报错:Can not convert {字符串} to class {全类名}
    public static XxxTypeEnum getEnumByCode(String code) {
        return ENUM_MAP.get(code);
    }
}

        相关代码截图:XxxEventReq、XxxTypeEnum

 

        经同事提醒,进一步排查问题,在idea下载hutool的源码,查看cn.hutool.core.convert.impl.EnumConverter.convertInternal方法源码如下:

	@Override
	protected Object convertInternal(Object value) {
		Enum enumValue = tryConvertEnum(value, this.enumClass);
		if (null == enumValue && false == value instanceof String) {
			// 最后尝试先将value转String,再valueOf转换
			enumValue = Enum.valueOf(this.enumClass, convertToStr(value));
		}

		if (null != enumValue) {
			return enumValue;
		}

        // 报错在此抛出
		throw new ConvertException("Can not convert {} to {}", value, this.enumClass);
	}

        继续查看tryConvertEnum方法代码,注意到方法描述中转换规则的【找到类似转换的静态方法调用实现转换且优先使用】内容,该内容指出【用户自定义的类似转换的静态方法】的优先级高于【Enum的valueOf方法】。因此,当枚举类中无【用户自定义的类似转换的静态方法】时,BeanUtil会使用【Enum的valueOf方法】根据字符串获取到枚举类对象并赋值给Bean中的该枚举类属性。当枚举类中增加自定义的getEnumByCode方法后,BeanUtil会根据该方法来转换对象,然而枚举类XxxTypeEnum中的code并没有包含ORDER_INVALID,所以getEnumByCode方法返回为null,进而导致convertInternal方法抛出异常。

        综上,bug是枚举类中XxxTypeEnum增加类似转换的静态方法getEnumByCode导致的。当然hutool的上述转换规则确实是个坑,在使用时要特别注意。

	/**
	 * 尝试转换,转换规则为:
	 * <ul>
	 *     <li>如果实现{@link EnumItem}接口,则调用fromInt或fromStr转换</li>
	 *     <li>找到类似转换的静态方法调用实现转换且优先使用</li>
	 *     <li>约定枚举类应该提供 valueOf(String) 和 valueOf(Integer)用于转换</li>
	 *     <li>oriInt /name 转换托底</li>
	 * </ul>
	 *
	 * @param value     被转换的值
	 * @param enumClass enum类
	 * @return 对应的枚举值
	 */
	protected static Enum tryConvertEnum(Object value, Class enumClass) {
		if (value == null) {
			return null;
		}

		// EnumItem实现转换
		if (EnumItem.class.isAssignableFrom(enumClass)) {
			final EnumItem first = (EnumItem) EnumUtil.getEnumAt(enumClass, 0);
			if (null != first) {
				if (value instanceof Integer) {
					return (Enum) first.fromInt((Integer) value);
				} else if (value instanceof String) {
					return (Enum) first.fromStr(value.toString());
				}
			}
		}

		// 用户自定义方法
		// 查找枚举中所有返回值为目标枚举对象的方法,如果发现方法参数匹配,就执行之
		try {
			final Map<Class<?>, Method> methodMap = getMethodMap(enumClass);
			if (MapUtil.isNotEmpty(methodMap)) {
				final Class<?> valueClass = value.getClass();
				for (Map.Entry<Class<?>, Method> entry : methodMap.entrySet()) {
					if (ClassUtil.isAssignable(entry.getKey(), valueClass)) {
						return ReflectUtil.invokeStatic(entry.getValue(), value);
					}
				}
			}
		} catch (Exception ignore) {
			//ignore
		}

		//oriInt 应该滞后使用 以 GB/T 2261.1-2003 性别编码为例,对应整数并非连续数字会导致数字转枚举时失败
		//0 - 未知的性别
		//1 - 男性
		//2 - 女性
		//5 - 女性改(变)为男性
		//6 - 男性改(变)为女性
		//9 - 未说明的性别
		Enum enumResult = null;
		if (value instanceof Integer) {
			enumResult = EnumUtil.getEnumAt(enumClass, (Integer) value);
		} else if (value instanceof String) {
			try {
				enumResult = Enum.valueOf(enumClass, (String) value);
			} catch (IllegalArgumentException e) {
				//ignore
			}
		}

		return enumResult;
	}


	/**
	 * 获取用于转换为enum的所有static方法
	 *
	 * @param enumClass 枚举类
	 * @return 转换方法map,key为方法参数类型,value为方法
	 */
	private static Map<Class<?>, Method> getMethodMap(Class<?> enumClass) {
		return VALUE_OF_METHOD_CACHE.get(enumClass, () -> Arrays.stream(enumClass.getMethods())
				.filter(ModifierUtil::isStatic)
				.filter(m -> m.getReturnType() == enumClass)
				.filter(m -> m.getParameterCount() == 1)
				.filter(m -> false == "valueOf".equals(m.getName()))
				.collect(Collectors.toMap(m -> m.getParameterTypes()[0], m -> m, (k1, k2) -> k1)));
	}

解决

        封装枚举类操作工具类EnumHandleUtil,将枚举类中XxxTypeEnum的getEnumByCode方法抽取到工具类中。

ps

        本文章中对应软件框架版本:

         Java:1.8

         spring boot:1.5.12.RELEASE

         hutool:5.7.16

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值