[Java]Javassist基本用法

Javassist是一个能够操作字节码框架,在学习的过程中存在了一些问题,用博客的方式记录下来,希望对大家有所帮助。


一、实例功能

    学习的实例来自于 IBM developer   主要功能实现计算一个方式具体的执行时间. 

二、代码实例

  

[java] view plain copy
  1. package org.java.javassist.one;  
  2.   
  3. /** 
  4.  * 该类并不是对StringBuilder进行解释,而是提供了中方式,方便我们来使用javassist的一些细节 
  5.  * @author xianglj 
  6.  * @date 2016/7/13 
  7.  * @time 9:59 
  8.  */  
  9. public class StringBuilderTest {  
  10.     /** 
  11.      * 假如我们现在需要计算该程序的计算时间: 
  12.      * 则可以标记开始时间(start)和结束时间(end) 
  13.      * 最终的执行时间为(end - start)的值 
  14.      * @param length 
  15.      * @return 
  16.      */  
  17.     public String buildString(int length) {  
  18.         String result = "";  
  19.         for(int i = 0; i<length; i++ ) {  
  20.             result += (i %26 + 'a');  
  21.         }  
  22.         return result;  
  23.     }  
  24.   
  25.     public static void main(String[] args) {  
  26.         /** 
  27.          * class.getName()返回的字符串中,不仅包括了类的名称,同时也包含了该类所在的包名称 
  28.          * <pre> 
  29.          *     格式: 
  30.          *     packagename.classname 
  31.          * </pre> 
  32.          */  
  33. //        System.out.println(StringBuilderTest.class.getName());  
  34.         StringBuilderTest test = new StringBuilderTest();  
  35.         if(null != args) {  
  36.             for(int i = 0, len = args.length; i<len; i++) {  
  37.                 String result = test.buildString(Integer.parseInt(args[i]));  
  38.                 System.out.println("result:" + result);  
  39.             }  
  40.         }  
  41.     }  
  42. }  
这是一个基本实例,通过一个参数传入的length,来生成length长度的字符串。(但是该方式存在一个很验证的性能问题,就是当length的长度组件增大时,该方法的效率就会越低),但在此处不必关系方法的效率问题

三、解决方案

   1) 第一个中解决方案,也是最直观的方式,就是在进入方法时,记录一个当前时间 start , 当代码执行完成之后,获取当前时间 end , 然后采用 (end - start)的方式,来获取代码执行时间

    2) 第二中方式,我们采用Javassit框架来实现:

        (1)  使用通过ClassPool 来获取 CtClass对象

        (2)  从CtClass对象中,获取buildString()的方法

        (3)  为buildString()方法添加代码块

特别说明:

         为方法添加代码块,有三种方式可以实现

       

[java] view plain copy
  1. * <pre>  
  2. *     1. 添加的代码不能引用在方法中其他地方定义的局部变量。这种限制使我不能在 Javassist 中使用在源代码中使用的同样方法实现计时代码,  
  3. *        在这种情况下,我在开始时添加的代码中定义了一个新的局部变量,并在结束处添加的代码中引用这个变量。  
  4. *  
  5. *     2. 我 可以在类中添加一个新的成员字段,并使用这个字段而不是局部变量。不过,这是一种糟糕的解决方案,  
  6. *        在一般性的使用中有一些限制。例如,考虑在一个递归方法中会发生的事情。每次方法调用自身时,上次保存的开始时间值就会被覆盖并且丢失。  
  7. *  
  8. *     3. 我可以保持原来方法的代码不变,只改变方法名,然后用原来的方法名增加一个新方法。  
  9. * </pre>  
前两种方式,在实现上都有一定的问题,所以我们采用第三种方式实现,会比较的容易实现

代码如下:

[java] view plain copy
  1. package org.java.javassist.one;  
  2.   
  3. import javassist.*;  
  4.   
  5. import java.io.IOException;  
  6.   
  7. /** 
  8.  * 通过Javassist来为需要实现计算的方法前后各加上一个拦截器, 
  9.  * 依次来实现方法计算的时间 
  10.  * <pre> 
  11.  *     1. 添加的代码不能引用在方法中其他地方定义的局部变量。这种限制使我不能在 Javassist 中使用在源代码中使用的同样方法实现计时代码, 
  12.  *        在这种情况下,我在开始时添加的代码中定义了一个新的局部变量,并在结束处添加的代码中引用这个变量。 
  13.  * 
  14.  *     2. 我 可以在类中添加一个新的成员字段,并使用这个字段而不是局部变量。不过,这是一种糟糕的解决方案, 
  15.  *        在一般性的使用中有一些限制。例如,考虑在一个递归方法中会发生的事情。每次方法调用自身时,上次保存的开始时间值就会被覆盖并且丢失。 
  16.  * 
  17.  *     3. 我可以保持原来方法的代码不变,只改变方法名,然后用原来的方法名增加一个新方法。 
  18.  * </pre> 
  19.  * @author xianglj 
  20.  * @date 2016/7/13 
  21.  * @time 10:07 
  22.  */  
  23. public class JavassistTiming {  
  24.     public static void main(String[] args) {  
  25.         //开始获取class的文件  
  26.         javassist();  
  27.     }  
  28.   
  29.     public static void javassist() {  
  30.         //开始获取class的文件  
  31.         try {  
  32. //            String classFileName = StringBuilderTest.class.getName();  
  33.             String classFileName = "org.java.javassist.one.StringBuilderTest";  
  34.             CtClass ctClass = ClassPool.getDefault().getCtClass(classFileName);  
  35.             if(ctClass == null) {  
  36.                 System.out.println("Class File (" + classFileName + ") Not Found.....");  
  37.             } else {  
  38.                 addTiming(ctClass, "buildString");  
  39.                 //为class添加计算时间的过滤器  
  40.                 ctClass.writeFile();  
  41.             }  
  42.             Class<?> clazz = ctClass.toClass();  
  43.             StringBuilderTest test = (StringBuilderTest) clazz.newInstance();  
  44.             test.buildString(2000);  
  45.   
  46.         } catch (NotFoundException e) { //类文件没有找到  
  47.             e.printStackTrace();  
  48.         } catch (CannotCompileException e) {  
  49.             e.printStackTrace();  
  50.         } catch (IOException e) {  
  51.             e.printStackTrace();  
  52.         } catch (InstantiationException e) {  
  53.             e.printStackTrace();  
  54.         } catch (IllegalAccessException e) {  
  55.             e.printStackTrace();  
  56.         }  
  57.     }  
  58.   
  59.     /** 
  60.      * 为方法添加拦截器: 
  61.      * <pre> 
  62.      *     构造拦截器方法的正文时使用一个 java.lang.StringBuffer 来累积正文文本(这显示了处理 String 的构造的正确方法, 
  63.      *     与在 StringBuilder 的构造中使用的方法是相对的)。这种变化取决于原来的方法是否有返回值。 
  64.      *     如果它 有返回值,那么构造的代码就将这个值保存在局部变量中,这样在拦截器方法结束时就可以返回它。 
  65.      *     如果原来的方法类型为 void ,那么就什么也不需要保存,也不用在拦截器方法中返回任何内容。 
  66.      * </pre> 
  67.      * @param clazz 方法所属的类 
  68.      * @param method 方法名称 
  69.      */  
  70.     private static void addTiming(CtClass clazz, String method) throws NotFoundException, CannotCompileException {  
  71.   
  72.         //获取方法信息,如果方法不存在,则抛出异常  
  73.         CtMethod ctMethod = clazz.getDeclaredMethod(method);  
  74.         //将旧的方法名称进行重新命名,并生成一个方法的副本,该副本方法采用了过滤器的方式  
  75.         String nname = method + "$impl";  
  76.         ctMethod.setName(nname);  
  77.         CtMethod newCtMethod = CtNewMethod.copy(ctMethod, method, clazz, null);  
  78.   
  79.         /* 
  80.          * 为该方法添加时间过滤器,用来计算时间。 
  81.          * 这就需要我们去判断获取时间的方法是否具有返回值 
  82.          */  
  83.         String type = ctMethod.getReturnType().getName();  
  84.         StringBuffer body = new StringBuffer();  
  85.         body.append("{\n long start = System.currentTimeMillis();\n");  
  86.   
  87.         if(!"void".equals(type)) {  
  88.             body.append(type + " result = ");  
  89.         }  
  90.   
  91.         //可以通过$$将传递给拦截器的参数,传递给原来的方法  
  92.         body.append(nname + "($$);\n");  
  93.   
  94.         //  finish body text generation with call to print the timing  
  95.         //  information, and return saved value (if not void)  
  96.         body.append("System.out.println(\"Call to method " + nname + " took \" + \n (System.currentTimeMillis()-start) + " +  "\" ms.\");\n");  
  97.         if(!"void".equals(type)) {  
  98.             body.append("return result;\n");  
  99.         }  
  100.   
  101.         body.append("}");  
  102.   
  103.         //替换拦截器方法的主体内容,并将该方法添加到class之中  
  104.         newCtMethod.setBody(body.toString());  
  105.         clazz.addMethod(newCtMethod);  
  106.   
  107.         //输出拦截器的代码块  
  108.         System.out.println("拦截器方法的主体:");  
  109.         System.out.println(body.toString());  
  110.     }  
  111. }  

可能会出现的问题:

1. LinkageError  我在实践的过程中,由于想方便,采用了StringBuilderTest.class.getName() 的方法来代替手写的字符串,这个时候,我在使用CtClass.toClass()时出现了异常,异常原因大致为: 一个Class只能被加载一次,因为我们在调用toClass()方法时,会去再加载Class,所以会出现重复加载。

官方文档如下Javassist Tutorial

[Java]Javassist基本用法

        初步学习就到这里,后面会继续更新关于该框架的学习