解决Hutool BeanUtil 拷贝异常场景

背景

我们使用的是Hutool工具包的cn.hutool.core.bean.BeanUtil解决对象拷贝复制场景。

工作中我们经常做这样工作:比如说将VO复制成DO。 VO、DTO、DTO、BO,RequestDTO互相转化。

业务

 我们服务作为系统的开放平台应用,统一维护管理第三方平台API接口。比如企业微信接口。而我们使用开源项目 wxJava 方便我们调用企业微信API。 我们需要将wxJava 的接口入参类复制一份作为项目的RequestDTO,做到业务隔离避免其他项目直接依赖。所以牵扯到到大量的对象拷贝工作。

场景

目标类  

WxCpWelcomeMsg

/**
 * 消息文本消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */·········        ·
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WxCpWelcomeMsg implements Serializable {
  private static final long serialVersionUID = 4170843890468921757L;

  @SerializedName("welcome_code")
  private String welcomeCode;

  private Text text;

  private List<Attachment> attachments;

  public String toJson() {
    return WxCpGsonBuilder.create().toJson(this);
  }
}
/**
 * 消息文本消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */
@Data
@Accessors(chain = true)
public class Text implements Serializable {
  private static final long serialVersionUID = 6608288753719551600L;
  private String content;
}
package me.chanjar.weixin.cp.bean.external.msg;

import com.google.gson.annotations.SerializedName;
import lombok.Data;
import me.chanjar.weixin.cp.constant.WxCpConsts;

import java.io.Serializable;

/**
 * @author chutian0124
 */
@Data
public class Attachment implements Serializable {
  private static final long serialVersionUID = -8078748379570640198L;

  @SerializedName("msgtype")
  private String msgType;

  private Image image;

  private Link link;

  @SerializedName("miniprogram")
  private MiniProgram miniProgram;

  private Video video;

  private File file;

  public void setImage(Image image) {
    this.image = image;
    this.msgType = WxCpConsts.WelcomeMsgType.IMAGE;
  }

  public void setLink(Link link) {
    this.link = link;
    this.msgType = WxCpConsts.WelcomeMsgType.LINK;
  }

  public void setMiniProgram(MiniProgram miniProgram) {
    this.miniProgram = miniProgram;
    this.msgType = WxCpConsts.WelcomeMsgType.MINIPROGRAM;
  }

  public void setVideo(Video video) {
    this.video = video;
    this.msgType = WxCpConsts.WelcomeMsgType.VIDEO;
  }

  public void setFile(File file) {
    this.file = file;
    this.msgType = WxCpConsts.WelcomeMsgType.FILE;
  }
}
/**
 * 图片消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */
@Data
public class Image implements Serializable {
  private static final long serialVersionUID = -606286372867787121L;

  @SerializedName("media_id")
  private String mediaId;

  @SerializedName("pic_url")
  private String picUrl;
}


/**
 * 图文消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */
@Data
public class Link implements Serializable {
  private static final long serialVersionUID = -8041816740881163875L;
  private String title;
  @SerializedName("picurl")
  private String picUrl;
  private String desc;
  private String url;
  @SerializedName("media_id")
  private String mediaId;
}
/**
 * 小程序消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */
@Data
public class MiniProgram implements Serializable {
  private static final long serialVersionUID = 4242074162638170679L;

  private String title;
  @SerializedName("pic_media_id")
  private String picMediaId;
  private String appid;
  private String page;
}
/**
 * 视频消息
 *
 * @author pg
 * @date 2021-6-21
 */
@Data
public class Video implements Serializable {
  private static final long serialVersionUID = -6048642921382867138L;
  @SerializedName("media_id")
  private String mediaId;
  @SerializedName("thumb_media_id")
  private String thumbMediaId;
}
/**
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2021-08-23
 */
@Data
public class File implements Serializable {
  private static final long serialVersionUID = 2794189478198329090L;

  @SerializedName("media_id")
  private String mediaId;
}





来源对象

WxCpWelcomeMsg 是我们自定义RequestDTO 

读取数据

 String content = "{\"attachments\":[{\"image\":{},\"msgType\":\"image\"}],\"platformCode\":\"corp_wx\",\"responseClass\":\"java.lang.Void\",\"responseType\":\"java.lang.Void\",\"text\":{\"content\":\"22\"},\"welcomeCode\":\"Eu8O9rXwWoaPRTXGmNT-F1_aDQevOWjI6FyVEyBnZLk\",\"wxApiEnum\":\"ExternalContact\"}";

WxCpWelcomeMsgRequest request = JSON.parseObject(content, WxCpWelcomeMsgRequest.class);

Hutool BeanUtil.copyProperties 拷贝对象有bug

代码如下

BeanUtil.copyProperties(request,WxCpWelcomeMsg.class);

结果

异常

msgType值竟然是file ,不是image。这是什么奇葩现象!

debug 探索问题

现象

 

 BeanUtil工具会调用目标类每个setter方法,哪怕入参是null,导致msgType等于file

尝试解决


WxCpWelcomeMsg wxCpWelcomeMsg1 = new WxCpWelcomeMsg();
BeanUtil.copyProperties(request, wxCpWelcomeMsg1,CopyOptions.create().setIgnoreNullValue(true));

配置拷贝策略,忽略null但还是不能解决。

过程我就不贴出来。直接给出最终定位的方法

	/**
	 * 转换值为指定类型
	 *
	 * @param <T>           转换的目标类型(转换器转换到的类型)
	 * @param type          类型目标
	 * @param value         被转换值
	 * @param defaultValue  默认值
	 * @param isCustomFirst 是否自定义转换器优先
	 * @return 转换后的值
	 * @throws ConvertException 转换器不存在
	 */
	@SuppressWarnings("unchecked")
	public <T> T convert(Type type, Object value, T defaultValue, boolean isCustomFirst) throws ConvertException {
		if (TypeUtil.isUnknown(type) && null == defaultValue) {
			// 对于用户不指定目标类型的情况,返回原值
			return (T) value;
		}
		if (ObjectUtil.isNull(value)) {
			return defaultValue;
		}
		if (TypeUtil.isUnknown(type)) {
			type = defaultValue.getClass();
		}

		if (type instanceof TypeReference) {
			type = ((TypeReference<?>) type).getType();
		}

		// 标准转换器
		final Converter<T> converter = getConverter(type, isCustomFirst);
		if (null != converter) {
			return converter.convert(value, defaultValue);
		}

		Class<T> rowType = (Class<T>) TypeUtil.getClass(type);
		if (null == rowType) {
			if (null != defaultValue) {
				rowType = (Class<T>) defaultValue.getClass();
			} else {
				// 无法识别的泛型类型,按照Object处理
				return (T) value;
			}
		}

		// 特殊类型转换,包括Collection、Map、强转、Array等
		final T result = convertSpecial(type, rowType, value, defaultValue);
		if (null != result) {
			return result;
		}

		// 尝试转Bean
		if (BeanUtil.isBean(rowType)) {
			return new BeanConverter<T>(type).convert(value, defaultValue);
		}

		// 无法转换
		throw new ConvertException("Can not Converter from [{}] to [{}]", value.getClass().getName(), type.getTypeName());
	}
	// 尝试转Bean
	if (BeanUtil.isBean(rowType)) {
		return new BeanConverter<T>(type).convert(value, defaultValue);
	}
	/**
	 * 构造,默认转换选项,注入失败的字段忽略
	 *
	 * @param beanType 转换成的目标Bean类型
	 */
	public BeanConverter(Type beanType) {
		this(beanType, CopyOptions.create().setIgnoreError(true));
	}

这块使用new BeanConverter(type) .构造器。 没有调用使用者传入的CopyOptions拷贝选项。

看起来Hutool 这块设计比较差!

使用BeanCopier 方案

WxCpWelcomeMsg wxCpWelcomeMsg = new WxCpWelcomeMsg();
BeanCopier beanCopier = BeanCopier.create(WxCpWelcomeMsgRequest.class, WxCpWelcomeMsg.class, false);
        beanCopier.copy(request,wxCpWelcomeMsg,null);

结果

异常

text没有赋值 

尝试解决

由于Text类定义使用Accessors 注解

/**
 * 消息文本消息.
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 * @date 2020-08-16
 */
@Data
@Accessors(chain = true)
public class Text implements Serializable {
  private static final long serialVersionUID = 6608288753719551600L;
  private String content;
}

翻看beanCopirer源码,无法获取包含返回值不为void的set方法。

使用Orika 方案

DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.registerFilter(new MyFilter<>());
mapperFactory.getMapperFacade().map(request,wxCpWelcomeMsg);
/**
 * 配置过滤器,若入参对象是空,则不注入
 **/
public class MyFilter<A,B>  extends NullFilter<A,B> {

    @Override
    public <S extends A, D extends B> boolean shouldMap(Type<S> sourceType, String sourceName, S source, Type<D> destType, String destName, D dest, MappingContext mappingContext) {
        return source != null;
    }
}

结果

按预期结果拷贝参数。

结论

 Orika 组件是兼容Lombok的Accessors 配置的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiguansheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值