Javassist字节码增强探秘

Javassist介绍

通过【Java开发者必读:掌握ASM技术的利器与实战应用】我们知道ASM是在指令层次上操作字节码的,通过字节码增强技术-ASM,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist

Javassist(Java Programming Assistant)是一个用于在运行时操作字节码的 Java 库,它允许开发人员动态生成、修改和分析 Java 类的字节码。Javassist提供了一种更高级别的 API,以 Java 代码的方式来操作字节码,而不需要直接操作复杂的字节码指令。这使得动态代码生成和修改变得更加容易和可维护。以下是 Javassist 的一些重要特点和使用方式:

  1. 动态代码生成: Javassist 允许您在运行时通过编写 Java 代码的方式来生成新的类和方法。这种方式使动态代码生成变得非常直观和易于理解。

  2. 类修改和增强: 您可以使用 Javassist 修改已经存在的类的字节码,例如添加、修改或删除字段、方法等。

  3. 字节码操作: Javassist 提供了一套 API,用于操作类的字节码指令,如创建方法、添加指令、修改参数等。这使得您能够以更高级别的抽象方式来进行字节码操作,而无需深入了解底层字节码细节。

  4. AOP 支持: Javassist 可以用于实现 AOP(面向切面编程)的功能,通过在方法前后插入代码来实现横切关注点的处理。

  5. 简化反射: Javassist 可以帮助您避免使用繁琐的 Java 反射 API,通过生成字节码来直接调用方法和访问字段。

  6. 跨版本支持: Javassist 支持处理不同版本的 Java 字节码,因此您可以在不同的 Java 版本间进行字节码操作。

Javassist操作步骤

使用 Javassist 的基本步骤如下:

  1. 导入库: 首先,您需要将 Javassist 库添加到项目的依赖中。

  2. 创建 ClassPool: ClassPool 是 Javassist 的核心类,用于加载和保存类文件。您可以通过 ClassPool 加载要操作的类。从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。

  3. 创建 CtClass: 使用 ClassPool 创建一个 CtClass 对象,该对象表示要操作的类。它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。

  4. 进行修改: 在 CtClass 对象上进行修改,例如添加新方法、修改字段、添加注解等。

  • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

  • 保存修改: 将修改后的 CtClass 对象保存到类文件或加载到 ClassLoader 中。

  • 使用生成的类: 修改后的类现在可以被实例化和使用,从而实现所需的功能。

  • 下面我们写一个小Demo来展示Javassist简单、快速的特点。我们依然是对MathUtils中的add()方法做增强,在方法调用前后分别输出”start”和”end”,实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。

    首先引用jar包:

    <dependency>  
        <groupId>org.javassist</groupId>  
        <artifactId>javassist</artifactId>  
        <version>3.28.0-GA</version>  
    </dependency>

    我们还用上篇文章的测试类

    public class MathUtils {  
        public int add(int a, int b) {  
            return a + b;  
        }  
    }
    public class JavassistTest {  
        public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {  
            ClassPool cp = ClassPool.getDefault();  
            CtClass cc = cp.get("com.demo.bytecode.MathUtils");  
            CtMethod m = cc.getDeclaredMethod("add");  
            m.insertBefore("{ System.out.println(\"start\"); }");  
            m.insertAfter("{ System.out.println(\"end\"); }");  
            Class c = cc.toClass();  
            cc.writeFile("/Users/xx/work/spring-demo/");  
            MathUtils h = (MathUtils) c.newInstance();  
            int result = h.add(1, 2);  
            System.out.println(result);  
        }  
    }

    运行后输出结果为:

    我们打开生成的class文件,可以看到已经生成了相关代码:

    ClassPool需要关注的方法:

    1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;

    2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;

    3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClasstoClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class

    4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

    CtClass需要关注的方法:

    1. freeze : 冻结一个类,使其不可修改;

    2. isFrozen : 判断一个类是否已被冻结;

    3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;

    4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;

    5. detach : 将该class从ClassPool中删除;

    6. writeFile : 根据CtClass生成 .class 文件;

    7. toClass : 通过类加载器加载该CtClass。

    上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

    CtMethod中的一些重要方法:

    1. insertBefore : 在方法的起始位置插入代码;

    2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception

    3. insertAt : 在指定的位置插入代码;

    4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;

    5. make : 创建一个新的方法。

    使用`Javassist`写个`Bean Copy`的工具

    首先定义一个对象转换的接口,生成的转换类实现这个接口

    public interface Converter {  
        /**  
        * 将一个对象复制到另一个对象  
        *  
        * @param from from  
        * @param to to      */  
        void convert(Object from, Object to);  
    }

    实现一个工具类CopyUtil生成转换器

    public class CopyUtil {  

        private static final ConcurrentHashMap<ConverterKey, Converter> CACHE = new ConcurrentHashMap<>(32);  
        private static final AtomicInteger ID = new AtomicInteger();  
        private static final ClassPool pool = ClassPool.getDefault();  
        private static final CtClass converterInterface;  

        static {  
            try {  
                converterInterface = pool.getCtClass(Converter.class.getName());  
            } catch (NotFoundException e) {  
                throw new RuntimeException(e);  
            }  
        }  

        public static void copy(Object from, Object to) {  
            Class<?> fromClass = from.getClass();  
            Class<?> toClass = to.getClass();  

            Converter converter = getConverter(fromClass, toClass);  
            converter.convert(from, to);  
        }  

        /**  
        * 从缓存获取converter  
        *  
        * @param fromClass 源类  
        * @param toClass 目标类  
        * @return 转换器      */  
        private static Converter getConverter(Class<?> fromClass, Class<?> toClass) {  
            ConverterKey key = new ConverterKey(fromClass, toClass);  
            return CACHE.computeIfAbsent(key, CopyUtil::generateConverter);  
        }  


        /**  
        * 使用javassist生成一个转换器  
        *  
        * @param key key  
        * @return converter      */  
        private static Converter generateConverter(ConverterKey key) {  

            Class<?> fromClass = key.fromClass;  
            Class<?> toClass = key.toClass;  

            CtClass converterClass = pool.makeClass("BeanConverter" + ID.getAndIncrement());  

            try {  
                converterClass.addInterface(converterInterface);  
                // 创建一个新的方法  
                CtMethod convertMethod = CtNewMethod.make(generateMethod(fromClass, toClass), converterClass);  
                converterClass.addMethod(convertMethod);  

                Class<?> type = converterClass.toClass(CopyUtil.class.getClassLoader(), CopyUtil.class.getProtectionDomain());  
                return (Converter) type.newInstance();  
            } catch (Exception e) {  
                throw new RuntimeException("- generate converter error", e);  
            }  
        }  

        /**  
        * 生成转换器方法  
        *  
        * @param fromClass 原始类  
        * @param toClass 目标类  
        * @return 方法代码      */  
        private static String generateMethod(Class<?> fromClass, Class<?> toClass) {  
            String prefix = "public void convert(Object from, Object to) {\n";  
            // 对象转换  
            String castFromCode = fromClass.getName() + " a = (" + fromClass.getName() + ") from;\n";  
            String castToCode = toClass.getName() + " b = (" + toClass.getName() + ") to;\n";  
            String postfix = "}\n";  
            // 获取原始类字段  
            Set<String> fromFields = getFields(fromClass);  
            // 获取目标类字段  
            Set<String> toFields = getFields(toClass);  

            fromFields.retainAll(toFields);  

            StringBuilder code = new StringBuilder();  
            for (String field : fromFields) {  
                field = StringUtils.capitalize(field);  
                code.append("b.set").append(field).append("(a.get").append(field).append("());\n");  
            }  

            return prefix + castFromCode + castToCode + code + postfix;  
        }  

        /**  
        * 获取一个类(包含父类)的所有属性  
        *  
        * @param type type  
        * @return 属性list      */  
        private static Set<String> getFields(Class<?> type) {  

            Field[] fields = type.getDeclaredFields();  
            Set<String> fieldSet = Stream.of(fields).map(Field::getName).collect(toSet());  

            Class<?> parent = type.getSuperclass();  
            if (type.equals(Object.class) || parent.equals(Object.class)) {  
                return fieldSet;  
            }  

            Set<String> parentFieldSet = getFields(parent);  
            fieldSet.addAll(parentFieldSet);  

            return fieldSet;  
        }  


        /**  
        * 用于缓存的键      */  
        @EqualsAndHashCode  
        @AllArgsConstructor  
        private static class ConverterKey {  
            Class<?> fromClass;  
            Class<?> toClass;  
        }  
    }

    下面我们测试一下

    public static void main(String[] args) {  
        Person person1 = new Person("zhangsan", 25);  
        Person person2 = new Person();  
        CopyUtil.copy(person1, person2);  
        System.out.println(person2.getName());  
        System.out.println(person2.getAge());  
    }  

    结果正常输出:

    总结

    总体而言,Javassist 是通过在运行时操作字节码,使用高级别的 API 和类抽象(如 CtClass、CtMethod 等),使得动态代码生成和修改变得更加直观和容易。这种方式使开发人员可以在不直接操作底层字节码指令的情况下,实现对 Java 类的动态操作。

    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 215,539评论 6 497
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 91,911评论 3 391
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 161,337评论 0 351
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 57,723评论 1 290
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 66,795评论 6 388
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 50,762评论 1 294
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 39,742评论 3 416
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 38,508评论 0 271
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 44,954评论 1 308
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 37,247评论 2 331
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 39,404评论 1 345
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 35,104评论 5 340
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 40,736评论 3 324
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 31,352评论 0 21
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 32,557评论 1 268
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 47,371评论 2 368
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 44,292评论 2 352

    推荐阅读更多精彩内容