我是靠谱客的博主 傲娇鞋子,最近开发中收集的这篇文章主要介绍tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

76688390da2aa123a5656124fd611538.png

写在前面

在面向对象编程领域,方法增强有好几种做法:

继承:符合开闭原则,简单地通过继承类的扩展,实现方法增强,但需通过调用子类方法才能增强;

装饰:同样符合开闭原则,继承的优化方案,相比于继承更加灵活。装饰者(Decorator)本身就是某一块功能增强的组件,可以通过一层一层的装饰实现渐进式功能增强,既能无限增强也能直接一层到位,这些都是灵活的,开发者可以根据现有的装饰者组合成自己想要最终对象。典型实现就是 http://java.io 中的 stream;

db05150f3ee986f388bb87dd7028231a.png

代理:代理分静态代理和动态代理。静态代理比较麻烦了,需要开发者显式创建代理类,把真实类实例赋给代理类,此时代理类持有了对真实类实例的引用,做到对真实类的增强,而一旦真实类方法签名发生变化,静态代理类一样需要跟着变化,这便是静态代理的痛点。

而我们今天的重点就是在动态代理(Dynamic Proxy)。

动态代理

动态代理好处不用说,无代码侵入且更加灵活,其实现方式很多,JDK 的默认实现便是通过实现java.lang.reflect.InvocationHandler#invoke接口方法,进而实现对具体类的方法增强。而功能强大的 CGlib 便是通过 ASM 工具修改字节码的方式实现的。

本文便基于 ASM 操作,完成一次入门级的演示。

“偷梁换柱”

9ec9555b8383c8b37bc26eb64a538338.png

图中我们可以看到,正常的类加载过程中,byte code 被加载进内存,走完类验证以及初始化流程以后,理论上就可以直接执行了。

然而这次,我们需要在 JVM 进程完全启动之前,监听类加载事件,替换字节码。此所谓 “偷梁换柱”。

具体怎么做?

借助 java.lang.instrument.ClassFileTransformer。

javaagent 的 premain 方法是在 main方法执行前执行的,那么我们只需要在 premain 方法里通过 instrumentation 指定transformer 实现对类加载事件的监听,代码奉上:

io.libriraries.asm.agent.Agent

public class Agent {

    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(
                (loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
                    // 只对 io/libriraries/asm/agent/Person 类做方法增强
                    if ("io/libriraries/asm/agent/Person".equals(className)) {
                        System.out.println("io/libriraries/asm/agent/Person transforming...");
                        ClassReader reader = new ClassReader(classfileBuffer);
                        // 要指定 COMPUTE_MAXS 新生成字节码需要自动计算操作数栈的最大值,否则会报错
                        ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS);
                        ClassVisitor cv = new EnhancerAdapter(writer);
                        reader.accept(cv, 0);
                        System.out.println("io/libriraries/asm/agent/Person transformed");


                        // debug 输出文件到磁盘,方便核查
                        try (FileOutputStream fos = new FileOutputStream("F:Person.class")) {
                            fos.write(writer.toByteArray());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }

                        return writer.toByteArray();
                    }
                    return classfileBuffer;
                }
        );
    }
}


lambda 表达式部分,实际上就是 ClassFileTransformer 的匿名类,实现其 transform 方法便可以做到对类加载事件的监听。 在这里我们做了过滤,只对 io/libriraries/asm/agent/Person 这个类做方法增强。

transform 方法的返回值便是 ASM 修改以后的类的二进制流,这部分的二进制流会替代之前原始的二进制流,进入到类加载的流程中,验证并初始化。

io.libriraries.asm.agent.EnhancerAdapter

/**
 * 增强适配器
 */
class EnhancerAdapter extends ClassVisitor {

    private final TraceClassVisitor tracer;

    public EnhancerAdapter(ClassVisitor cv) {
        super(ASM6, cv);
        PrintWriter pw = new PrintWriter(System.out);
        tracer = new TraceClassVisitor(cv, pw);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

        final MethodVisitor mv = tracer.visitMethod(access, name, desc, signature, exceptions);
        if (isIgnore(mv, access, name)) {
            return mv;
        }
        return new EnhancerMethodAdapter(mv, access, name, desc);
    }

    @Override
    public void visitEnd() {
        System.out.println(tracer.p.getText());
        super.visitEnd();
    }

    /**
     * 忽略构造方法、类加载初始化方法,final方法和 abstract 方法
     *
     * @param mv 
     * @param access
     * @param methodName
     * @return
     */
    private boolean isIgnore(MethodVisitor mv, int access, String methodName) {
        return null == mv
                || isAbstract(access)
                || isFinalMethod(access)
                || "<clinit>".equals(methodName)
                || "<init>".equals(methodName);
    }

    private boolean isAbstract(int access) {
        return (ACC_ABSTRACT & access) == ACC_ABSTRACT;
    }

    private boolean isFinalMethod(int methodAccess) {
        return (ACC_FINAL & methodAccess) == ACC_FINAL;
    }
} 

io.libriraries.asm.agent.EnhancerMethodAdapter

/**
 * 方法级适配器
 */
class EnhancerMethodAdapter extends AdviceAdapter {

    private final String name;

    protected EnhancerMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
        super(ASM6, mv, access, name, desc);
        this.name = name;
    }

    /**
     * 方法前置
     */
    @Override
    protected void onMethodEnter() {
        // 前置逻辑 => System.out.println("method : " + name + " invoke start...");
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("method : " + name + " invoke start...");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    /**
     * 方法后置
     *
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        // 后置逻辑 => System.out.println("method : " + name + " invoke end...");
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("method : " + name + " invoke end...");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        super.onMethodExit(opcode);
    }
}

ASM 语法很有趣,是基于Visit Design Pattern的。所以你能看到ClassVisitor,MethodVisitor,FieldVisitor甚至AnnotationVisitor这些接口的存在。作为开发者,你只需要根据需求实现它们的方法即可。

实现它们有什么用呢?

答:实现基于事件的回调。

在通过 ClassReader 读取类二进制流的过程中,ClassReader会根据一定的顺序读取类结构元素,当读取(visit)到某个元素的时候,会触发你实现的回调逻辑(也就是上述好几个visitor的实现方法,由你自己实现),典型例子参考上面的 EnhancerAdapter 。所以通常说 ASM 也是事件驱动的 Event-Based API。

ClassReader 的 visit 顺序 是这样的:

visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod)* visitEnd

当然 ClassReader 不能修改、删除或新增类元素。这里我们要借助 ClassWriter 实现。visit 这个词也很有意思,在 ASM 的设计上是模棱两可的,对于 ClassReader 来说,可以理解为访问、读取;对于 ClassWriter来说, 就得理解成写入了。当然这里的写入并非写入到class文件,也不是写入到类加载读取的 Class 二进制流里,这里仅仅是写入到 ClassWriter 维护的内存副本。这个副本到最后可以通过 toByteArray() 方法拿到修改后的类字节码二进制流。

无论 ClassWriter 还是 ClassReader ,他们都是 ClassVisitor的实现类。因此不难理解 visit 本身的多义性。

TraceClassVisitor 在这里是可有可无,不影响逻辑,只是为了方便观察修改后的字节码是怎样的。可以理解 tracer 是 ClassWriter 的代理。

onMethodEnter 和 onMethodExit 分别对应方法的进入和退出,这就和我们之前的动态代理对应上了。ASM 方法增强本质就是在这两个回调方法里注入逻辑(当然是以字节码的形式注入啦!)

嗯,这里就涉及到 JVM 字节码指令问题,回头我会在一篇文章里整理字节码的速查表(Cheat Sheet),以备随时翻查。除此之外还有异常处理的逻辑,不在此篇阐述。

再回头看上面代码,是不是容易理解很多呢?

上面的代码已经完成了必要的逻辑,而我们在使用 premain 时千万不要忘记在 MANIFEST中填写这个所属类的全限定名。

具体我借助了 Maven 插件,指定 io.libriraries.asm.agent.Agent 为Premain Class。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>2.3.1</version>
  <configuration>
      <archive>
          <manifest>
              <addClasspath>true</addClasspath>
          </manifest>
          <manifestEntries>
              <Premain-Class>
                  io.libriraries.asm.agent.Agent
              </Premain-Class>
          </manifestEntries>
      </archive>
  </configuration>
</plugin>

以上,我们的 Agent 端就大功告成了!我们将其打成 jar 包。

这时候我们需要写个简单的调用来验证下这里的方法增强是否成功。

public class Main {

    public static void main(String[] args) {
        // p => ASM Enhancer
        Person p = new Person();
        p.doSth();
    }
}

执行这个 main 方法的时候要带上一个 JVM 参数

-javaagent:target/asm-enhance-agent-1.0-SNAPSHOT.jar

asm-enhance-agent-1.0-SNAPSHOT.jar 就是刚才打出来的 jar 包。

执行结果

io/libriraries/asm/agent/Person transforming...
io/libriraries/asm/agent/Person transformed
method : doSth invoke start...
this guy is doing sth
method : doSth invoke end...

done.

小结

ASM 是个很庞大的工具,除了本篇涉及到的仍然有许多 API 需要探索,这里仅仅是九牛一毛。希望本篇有助于初学者打破 ASM 入门的壁垒。

本篇代码完整奉上:

https://gist.github.com/leonlibraries/7c56db347939866f9513b961088e64d6

参考资料:

《ASM 使用指南》

http://web.cs.ucla.edu/~msb/cs239-tutorial/

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

最后

以上就是傲娇鞋子为你收集整理的tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?的全部内容,希望文章能够帮你解决tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(39)

评论列表共有 0 条评论

立即
投稿
返回
顶部