使用javassist生成对象转换器Converter

在编程当中,作为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

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注