美文网首页
Java AOP利剑之ASM,真正的AOP

Java AOP利剑之ASM,真正的AOP

作者: anrikuwen | 来源:发表于2019-04-29 09:52 被阅读0次

    在前面有一篇博客中讲了如何通过Java的动态代理来实现AOP编程。之前也讲过其实动态代理并不能算代码层面的AOP编程,其实质是在运行时动态生成一个类然后以静态代理的方式来实现AOP编程的。

    今天要讲的ASM是直接通过对代码进行修改没有使用代理来进行AOP编程的。

    ASM简介

    这篇博客主要是将如何使用ASM来实现AOP的,ASM不是重点,因此ASM在这里就只做一些简单的介绍。

    ASM是一个用来生成字节码或者修改字节码的开源库。官网地址

    org.objectweb.asm包是一些基本的ASM类型,比如ClassReader、ClassWritter都在这里面。依赖于ASM的访问者模式可以很轻易的就在ClassReader、ClassWritter之间添加新的访问者进行字节码的修改

    org.objectweb.asm.tree包下类主要的作用是将输入的字节码以树的形式进行保存,这样做好处是可以很清晰地在对应的节点进行字节码的修改,用着也比直接中间插入访问者方便。

    简单来说,第一种在ClassReader、ClassWritter的方式类似于xml的sax解析;而使用树形式则类似于dom解析。

    这篇博客只会用到上面两个包下的类,其它的包有兴趣的可以自己去看一下这里就不一一介绍了。

    更详细ASM的入门可以看Instrumenting Java Bytecode with ASM,里面介绍的例子是使用的ClassReader、ClassWritter之间夹访问者的方式。

    本篇博客使用的树节点的方式进行字节码的修改实现AOP。

    准备

    首先去官网的这个地方asm和asm-tree目录下下载7.1版本的jar包。

    代码结构

    code_structure

    common包下的代码就是前面中common包下一样的代码这里就不介绍了。

    asm包下的代码会对LinuxPC、LinuxService的字节码进行修改,进行SS(由于简书貌似会屏蔽全称,这里使用简称)属性的添加以及代理的执行。

    common

    pc

    package common.pc;
    
    /**
     * 代表一台个人电脑
     */
    public interface PC {
    
        void searchByGoogle();
    
    }
    
    package common.pc;
    
    /**
     * Linux系统的个人电脑
     */
    public class LinuxPC implements PC {
    
        @Override
        public void searchByGoogle() {
            System.out.println("Searching with Google by LinuxPC");
        }
        
    }
    

    service

    package common.service;
    
    /**
     * 代表一台服务器
     */
    public interface Service {
        void searchByGoogle();
    }
    
    package common.service;
    
    /**
     * 一台Linux服务器
     */
    public class LinuxService implements Service {
        @Override
        public void searchByGoogle() {
            System.out.println("Searching with Google by LinuxService");
        }
    }
    

    SS

    package common;
    
    /**
     * 现实中SS是要分平台的,这里为了方便就想成全平台用一个SS。总之就想成服务端实现代理的一个软件吧。
     * 而且现实中不仅服务端需要安装SS,客户端也是需要的。这里客户端也忽略掉。
     */
    public class SS {
    
        public void startProxy() {
            System.out.println("start proxy");
        }
    
        public void stopProxy() {
            System.out.println("stop proxy");
        }
    }
    

    asm

    package asm;
    
    import common.SS;
    import org.objectweb.asm.*;
    import org.objectweb.asm.tree.*;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.List;
    import static org.objectweb.asm.Opcodes.*;
    
    public class InstallSS {
    
        private static final String SS_DESCRIPTER = Type.getDescriptor(SS.class);
        private static final String SS = Type.getInternalName(SS.class);
        private static String FIELD_OWNER = "";
    
        public static void main(String[] args) {
            FIELD_OWNER = args[1].substring(0, args[1].length() - 6);
            // 其中arg[0]是源字节码文件,args[1]是目标字节码文件
            installSS(args[0], args[1]);
        }
    
        /**
         * 进行SS的安装
         * @param src 源字节码
         * @param dst 目标字节码
         */
        public static void installSS(String src, String dst) {
            FileInputStream fis = null;
            FileOutputStream fos = null;
            try {
                byte[] outputByteCode;
                fis = new FileInputStream(src);
                // ClassReader读入字节码
                ClassReader cr = new ClassReader(fis);
                // ClassNode将字节码以节点树的形式表示
                ClassNode cn = new ClassNode(ASM7);
                // SKIP_FRAMES用于避免访问帧内容,因为改变字节码的过程中帧内容会被改变,比如局部变量、操作数栈都可能改变。
                cr.accept(cn, ClassReader.SKIP_FRAMES);
    
                // 进行SS属性的添加
                addSSField(cn.fields);
    
                for (MethodNode methodNode : cn.methods) {
                    if (methodNode.name.equals("<init>")) {
                        // 构造器中对SS属性进行初始化
                        initSSField(methodNode);
                    } else if (methodNode.name.equals("searchByGoogle")) {
                        // searchByGoogle方法中添加SS的调用
                        addSSExecute(methodNode);
                    }
                }
                // COMPUTE_FRAMES表示ASM会自动计算所有内容,visitFrame和visitMaxs方法都会被忽略掉
                // 还有一个COMPUTE_MAXS是会自定计算局部变量表和操作数栈的大小,visitMaxs会被忽略掉。
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
                cn.accept(cw);
                // 生成的字节码写入目标文件中
                outputByteCode = cw.toByteArray();
                fos = new FileOutputStream(dst);
                fos.write(outputByteCode);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        /**
         * 给src中的字节码添加SS的属性
         * @param fields
         */
        private static void addSSField(List<FieldNode> fields) {
            boolean isHaveSSFiled = false;
            for (FieldNode fieldNode : fields) {
                if (fieldNode.desc.equals(SS_DESCRIPTER)) {
                    isHaveSSFiled = true;
                    break;
                }
            }
    
            if (!isHaveSSFiled) {
                fields.add(new FieldNode(ACC_PRIVATE, "SS", SS_DESCRIPTER, null, null));
            }
        }
    
        /**
         * 在构造方法中对SS属性进行初始化
         * @param methodNode 表示该字节码一个方法节点的值
         */
        private static void initSSField(MethodNode methodNode) {
            AbstractInsnNode[] nodes = methodNode.instructions.toArray();
            int length = nodes.length;
            // 初始化相关的字节码指令
            InsnList insnList = new InsnList();
            insnList.add(new VarInsnNode(ALOAD, 0));
            insnList.add(new TypeInsnNode(NEW, SS));
            insnList.add(new InsnNode(DUP));
            insnList.add(new MethodInsnNode(INVOKESPECIAL, SS, "<init>", "()V", false));
            insnList.add(new FieldInsnNode(PUTFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
    
            methodNode.instructions.insertBefore(nodes[length - 1], insnList);
        }
    
        /**
         * 在searchByGoogle方法调用中进行SS的startProxy和stopProxy调用。
         * @param methodNode 表示该字节码一个方法节点的值
         */
        private static void addSSExecute(MethodNode methodNode) {
            AbstractInsnNode[] nodes = methodNode.instructions.toArray();
            int length = nodes.length;
            // searchByGoogle方法前面添加上SS的startProxy方法调用
            InsnList startInsnList = new InsnList();
            startInsnList.add(new VarInsnNode(ALOAD, 0));
            startInsnList.add(new FieldInsnNode(GETFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
            startInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, SS, "startProxy", "()V", false));
            methodNode.instructions.insertBefore(nodes[0], startInsnList);
    
            // searchByGoogle方法的后面加上SS的stopProxy方法的调用
            InsnList endInsnList = new InsnList();
            endInsnList.add(new VarInsnNode(ALOAD, 0));
            endInsnList.add(new FieldInsnNode(GETFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
            endInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, SS, "stopProxy", "()V", false));
            methodNode.instructions.insertBefore(nodes[length - 1], endInsnList);
        }
    
    }
    

    字节码修改的过程这里就不详解了,重点注释已经写出来了。

    这里对字节码的修改主要使用了的org.objectweb.asm.tree包下的内容,比如说ClassNode、MethodNode、FieldNode都是这个包下的。

    当然也有org.objectweb.asm包下的,主要就是ClassReader、ClassWriter用于对字节码输入以及输出。

    主包

    import common.pc.LinuxPC;
    import common.service.LinuxService;
    
    public class Main {
    
        public static void main(String[] args) {
            LinuxPC linuxPC = new LinuxPC();
            LinuxService linuxService = new LinuxService();
            linuxPC.searchByGoogle();
            linuxService.searchByGoogle();
        }
    }
    

    很简单就是基本的调用。

    然后主包下的两个jar包是在准备阶段的时候进行下载的。

    编译执行

    然后在主包下依次输入下面的命令:

    # 编译两种不同类型的国外电脑
    javac common/pc/LinuxPC.java
    javac common/service/LinuxService.java
    
    # 对上面编译出来的字节码做一个备份
    cp common/pc/LinuxPC.class common/pc/LinuxPC.class.bak
    cp common/service/LinuxService.class common/service/LinuxService.class.bak
    
    # 对修改字节码进行SS添加并调用的类进行编译
    javac -cp asm-tree-7.1.jar:asm-7.1.jar:. asm/InstallSS.java
    
    # 执行InstallSS对LinuxPC、LinuxService字节码进行修改(从备份文件中读取修改的字节码方法对应的.class结尾的文件)
    java -cp .:asm-7.1.jar:asm-tree-7.1.jar asm.InstallSS common/pc/LinuxPC.class.bak common/pc/LinuxPC.class
    java -cp .:asm-7.1.jar:asm-tree-7.1.jar asm.InstallSS common/service/LinuxService.class.bak common/service/LinuxService.class
    
    # 编译并调用Main
    javac Main.java
    java Main
    

    执行之后可以看到如下执行结果:

    start proxy
    Searching with Google by LinuxPC
    stop proxy
    start proxy
    Searching with Google by LinuxService
    stop proxy
    

    其实修改后的LinuxPC和LinuxService分别如下:

    修改后的LinuxPC.java:

    package common.pc;
    
    import common.SS;
    
    public class LinuxPC implements PC {
    
        private SS SS;
    
        public LinuxPC() {
            SS = new SS();
        }
    
        @Override
        public void searchByGoogle() {
            SS.startProxy();
            System.out.println("Searching with Google by LinuxPC");
            SS.stopProxy();
        }
    }
    

    修改后的LinuxService.java:

    package common.service;
    
    import common.SS;
    
    public class LinuxService implements Service {
        
        private SS SS;
    
        public LinuxService() {
            SS = new SS();
        }
    
        @Override
        public void searchByGoogle() {
            SS.startProxy();
            System.out.println("Searching with Google by LinuxService");
            SS.stopProxy();
        }
    }
    

    可以看到使用ASM这里没有用静态代理或动态代理,而是通过直接修改字节码来实现了不同类型的类相同功能的添加。

    总结

    本篇博客就一个目标通过ASM使用树节点的形式来实现AOP编程

    然后就是ASM修改字节码的两种方式:

    • 借助于访问者模式,在ClassReader、ClassWritter之间添加自己实现的ClassVisitor来实现。可以参考Instrumenting Java Bytecode with ASM中的例子
    • 通过树节点的形式来进行字节码修改的实现。本篇博客使用的方式。

    然后两个方式的优缺点:

    • 添加ClassVisitor的方式比较简便,而且只用引org.objectweb.asm这一个jar包就行。但是修改起来逻辑不太清晰。
    • 通过树节点的方式,类、属性、方法等都被抽象为一个个树节点。要对某个方法、某个属性进行修改十分的清新,而且更好控制。不过需要引用org.objectweb.asm和org.objectweb.asm.tree两个jar,稍微麻烦点

    相关文章

      网友评论

          本文标题:Java AOP利剑之ASM,真正的AOP

          本文链接:https://www.haomeiwen.com/subject/zxmenqtx.html