解决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;}// 尝试转Beanif (BeanUtil.isBean(rowType)) {return new BeanConverter<T>(type).convert(value, defaultValue);}// 无法转换throw new ConvertException("Can not Converter from [{}] to [{}]", value.getClass().getName(), type.getTypeName());}
// 尝试转Beanif (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> {@Overridepublic <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 配置的。