使用 Javassist 运行时生成泛型子类

z32xzh27 7年前
   <p>越是复杂的项目希望使用者能愉快的编码的话,可能就要使用到字节码增强工具来暗地里做些手脚。这方面的工具有 JDK 的 Instrumentation, <a href="/misc/goto?guid=4958197083673280437" rel="nofollow,noindex">ASM</a> , <a href="/misc/goto?guid=4959727766699090763" rel="nofollow,noindex">BCEL</a> , <a href="/misc/goto?guid=4959735891708440762" rel="nofollow,noindex">CGLib</a> , <a href="/misc/goto?guid=4959735891813332262" rel="nofollow,noindex">Javassist</a> , 还有 <a href="/misc/goto?guid=4959675430703778613" rel="nofollow,noindex">Byte Buddy</a> . Javassist 和 Byte Buddy 更贴近我们编码中的概念,使用起来也简单,而其他几个工具需要我们更多的了解字节码指令,以及常量池等概念。所以我着重去了解怎么运用 Javassist 和 Byte Buddy 来动态修改来生成类文件。</p>    <p>所以本文是系列中的第一篇,旨在以一个 Javassist 的例子来了解它的基本使用方法。本例中在运行时动态生成一个类的子类,并且是泛型的,实现了一个方法,给类加上了一个注解,最终生成一个类文件。总之尽可能的让这个例子具有代表性,同时又需控制它的复杂性。最后通过加载类文件的方式来验证前面生成的类是否是正确的,也可以直接反编译生成的类文件来查看源代码,不过实际操作中我们可能会被反编译出来的源代码欺骗。</p>    <p>本例所使用的 Javassist 的版本是 3.21.0-GA, 是在一个 Maven 项目中测试的,所以 Maven 的依赖是</p>    <pre>  <code class="language-java"><dependency>      <groupId>org.javassist</groupId>      <artifactId>javassist</artifactId>      <version>3.21.0-GA</version>  </dependency></code></pre>    <p>接着创建好基类 Repository 和注解 Scope, 它们的内容分别如下</p>    <p>泛型的 Repository 类</p>    <pre>  <code class="language-java">package cc.unmi;    public abstract class Repository<T> {      abstract T findOne();  }</code></pre>    <p>注解 Scope</p>    <pre>  <code class="language-java">package cc.unmi;    import java.lang.annotaion.Retention;  import static java.lang.annotation.RetentionPolicy.RUNTIME;    @Retention(RUNTIME)  public @interface Scope {      String value();  }</code></pre>    <p>下面是动态生成子类以及测试的代码</p>    <pre>  <code class="language-java">package cc.unmi;    import javassist.ClassPool;  import javassist.CtClass;  import javassist.CtConstructor;  import javassist.CtMethod;  import javassist.CtNewConstructor;  import javassist.CtNewMethod;  import javassist.bytecode.AnnotationsAttribute;  import javassist.bytecode.ConstPool;  import javassist.bytecode.SignatureAttribute;  import javassist.bytecode.annotation.Annotation;  import javassist.bytecode.annotation.StringMemberValue;    public class Main {        @SuppressWarnings("unchecked")      public static void main(String[] args) throws Exception {          ClassPool pool = ClassPool.getDefault();          CtClass subClass = pool.makeClass("cc.unmi.UserRepository"); //类的全限名称          subClass.setSuperclass(pool.get(Repository.class.getName())); //指定父类,也可以在 makeClass() 的第二个参数指定            //Javassist 对泛型的支持不甚友好,实现方法中还是会把类型擦除          subClass.setGenericSignature(new SignatureAttribute.TypeVariable("Repository<String>").encode());            //即使隐式行为默认的构造函数调用父类的构造函数也必须说明          CtClass[] params = new CtClass[]{ };          CtConstructor ctor = CtNewConstructor.make( params, null, CtNewConstructor.PASS_PARAMS, null, null, subClass );          subClass.addConstructor(ctor);            //使用了源代码的方式来实现一个方法,注意这里的类型是被擦除的, 把 Object 改成 String 反而有问题,见后面的解释          CtMethod findOneMethod = CtNewMethod.make("public Object findOne(){return \"Yanbin\";}", subClass);          subClass.addMethod(findOneMethod);            //加个注解确实复杂,如果想一步创建 Annotation 用 new Annotation("Scope(value=\"Request\")", constPool)          //产生成类文件反编译后看起来也对的,但可能用反射 API 就是看不到它          ConstPool constPool = subClass.getClassFile().getConstPool();          AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);          Annotation scopeAnnotation = new Annotation(Scope.class.getName(), constPool);          scopeAnnotation.addMemberValue("value", new StringMemberValue("Request", constPool));          annotationsAttribute.addAnnotation(scopeAnnotation);          subClass.getClassFile().addAttribute(annotationsAttribute);            //由于是 Maven 项目,所以写入到这个目录中,最后的类文件是 target/classes/cc/unmi/UserRepository.class          subClass.writeFile("target/classes");            Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("cc.unmi.UserRepository");          System.out.println(repositoryClass.getAnnotation(Scope.class).value()); //输出 Request            Repository<String> repository = repositoryClass.newInstance();          System.out.println(repository.findOne()); //输出 Yanbin        }  }</code></pre>    <p>从控制台的输出可以说明动态生成的类是我们期望的结果。详情请参考源代码中的注释。</p>    <p>代码中我们想要生成的类原型是 class UserRepository extends Repository<String>{} , 那么应该实现的就是</p>    <p>本文原始链接 <a href="/misc/goto?guid=4959735891936516287" rel="nofollow,noindex">http://unmi.cc/leverage-javassist-generate-generic-subclass/</a> , 来自隔叶黄莺 Unmi Blog</p>    <p>public String findOne() { ... }</p>    <p>但要是把生成方法的那行代码改成如下</p>    <p>CtNewMethod.make("public String findOne(){return \"Yanbin\";}, subClass);</p>    <p>这时候你要是查看生成类文件经反编译的源代码也很漂亮,显示为返回类型是 String , 可是一加载调用 fineOne() 方法时就悲剧了,控制台的输出就成了</p>    <p>Request</p>    <p>Exception in thread "main" java.lang.AbstractMethodError: cc.unmi.Repository.findOne()Ljava/lang/Object;</p>    <p>at cc.unmi.Main.main(Main.java:45)</p>    <p>对于如何为类指定泛型,可参考 <a href="/misc/goto?guid=4959735892029553867" rel="nofollow,noindex">CtClass.setGenericSignature API</a> 。</p>    <p>最后我们可以看一下生成的 target/classes/cc/unmi/UserRepository.class 文件在 IntelliJ IDEA 中反编译后的样子</p>    <pre>  <code class="language-java">//  // Source code recreated from a .class file by IntelliJ IDEA  // (powered by Fernflower decompiler)  //    package cc.unmi;    import cc.unmi.Scope;    @Scope("Request")  public class UserRepository extends Repository<String> {      public UserRepository() {      }        public Object findOne() {          return "Yanbin";      }  }</code></pre>    <p>应用延伸:</p>    <ol>     <li>复杂的泛型,如 <List<User>>, class UserRepository<T extend User> extends Repository<T> 等,或是方法中的泛型参数</li>     <li>定义新的方法,较少情况,因为基本上我们用以生成字节码的话是基于接口来编程</li>     <li>实现接口,或生成子接口, 相应的就是 makeInterface(...)</li>     <li>是否能更多使用 Java 代码的方式来生成类各种部件</li>     <li>除了生成 Class 文件,我们也可以得到所生成类的引用; 或字节码的内容,可用自定义的类加载器进行加载</li>     <li>是否能同时生成相应的源代码到 Maven 项目的 generated-sources 目录中?未曾试过,找到两个相关的库 <a href="/misc/goto?guid=4959735892125356873" rel="nofollow,noindex">Roaster</a> 和 <a href="/misc/goto?guid=4959735892216512462" rel="nofollow,noindex">SrcGen4Javassist</a> .</li>    </ol>    <p>相关链接: <a href="/misc/goto?guid=4959735892310109523" rel="nofollow,noindex">Javassist 官方指南</a> , 进去有几页内容。</p>    <p> </p>    <p>来自:http://unmi.cc/leverage-javassist-generate-generic-subclass/</p>    <p> </p>