ASM字节码插桩探索

ASM字节码插桩实战

ASM简介

ASM(全称为 “Abstract Syntax Tree for Manipulation”)是一个用于操作Java字节码的库。它允许你动态地生成、转换和分析Java字节码,可以用于实现诸如字节码增强、代码生成、静态分析等功能。

ASM 的优点:

  1. 低级别操作:ASM 提供了一种低级别的字节码操作方式,允许你直接操作字节码指令,从而能够更精确地控制和修改字节码。

  2. 性能:由于 ASM 是基于底层字节码操作的,相对而言,它的性能更高,生成的字节码更紧凑,执行效率更高。

  3. 灵活性:ASM 提供了对字节码的细粒度访问,可以进行更复杂和灵活的字节码操作,适用于需要更高级别的控制和自定义的场景。

ASM API

ASM 提供了一组核心 API,用于操作和分析 Java 字节码。以下是 ASM 的核心 API:

  1. ClassVisitor:ClassVisitor 是 ASM 的核心接口之一,用于访问和修改类的结构。通过实现 ClassVisitor 接口,你可以在类被读取、转换和写入时进行自定义的操作。

  2. MethodVisitor:MethodVisitor 是 ClassVisitor 的一个子接口,用于访问和修改方法的结构。你可以实现 MethodVisitor 接口来自定义访问和修改方法的行为。

  3. FieldVisitor:FieldVisitor 是 ClassVisitor 的另一个子接口,用于访问和修改类的字段。通过实现 FieldVisitor 接口,你可以自定义访问和修改字段的行为。

  4. AnnotationVisitor:AnnotationVisitor 是 ASM 的接口之一,用于访问和修改类、方法、字段的注解。通过实现 AnnotationVisitor 接口,你可以自定义访问和修改注解的行为。

  5. ClassReader:ClassReader 是一个用于解析和读取类文件的类。你可以使用 ClassReader 来读取类文件,并以合适的方式将其传递给 ClassVisitor 进行进一步的操作。

  6. ClassWriter:ClassWriter 是一个用于生成字节码的类。你可以使用 ClassWriter 来生成修改后的字节码,并将其写入新的类文件或者加载到内存中。

除了以上列出的核心 API,ASM 还提供了其他一些辅助类和接口,用于处理字节码的各个方面,例如 Type 类用于表示和操作类型信息,Handle 类用于表示方法句柄等。

这些核心 API 为你提供了灵活和底层的手段来操作和分析字节码,允许你进行各种自定义的字节码操作,例如生成新的类、修改现有类的结构、增强方法等。

使用ASM实现AOP案例

  • 首先创建一个javaagent项目,给出pom文件。Premain-Class标签中的值需要替换为自己的agent premain的类。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>org.examplegroupId>
    <artifactId>SQLQueryAgentartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.ow2.asmgroupId>
                <artifactId>asm-bomartifactId>
                <version>9.5version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asmartifactId>
        dependency>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asm-commonsartifactId>
        dependency>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asm-treeartifactId>
        dependency>
        <dependency>
            <groupId>org.javassistgroupId>
            <artifactId>javassistartifactId>
            <version>3.28.0-GAversion>
        dependency>
    dependencies>

    <build>
        <plugins>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-shade-pluginartifactId>
                <configuration>
                    <createDependencyReducedPom>falsecreateDependencyReducedPom>
                    <finalName>${project.artifactId}finalName>
                    <filters>
                        <filter>
                            <artifact>*:*artifact>
                            <excludes>
                                <exclude>META-INF/**exclude>
                            excludes>
                        filter>
                    filters>
                    <transformers>
                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <manifestEntries>
                                <Premain-Class>com.*.*.StartUpPremain-Class>
                                <Can-Redefine-Classes>trueCan-Redefine-Classes>
                                <Can-Retransform-Classes>trueCan-Retransform-Classes>
                            manifestEntries>
                        transformer>
                    transformers>
                configuration>
                <executions>
                    <execution>
                        <phase>packagephase>
                        <goals>
                            <goal>shadegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

project>
  • 创建agent启动来StartUp
import java.lang.instrument.Instrumentation;

public class StartUp {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("-----------------启动开始---------------------");
        inst.addTransformer(new MyTransformer(),true);
        System.out.println("-----------------启动结束---------------------");
    }

}
  • 创建MyTransformer实现ClassFileTransformer
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;


public class MyTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer){
        if (className != null && className.equals("com/**/service/impl/QhyuServiceImpl")) {
            try {
                ClassReader classReader = new ClassReader(className);
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                ClassVisitor classVisitor = new MyClassVisitor(classWriter);
                classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
                return classWriter.toByteArray();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }
}
  • 创建MyClassVisitor继承ClassVisitor实现Opcodes
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
        System.out.println("MyClassVisitor初始化完成");
    }
    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        System.out.println("visitMethod-qhyu1"+name);
        cv.visit(version, access, name, signature, superName, interfaces);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("visitMethod-qhyu2"+name);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
        if (!name.equals("") && mv != null) {
            System.out.println("qhyu-name:"+name);
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }
    static class MyMethodVisitor extends MethodVisitor implements Opcodes {
        public MyMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }

        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("qhyustart");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        }
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                    || opcode == Opcodes.ATHROW) {
                //方法在返回之前,打印"end"
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("qhyuend");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
            }
            mv.visitInsn(opcode);
        }
    }
}

利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

  • 首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。
  • 接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
  • MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
  • 综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

Springboot项目配置agent

我的springboot项目需要使用到这个插桩的agent,所以启动的时候带上参数就行

-javaagent:G:\SQLQueryAgent\SQLQueryAgent\target\SQLQueryAgent.jar 

ASM字节码插桩探索_第1张图片

但是我启动之后报错了,错误如下:

java.lang.IllegalArgumentException
	at org.objectweb.asm.ClassReader.<init>(Unknown Source)
	at org.objectweb.asm.ClassReader.<init>(Unknown Source)
	at org.objectweb.asm.ClassReader.<init>(Unknown Source)
	at com.swcares.skyworth.transformer.MeituanTransformer.transform(MeituanTransformer.java:31)
	at sun.instrument.TransformerManager.transform(TransformerManager.java:188)
	at sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:428)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.springframework.util.ClassUtils.forName(ClassUtils.java:284)
	at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClass(AbstractBeanDefinition.java:469)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doResolveBeanClass(AbstractBeanFactory.java:1545)
	at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1472)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:682)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:649)
	at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1608)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:524)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:495)
	at org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationCondition.checkDefaultDispatcherName(DispatcherServletAutoConfiguration.java:172)
	at org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationCondition.getMatchOutcome(DispatcherServletAutoConfiguration.java:163)
	at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
	at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
	at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:225)
	at org.springframework.context.annotation.ConfigurationClassParser.processMemberClasses(ConfigurationClassParser.java:371)
	at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:271)
	at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:249)
	at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:599)
	at org.springframework.context.annotation.ConfigurationClassParser.access$800(ConfigurationClassParser.java:110)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.lambda$processGroupImports$1(ConfigurationClassParser.java:811)
	at java.util.ArrayList.forEach(ArrayList.java:1257)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.processGroupImports(ConfigurationClassParser.java:808)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorHandler.process(ConfigurationClassParser.java:779)
	at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:192)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:319)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:236)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:280)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:96)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:707)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:533)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
	at com.swcares.skyworth.WebApplication.main(WebApplication.java:12)

这个是因为asm的版本问题导致的,我搜索了下项目中的asm,我们使用的是9.5版本。但是springboot项目中cglib和test框架带了4.2版本和5.0.4版本。

在这里插入图片描述

解决方案:

排除其他的,引入9.5,重新启动。

<dependency>
    <groupId>org.ow2.asmgroupId>
    <artifactId>asmartifactId>
    <version>9.5version>
    <exclusions>
        <exclusion>
            <groupId>*groupId>
            <artifactId>*artifactId>
        exclusion>
    exclusions>
dependency>

在这里插入图片描述

这样就可以在sql查询前后进行aop处理了。

插件分享ASM Bytecode Viewer

我的IDEA是社区版,使用的ASM Bytecode Viewer插件还挺流畅,这样在插入一些东西的时候,可以用java代码写出来然后查看ASMified查看,例如我的System.out.println(“qhyustart”)代码ASM字节码插桩探索_第2张图片

你可能感兴趣的:(程序员的日常,JVM,开发语言,java)