关注公众号【1024个为什么】,及时接收最新推送文章!
最近又是一堆前后端交互的接口,参数的接收、转换,重复编码真的很崩溃。
究其根本原因,还是前后端语言不统一,对基本数据类型的支持有所差异造成的。就形成了 vo 一般都采用 String 类型来接收这些有差异的数据类型的解决方案,但 dto 和数据库需要的类型又是其真正对应的数据类型。
所以本文主要讨论 vo 和 dto 之间的拷贝问题。
|| 诉求
| 诉求一:同名、不同类型的属性,在拷贝时也能赋值
主要是Long、BigDecimal等类型前后端精度问题,vo 里一般都定义为 String 类型,但类型不同的属性在拷贝时又会被略过。
| 诉求二:日期格式化能够灵活适应
页面可能展示年月日,年月日时分秒、时分...,后端是一个Date类型,怎么能适应不同vo呢?
| 诉求三:枚举自动映射
库里存的 1、2,页面要展示成 男、女,每次都要判断赋值?
|| 现有方案
| 规规矩矩,手写拷贝
每一对 vo 和 dto 都写一个拷贝方法,对需要单独处理的属性,逐一处理。
public void voToDto(Vo vo, Dto dto){
dto.setOrderId(Long.valueOf(vo.getOrderId()));
dto.setServiceTime(new SimpleDateFormat().parse(vo.getServiceTime()), );
...
}
public void dtoToVo(Dto dto, Vo vo){
// if(dto.getSex() == 1){
// vo.setSex("男");
// }
vo.setSex(SexEnum.getDescByCode(dto.getSex()));
vo.setOrderId(String.valueOf(dto.getOrderId()));
vo.setServiceTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(dto.getServiceTime()));
...
}
缺点:费时费力,无任何复用可言
优点:前面的三点诉求都能满足
| 三方工具 + 改写setter方法
vo 多声明一套 String 类型的属性,并且改写 setter 方法,在 setter 方法中做特殊处理。
private Long orderId;
private String orderIdStr;
private Integer sex;
private Integer sexStr;
public void setOrderId(Long orderId) {
this.orderId = orderId;
this.orderIdStr = String.valueOf(orderId);
}
public void setSex(Integer sex) {
this.sex = sex;
this.sexStr = SexEnum.getDescByCode(sex);
}
缺点:只省去了同名同类型属性的赋值工作量,其他的还是没省去,也无复用可言
优点:前面的三点诉求都能满足
| 三方工具 + 重写转换器
三方工具默认只支持同名同类型属性间的赋值,毕竟不同类型属性赋值存在类型转换异常的风险,所以三方工具的做法也是合理的。
三方工具基本也都提供了重写转换逻辑的功能,可以重写转换器满足特定的需求。
public class MyConverter implements Converter {
@Override
public boolean convert(Object value, Class target, Object context) {
...
}
}
缺点:不同的三方工具,提供的转换器不同,有一定的学习成本;可能需要写很多个转换器;诉求三无法满足
优点:vo 和 dto 无需任何改动,代码量也会缩减不少,可以满足较为复杂的需求
|| 我的方案
| 思路
整体方向是解决开头提到的三个诉求。
自定义注解 + 自定义拷贝工具。
1、同名不同类型的属性,直接强转拷贝。回想我们的使用场景,都是明确的同一业务场景下的对象拷贝
2、自定义注解实现可配置的日期格式化、可配置的枚举数据源
3、拷贝逻辑中,对自定义的注解进行处理
| 上代码
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface FromEnum {
Class clazz() default Enum.class;
// code属性名
String codeFiledName() default "code";
// value属性名
String valueFiledName() default "desc";
}
public @interface Pattern {
String pattern() default "yyyy-MM-dd HH:mm:ss";
}
拷贝逻辑
public static <I, O> O convertVo(I inputObj, Class<O> outputType) {
try {
if (inputObj == null) {
return null;
}
O k = outputType.newInstance();
Map<String, String> inFiledMap = new HashMap<>();
Map<String, String> outFiledMap = new HashMap<>();
Field[] inFields = inputObj.getClass().getDeclaredFields();
Arrays.stream(inFields).forEach((inf) -> {
inFiledMap.put(inf.getName(), inf.getType().getSimpleName().toUpperCase());
});
Field[] outFields = outputType.getDeclaredFields();
Arrays.stream(outFields).forEach((of) -> {
outFiledMap.put(of.getName(), of.getType().getSimpleName().toUpperCase());
});
for (Field inField : inFields) {
inField.setAccessible(true);
if (!Modifier.isStatic(inField.getModifiers()) && inField.get(inputObj) != null) {
Field outField = outputType.getDeclaredField(inField.getName());
outField.setAccessible(true);
// String -> Date
if ("STRING".equals(inFiledMap.get(inField.getName())) && "DATE".equals(outFiledMap.get(inField.getName()))) {
// 未指定pattern
if (inField.getAnnotation(Pattern.class) == null) {
outField.set(k, DateUtil.parseDate((String) inField.get(inputObj)));
} else { // 指定pattern
outField.set(k, DateUtil.parseDate((String) inField.get(inputObj), inField.getAnnotation(Pattern.class).pattern()));
}
continue;
// String -> Long
} else if ("STRING".equals(inFiledMap.get(inField.getName())) && "LONG".equals(outFiledMap.get(inField.getName()))) {
outField.set(k, Long.valueOf((String) inField.get(inputObj)));
continue;
// String -> BigDecimal
} else if ("STRING".equals(inFiledMap.get(inField.getName())) && "BIGDECIMAL".equals(outFiledMap.get(inField.getName()))){
outField.set(k, new BigDecimal((String) inField.get(inputObj)));
continue;
// Long -> String
} else if("LONG".equals(inFiledMap.get(inField.getName())) && "STRING".equals(outFiledMap.get(inField.getName()))){
outField.set(k, inField.get(inputObj).toString());
continue;
// Date -> String
} else if("DATE".equals(inFiledMap.get(inField.getName())) && "STRING".equals(outFiledMap.get(inField.getName()))){
// 未指定pattern
if (outField.getAnnotation(Pattern.class) == null) {
outField.set(k, DateUtil.formatDate2String((Date) inField.get(inputObj)));
} else { // 指定pattern
outField.set(k, DateUtil.formatDate2String((Date) inField.get(inputObj), outField.getAnnotation(Pattern.class).pattern()));
}
continue;
//BigDecimal -> String
}else if ("BIGDECIMAL".equals(inFiledMap.get(inField.getName())) && "STRING".equals(outFiledMap.get(inField.getName()))){
outField.set(k, inField.get(inputObj).toString());
continue;
// 属性类型相同,直接赋值
}else if(inFiledMap.get(inField.getName()).equals(outFiledMap.get(inField.getName()))){
outField.set(k, inField.get(inputObj));
// 自动映射枚举
if("INTEGER".equals(inFiledMap.get(inField.getName()))
&& "STRING".equals(outFiledMap.get(inField.getName() + "Str"))
&& outputType.getDeclaredField(inField.getName() + "Str").getAnnotation(FromEnum.class) != null){
FromEnum fromEnum = outputType.getDeclaredField(inField.getName() + "Str").getAnnotation(FromEnum.class);
Class enumClz = fromEnum.clazz();
String codeFiledName = fromEnum.codeFiledName();
String valueFiledName = fromEnum.valueFiledName();
Object[] enumConstants = enumClz.getEnumConstants();
for (Object enumConstant : enumConstants) {
Method code = enumConstant.getClass().getMethod("get" + codeFiledName.toUpperCase().substring(0,1) + codeFiledName.substring(1, codeFiledName.length()));
Method value = enumConstant.getClass().getMethod("get" + valueFiledName.toUpperCase().substring(0,1) + valueFiledName.substring(1, valueFiledName.length()));
if(code != null && value != null){
if (inField.get(inputObj).equals(code.invoke(enumConstant))) {
Field fieldStr = outputType.getDeclaredField(inField.getName() + "Str");
fieldStr.setAccessible(true);
fieldStr.set(k, value.invoke(enumConstant));
break;
}
}
}
}
}
}
}
return k;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
使用
@Pattern(pattern = "yyyy-MM-dd")
private String serviceTime;
@FromEnum(clazz = com.test.SexEnum.class)
private String sex;
BeanCopierUtil.convertVo(vo, Dto.class)
属性值合法性校验也可以加在拷贝方法的前面,更加严谨一些。
|| 总结
1、工欲善其事必先利其器
2、一个够用的工具可能比一个通用的工具更合适
3、在重复造轮子的过程中也可以适当的改造一下轮子