在编程当中,作为Converter, spring体系自带的conversionService可以解决大部分基本对象的转换问题,但对于在业务系统中写的domain,vo,po等对象.spring是不能完成相应对象之间的转换的, 当然也可以使用类似BeanUtils.copyProperties来完成属性之间的数据复制功能. 除此之外,还可以使用第三方组件,比如dozer,都可以进行信息复制处理.
不过上面的方法的问题在于, 除扩展之外,相应的转换过程均是使用反射来完成的.比如通过 PropertyDescriptor 来获取property的readMethod, 然后再通过writeMethod来写入目标对象的数据值,或者直接通过Field.set来完成字段级的数据写入.从编码手法上来看,当前我们更希望通过一些非反射的手法来完成这个操作, 一种实现方法即是通过字节码生成来构建一个特定场景的Converter, 直接通过方法调用来完成相应的映射过程.
一个标准的转换器接口定义如下:
public interface Converter<S, T> { T convert(S s); }
实现者除了要完成具体的convert过程之外, 还需要对外暴露出具体的泛型信息,以方便框架进行读取和解析. 比如spring conversionService即会通过读取converter实现类泛型来完成内部 from -> to的映射过程(而不需要调用者手动进行类型传参).
本文描述了一种通过javassist,字节码操作工具, 动态地读取from,to类的描述信息和注解信息, 完成convert body的字符串生成. 同时, 写入相应的泛型信息, 以实现泛型编程.
本文实现场景中所使用的 From 类,和 To 类 描述如下.相应的converter中的 S 表示 from, T 表示 To, 即从from对象转换为to对象.
public class From1 { String username; String password; } public class To1 { String username; String password; String password2; }
上面的 To1 类中的password2 属性期望来源为 From1 中的 password,即实现 不同属性之间的映射功能.
使用字节码生成,则我们期望生成的目标代码参考如下:
public class ConvertImpl implement Converter<From1, To1> { public From1 convert(To1 s) { From1 from = (From1)$1; To1 to = new To1(); to.setUsername(from.getUsername()); to.setPassword(from.getPassword()); to.setPassword2(from.getPassword()); return to; } }
1 属性映射
这一步,直接使用标准的PropertyDescriptor即可拿到相应的from, to 类的属性信息, 按照默认的的匹配规则,我们可以解决 username, password 属性的同名映射问题. 针对不同属性映射的问题,可以通过字段级注解,类级注解,或Mixin类来完成相应属性的额外标问题.
如通过字段级注解(前提是上面的处理类均是实际源码可修改类), 在上面的 To1 类的 password2 字段上, 可以通过以下注解来完成.
@MapField.From(clazz = From1.class, property = "password") String password2;
以上注解即表示, 在进行转换时,password2 的值由 From1 对象的 password 属性得来.
2 生成转换代码(method body)
通过属性映射,我们可以拿到一份 Map 类似的映射结构,表示针对目标对象的每一个属性,均由源对象的哪个属性得来. 当然, 可能存在源属性中并不能支持目标特定属性的情况(这种情况认为不存在匹配,则直接忽略,目标属性置null).
由于javassist生成方法代码并不需要像asm一样, 使用大量的原生 字节码指令集,而是只需要生成一个如实际代码一样的字符串即可.因此, 我们这里直接使用java拼好相应的字符串即可. 参考代码如下:
val fromVarName = "from"; val toVarName = "to"; StringBuilder builder = new StringBuilder("{"); //val input = (Input)s; 下面的$1表示javassist内置变量 第1个参数对象 builder.append(fromTypeName).append(" ").append(fromVarName).append(" = (").append(fromTypeName).append(")$1;"); //val output = new outputType(); builder.append(toTypeName).append(" ").append(toVarName).append(" = new ").append(toTypeName).append("();"); val pairMap = generatePair(to, from, extraMapping); pairMap.forEach((out, input) -> { //output.setxxx(input.getyyy()); builder.append(toVarName).append(".").append(out.setterName()).append("(").append(fromVarName).append(".").append(input.getterName()).append("());"); }); //return xxx; builder.append("return ").append(toVarName).append(";"); builder.append("}");
通过上面的代码,即完成相应的方法body的生成. 然后再加上相应的方法处理,类处理,则整个convert实现类即生成完毕. 相应的完整参考如下:
val ctClass = classPool.makeClass(newName); //实现接口 CtClass interfaceClass = classPool.getCtClass(interfaceClazz.getName()); ctClass.addInterface(interfaceClass); val method = CtNewMethod.make(objCtClass, "convert", new CtClass[]{objCtClass}, new CtClass[0], null, ctClass); method.setBody(body); //这里的body即上面的整个字符串 //实现并添加方法 ctClass.addMethod(method); //最终toClass生成实现类 val clazz = ctClass.toClass();
3 补充泛型信息
在java中, 相应的泛型信息是额外存储的,而并不是直接在定义类时或者定义方法时即配置好.而是通过额外的属性来描述. 详见: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.9.
因此,在上一步的方法定义时,我们定义的入参和出参的类型均为 java.lang.Object 类型,而不是实际泛型类型.(如果定义为泛型类型反而会报原convert方法未实现的异常,即AbstractMethodError). 按照 method的签名信息描述, 这里生成的方法本身没有额外的泛型信息,因此只需要将参数类型和返回类型描述一下即可,即实际的 From1 类型和 To1 类型.
//方法签名组成 自身方法泛型, 参数Type, 返回类型Type, 异常Type, 因此没有额外的泛型,只需要传null即可 val methodSignature = new SignatureAttribute.MethodSignature(null, new SignatureAttribute.Type[]{fromSig}, toSig, null); method.setGenericSignature(methodSignature.encode());
类上面的泛型就稍微麻烦一些, 在当前的实现类中.泛型是存在于接口上的,而不是实现类中. 在接口声明时,相应的泛型为S,T, 而在实现之后,相应的具体泛型即变化为From1, To1, 因此在描述时应该使用实际的类型来代替相应的S,T. 具体的泛型标注如下:
//实现类型type val fromSig = new SignatureAttribute.ClassType(fromTypeName); val toSig = new SignatureAttribute.ClassType(toTypeName); //相应的泛型参数定义 val typeArg = new SignatureAttribute.TypeArgument(fromSig); val typeArg2 = new SignatureAttribute.TypeArgument(toSig); //完整的接口泛型描述 val interfaceClassType = new SignatureAttribute.ClassType(interfaceClass.getName(), new SignatureAttribute.TypeArgument[]{typeArg, typeArg2}); //实现类的泛型描述 实现类的泛型组成 自身泛型,父类泛型,接口泛型[], 因此这里均没有,则实际传入null val signature = new SignatureAttribute.ClassSignature(null, null, new SignatureAttribute.ClassType[]{interfaceClassType}); ctClass.setGenericSignature(signature.encode());
总结:
通过以上的步骤,即相当于创建了一个新的converter类, 来完成特定于业务的转换器. 同时, 满足于实际框架的需求, 泛型信息也正常. 在实际业务使用时, 可即进或提前将相应的converter实现类生成好, 放入全局ConverterUtils中的map中,在具体运行时即可通过 from->to 的映射找到处理类, 来完成转换过程.
相比于通过反射来完成的过程,在性能上会好一些, 不足就是第一次生成时因此获取映射信息以及生成过程,会有一些影响.
上面描述的业务功能还稍简单,实际应用时,还需要处理属性映射之间类型不一致的情况,以及级联映射的问题, map, List等属性的问题. 这些均是具体的功能集, 在具体业务时可实际处理.
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201903290001.html
动态生成类确实有用武之地,但是这种converter还是用mapstruct编译时代码生成来搞定,要不然生成的代码只有在线上跑的时候才能知道具体是什么逻辑,很多时候生成的代码在极端情况下会有问题,而mapstruct生成的代码,是在本地保存的,在浏览代码时,比较容易排查问题。