zoukankan      html  css  js  c++  java
  • Javassist基本用法汇总

    最近项目需要对基础架构做增强,需要基于字节码在不侵入原有代码的情况下实现, 故把javassist的基本用法过了一遍。这篇博客就是把主要讲讲为什么要用javassist以及javassist的基本用法。

    1.为什么要使用javassist(上手成本低)

    基于字节码增强的框架有两个ASM和javassit,下面是两个框架的特点以及对比

    Javassist & ASM 对比

    1.Javassist源代码级API比ASM中实际的字节码操作更容易使用
    2.Javassist在复杂的字节码级操作上提供了更高级别的抽象层。Javassist源代码级API只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
    3.Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
    总的来说ASM比Javassist快得多,并且提供了更好的性能。Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
    总之,如果有人需要更简单的方法来动态操作或创建Java类,那么应该使用Javassist API 。如果需要注重性能地方,应该使用ASM库。

    2. javassist基本用法(基于3.28.0-GA的版本) 

    Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.

    Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类.

    ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
    CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
    CtMethods: 表示类中的方法
    CtFields: 表示类中的字段 

    2.1 ClassPool对象

    2.1.1 ClassPool的创建

    // 获取ClassPool对象, 使用系统默认类路径
    ClassPool pool = new ClassPool(true);
    // 效果与 new ClassPool(true) 一致
    ClassPool pool1 = ClassPool.getDefault();

    为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.

    // 从ClassPool中删除CtClass对象
    ctClass.detach();
    // 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
    ClassPool pool2 = new ClassPool(true);

    2.1.2 classpath

    通过 ClassPool.getDefault()获取的ClassPool使用JVM的classpath.在Tomcat等Web服务器运行时, 服务器会使用多个类加载器作为系统类加载器, 这可能导致ClassPool可能无法找到用户的类. 这时, ClassPool须添加额外的classpath才能搜索到用户的类.

    // 将classpath插入到指定classpath之前
    pool.insertClassPath(new ClassClassPath(this.getClass()));
    // 将classpath添加到指定classpath之后
    pool.appendClassPath(new ClassClassPath(this.getClass()));
    // 将一个目录作为classpath
    pool.insertClassPath("/xxx/lib");

    2.2 CtClass对象

    2.2.1 获取CtClass

    // 通过类名获取 CtClass, 未找到会抛出异常
    CtClass ctClass = pool.get("com.kawa.ssist.JustRun");
    // 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
    CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun"); 

    2.2.2 创建CtClass

    // 复制一个类
    CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq");
    // 创建一个新类
    CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna");
    // 通过class文件创建一个新类
    CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));

    2.2.3 CtClass基础信息

    // 类名
    String simpleName = ctClass.getSimpleName();
    // 类全名
    String name = ctClass.getName();
    // 包名
    String packageName = ctClass.getPackageName();
    // 接口
    CtClass[] interfaces = ctClass.getInterfaces();
    // 继承类
    CtClass superclass = ctClass.getSuperclass();
    // 获取类方法
    CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
    // 获取类字段
    CtField ctField = ctClass.getField("name");
    // 判断数组类型
    ctClass.isArray();
    // 判断原生类型
    ctClass.isPrimitive();
    // 判断接口类型
    ctClass.isInterface();
    // 判断枚举类型
    ctClass.isEnum();
    // 判断注解类型
    ctClass.isAnnotation();
    // 冻结一个类,使其不可修改
    ctClass.freeze () 
    // 判断一个类是否已被冻结
    ctClass.isFrozen()
    // 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用
    ctClass.prune() 
    //解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用prune方法
    ctClass.defrost() 

    2.2.4 CtClass类操作

    // 添加接口
    ctClass.addInterface(...);
    // 添加构造器
    ctClass.addConstructor(...);
    // 添加字段
    ctClass.addField(...);
    // 添加方法
    ctClass.addMethod(...);

    2.2.5 CtClass类编译

    // 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
    Class clazz = ctClass.toClass();
    // 类的字节码文件
    ClassFile classFile = ctClass.getClassFile();
    // 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
    byte[] bytes = ctClass.toBytecode();

    2.3 CtMethod对象

    2.3.1 获取CtMethod属性

    CtClass ctClass5 = pool.get(TestService.class.getName());
    CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
    // 方法名
    String methodName = ctMethod.getName();
    // 返回类型
    CtClass returnType = ctMethod.getReturnType();
    // 方法参数, 通过此种方式得到方法参数列表
    // 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
    ctMethod.getLongName();
    // 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
    ctMethod.getSignature();
    
    // 获取方法参数名称, 可以通过这种方式得到方法真实参数名称
    List<String> argKeys = new ArrayList<>();
    MethodInfo methodInfo = ctMethod.getMethodInfo();
    CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
    LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
    int len = ctMethod.getParameterTypes().length;
    // 非静态的成员函数的第一个参数是this
    int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
    for (int i = pos; i < len; i++) {
        argKeys.add(attr.variableName(i));
    }

    2.3.2 CtMethod方法体修改

    // 在方法体前插入代码块
    ctMethod.insertBefore("");
    // 在方法体后插入代码块
    ctMethod.insertAfter("");
    // 在某行 字节码 后插入代码块
    ctMethod.insertAt(10, "");
    // 添加参数
    ctMethod.addParameter(CtClass);
    // 设置方法名
    ctMethod.setName("newName");
    // 设置方法体 $0=this / $1,$2,$3... 代表方法参数
    ctMethod.setBody("{$0.name = $1;}");
    //创建一个新的方法
    ctMethod.make("kawa",CtClass);

    2.3.3 异常块 addCatch()

    在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束

    CtMethod m = ...;
    CtClass etype = ClassPool.getDefault().get("java.io.IOException");
    m.addCatch("{ System.out.println($e); throw $e; }", etype);
    // 等同于添加如下代码: 
    try {
        // the original method body
    } catch (java.io.IOException e) {
        System.out.println(e);
        throw e;
    }

    2.4 特殊标识

    $0

    方法调用的目标对象. 它不等于this, 它代表了调用者. 如果方法是静态的, 则$0为null.

    $1, $2 ..

    方法的参数 m.insertBefore("{ System.out.println($1); System.out.println($2); }");

    $$

    是所有方法参数的简写, 主要用在方法调用上. 例如:
    
    // 原方法
    move(String a,String b)
    move($$)
    move($1,$2)
    // 如果新增一个方法, 方法含有move的所有参数, 则可以这些写: 
    move($$, context)
    move($1, $2, context)

    $args

    $args 指的是方法所有参数的数组,类似Object[],如果参数中含有基本类型,则会转成其包装类型。需要注意的时候,$args[0]对应的是$1,而不是$0,$0!=$args[0],$0=this

    $cflow

    $cflow意思为控制流(control flow),是一个只读的变量,值为一个方法调用的深度。例
    
    //原方法
    int fact(int n) {
        if (n <= 1)
            return n;
        else
            return n * fact(n - 1);
    }
    //javassist调用
    CtMethod cm = ...;
    //这里代表使用了cflow
    cm.useCflow("fact");
    //这里用了cflow,说明当深度为0的时候,就是开始当第一次调用fact的方法的时候,打印方法的第一个参数
    cm.insertBefore("if ($cflow(fact) == 0)"
                  + "    System.out.println("fact " + $1);");

    $_ 与 $r

    $_是方法调用的结果;$r是返回结果的类型, 用于强制类型转换
    
    Object result = ... ;
    $_ = ($r)result; 

    $w

    基本类型的包装类 Integer i = ($w)5;

    $class

    一个 java.lang.Class 对象, 表示当前正在修改的类

    $sig

    类型为 java.lang.Class 的参数类型数组

    $type

    一个 java.lang.Class 对象, 表示返回值类型

    $proceed

    调用表达式中方法的名称

    参考博客:https://zhuanlan.zhihu.com/p/349661837

    参考文档:https://www.javassist.org/tutorial/tutorial.html 

  • 相关阅读:
    129 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 03 饿汉模式 VS 懒汉模式 02 懒汉式的代码实现
    128 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 03 饿汉模式 VS 懒汉模式 01 饿汉式的代码实现
    127 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 02 单例模式概述 01 单例模式的定义和作用
    126 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 01 设计模式概述 01 设计模式简介
    125 01 Android 零基础入门 02 Java面向对象 05 Java继承(下)05 Java继承(下)总结 01 Java继承(下)知识点总结
    leetcode-----121. 买卖股票的最佳时机
    leetcode-----104. 二叉树的最大深度
    Json串的字段如果和类中字段不一致,如何映射、转换?
    Mybatis-Plus的Service方法使用 之 泛型方法default <V> List<V> listObjs(Function<? super Object, V> mapper)
    模糊查询
  • 原文地址:https://www.cnblogs.com/hlkawa/p/15383289.html
Copyright © 2011-2022 走看看