字节码动态注入 2(javassist)

北城余情 提交于 2020-08-18 05:36:22

请看完文章再抄。~~~~~~~~~~~~~~··

在Android开发中,我们经常通过Gradle Plugin配合Android Gradle Plugin提供的Tranform API,并应用Javassit字节码编辑库在Android打包过程中做一些特殊操作。例如:自动埋点,热修复等。

Javassit提供了一个方便获取ClassPool的方法,ClassPool.getDefault(),它是个单列对象。

当你在使用assemble命令打包你的Android应用时,默认会执行assembleDebug和assembleRelease,如果你增加了定义的buildTypes或者flavors,所有的assembleXXX命令都会执行。因此assemble多次调用void transform(TransformInvocation transformInvocation)方法。

此时,如果你是使用ClassPool.getDefault()来存放你需要操作的Class,并且在自定义Transform中对CtClass应用writeFile(),toClass()或者toByteCode()方法将其转换成Class文件,那么Javassist就会冻结(frozen)这个CtClass对象,之后就不能修改这个CtClass对象了。所以transform方法第二次执行时,我们在对ClassPool.getDefault()里面的CtClass做writeFile(),toClass()或者toByteCode()操作就会发生xxx class is frozen.的错误。

Javassit的此异常是为了警告开发者不要修改已经被JVM加载的class文件,因为JVM不允许重新加载一个类。

解决方法:不要使用ClassPool.getDefault()来获取ClassPool,通过ClassPool classPool = new ClassPool(true)的方式自己创建,因为每次都是新创建的ClassPool,所以在执行assemble后多次调用void transform(TransformInvocation transformInvocation)方法不会出现上述异常。

1.环境 mac,如果是Linux更方便 2.Maven 3.1.0 jdk1.6 额外修改maven配置文件和maven settings.xml

  1. maven 3.5 jdk1.8 额外修改maven配置文件和maven settings.xml

JDK是向上兼容。 比如JDK1.8就兼容1.5 1.6 1.7

1.java探针 java探针分为动态注入和静态注入

需要了解 :linux

实例1:给文件创建软链接 命令:ln -s log2013.log link2013 说明:为log2013.log文件创建软链接link2013,如果log2013.log丢失,link2013将失效

具体用法是:ln -s 源文件 目标文件。 (link)它就可以,不必重复的占用磁盘空间。例如:ln -s /bin/less /usr/local/bin/less

Mac OS X安装JDK1.6及相关解决找不到tools.jar的问题 1.安装JDK1.6

oracle官网从jdk1.7开始才有Mac版的安装包,但有的项目必须使用jdk1.6,所以必须从其他途径安装jdk1.6了。

2.包路径等问题

系统默认安装的JRE路径  /System/Library/Frameworks/JavaVM.framework/

oracle和apple等安装的JDK包的路径 /Library/Java/JavaVirtualMachines/

3.JAVA_HOME在哪了? /Library/Java/JavaVirtualMachines/1.6.0_38-b04-436.jdk/Contents/Home

注:1.6.0_38-b04-436.jdk目录名字与安装的jdk版本有关

4.rt.jar、jsse.jar、tools.jar去哪了? rt.jar和tools.jar已经集成到/Library/Java/JavaVirtualMachines/1.6.0_38-b04-436.jdk/Contents/Classes/classes.jar

jsse.jar也在Classes目录下

建议把classes.jar和jsse.jar建立软连接到/Library/Java/JavaVirtualMachines/1.6.0_38-b04-436.jdk/Contents/Home/lib/下,并且classes.jar的软链接命名为rt.jar。同理,也建多一个为tools.jar的软链接。

这样就可以避免一些时候会发生找不到rt.jar、tools.jar的问题了。

  1. 全局查找tools.jar find / -name tools.jar  2. 例如:

#进到lib目录 cd /Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib

#建立软连接 ln -s /Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bundle/Classes/classes.jar tools.jar

查找这个类的根本解决办法是我用到了com.sun.tools.attach 包下的VirtualMachine类,根据这个类所在的jar包找到了classes.jar包 如图:

3.配置文件: 只需要在/etc/bashrc或者/etc/profile下添加配置

5.配置JAVA_HOME Mac OS X的环境变量文件在/etc/profile,unix一贯重要的文件。 在此添加最下端添加

 export JAVA_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home export PATH=$PATH:$JAVA_HOME/bin 保存后退出,然后注销就生效了。

注:添加JAVA_HOME后,系统也会使用你配置的JAVA_HOME的jdk为默认JDK。

  1. instrutment中retransformClasses和redefineClasses

总结: class文件随着虚拟机启动的时候,会经过premain方法,premain方法中定义了transform,这个premain在虚拟机启动的时候会被执行一次,然后通过transform方法对类进行了修饰,就好像被穿了一件衣服,class加载的时候注入了transform中的内容,以后每次class执行的时候就会走一次transform里面的东西,就好下你给之前给你穿了一个衣服,以后每次看你你都穿着这个衣服。

retransformClasses是因为agent虽然嵌入了,但是在虚拟机启动的时候,某些类比如thread,在javaagent启动之前就已经加载到了内存,javaagent也是类,在javaagent加载之前虚拟机需要加载一些必须的类来保证我的javaagent的运行,比如说thread,这个时候thread就没有被”穿上衣服“,即没有被transform修饰,也就不能被javaagent监控到,这个时候就需要retransformClasses重新加载,注意retransformClasses会让没有被”穿上衣服的类”穿上衣服“

redefineClasses也是重新加载一次,但是这里注意并没有给类”穿衣服“,即通过这种方法加载的时候,类不会经过transform方法。这个方法的作用类似于,原来给类”穿上了衣服“,通过这个方法可以给这个类”脱了衣服“

一个关于redefineClass的不错的博文:https://blog.csdn.net/raintungli/article/details/51655608

总的来说他们两个的共同点都是让类(class文件)重新加载进入内存,不同的是前者是为了“穿衣服”,后者是为了“脱衣服”

基于Java Agent的attach方式实现方法耗时监控

在上一篇中我们已经介绍了java agent的相关概念和思想,给出了premain方式的实现代码。本篇主要是实现了attach方式,不同之处主要如下:

premain是静态修改,在类加载之前修改; attach是动态修改,在类加载后修改 要使premain生效重启应用,而attach不重启应用即可修改字节码并让其重新加载 可以看到attach的方式更加强大,其核心原理首先是找到相关的进程id, 然后根据进程id去动态修改相关字节码,具体的修改方式和premain无差,下面就直接给出详细实现。

项目结构(此处为了方便把主程序和Agent程序放在一起, 实际生产中肯定是分开的):

先看测试结果:

打成jar包

mvn clean package

运行主程序

java -jar target/myAgent-jar-with-dependencies.jar

运行agent程序, 注意带上系统的lib目录

java -Djava.ext.dirs=${JAVA_HOME}/lib -jar target/myAgent-jar-with-dependencies.jar LoadAgent

如上图所示,可以看到首先找到主程序的进程id为34077,然后再attach上去

代码:

maven.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
    <groupId>com.hebh</groupId>
    <artifactId>agent-demo</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>A custom project using myfaces</name>
    <url>http://www.myorganization.org</url>

    <build>
        <finalName>myAgent</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <!-- jdk6要改为6 -->
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <!-- jdk6要加 -->
                <!--<version>2.5.5</version>-->
                <configuration>
                    <archive>
                        <!--避免MANIFEST.MF被覆盖-->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <!--打包时加入依赖-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- Project dependencies -->

    <dependencies>
        <!--jdk6 mac版本 -->
        <!--<dependency>-->
            <!--<groupId>com.sun</groupId>-->
            <!--<artifactId>tools</artifactId>-->
            <!--<version>1.6</version>-->
            <!--<scope>system</scope>-->
            <!--<systemPath>${java.home}/lib/tools.jar</systemPath>-->
        <!--</dependency>-->

    <!-- Project dependencies -->

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <!-- jdk6改为6 -->
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
       <!-- 这是jdk7以上版本的 -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.1-GA</version>
        </dependency>

        <!-- jdk6 -->
        <!-- https://mvnrepository.com/artifact/javassist/javassist -->
        <!--<dependency>-->
            <!--<groupId>javassist</groupId>-->
            <!--<artifactId>javassist</artifactId>-->
            <!--<version>3.1</version>-->
        <!--</dependency>-->

        <!-- log4j支持版本是1.7以上 -->
        <!--<dependency>-->
            <!--<groupId>org.apache.logging.log4j</groupId>-->
            <!--<artifactId>log4j-api</artifactId>-->
            <!--<version>2.11.1</version>-->

        <!--</dependency>-->
        <!--<dependency>-->
            <!--<groupId>org.apache.logging.log4j</groupId>-->
            <!--<artifactId>log4j-core</artifactId>-->
            <!--<version>2.11.1</version>-->

        <!--</dependency>-->
    </dependencies>

</project>

MANIFEST.MF

Main-Class: com.hebh.demo.application.Launcher
Agent-Class: com.hebh.demo.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Launcher.java

package com.hebh.demo.application;

/**
 * description:主程序和Agent程序的路由:启动类
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:11 PM
 */
public class Launcher {
    public static void main(String[] args) throws Exception {
        if(args != null && args.length > 0 && "LoadAgent".equals(args[0])) {
            new AgentLoader().run();
        }else{
            new MyApplication().run();
        }
    }
}


项目部分

MyApplication.java

package com.hebh.demo.application;

import java.util.logging.Logger;

/**
 * description:主程序部分:
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:12 PM
 */
public class MyApplication {
    private static Logger logger = Logger.getLogger(MyApplication.class+"");

    public static void run() throws Exception {
        logger.info("[Application] Starting My application");
        Runner runner = new Runner();
        for(;;){
            runner.run();
        }
    }
}


Runner.java

package com.hebh.demo.application;

import java.util.logging.Logger;

/**
 * description:
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:12 PM
 */
public class Runner {
    private static final Logger logger = Logger.getLogger(Runner.class+"");
    public void run() throws InterruptedException{
        long sleep = (long)(Math.random() * 1000 + 200);
        Thread.sleep(sleep);

        logger.info(String.format("run in [%d] millis!", sleep));
    }

//    public static void main(String[] args) {
//
//        System.out.println(String.format("run in [{%d}] millis!", 23));
//        System.out.println(String.format("Exception %s", "wwww"));
//    }
}

agent部分

AgentLoader.java

package com.hebh.demo.application;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;



import java.io.File;
import java.util.List;
import java.util.logging.Logger;

/**
 * description:Agent部分:
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:13 PM
 */
public class AgentLoader {
    private static Logger logger = Logger.getLogger(AgentLoader.class+"");
    private static volatile boolean flag = false;

    public static void run() {

        //这里你需要修改成你的线程标识
        //指定jar路径
        String agentFilePath = "/Users/heliming/IdeaProjects/fac/target/myAgent-jar-with-dependencies.jar";

        //需要attach的进程标识
        String applicationName = "myAgent";

        VirtualMachineDescriptor jvms = null;
        List<VirtualMachineDescriptor> list =  VirtualMachine.list();
        for (VirtualMachineDescriptor jvm : list) {

            logger.info(String.format("jvm:{%s}", jvm.displayName()));
            if(jvm.displayName().contains(applicationName)){
                jvms = jvm;
                flag = true;
//                jvm.id();
            }
        }

        if(!flag){
            logger.info(" error Target Application not found");
            return;
        }


        //查到需要监控的进程
//        Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
//                .stream()
//                .filter(jvm -> {
//                    logger.info("jvm:{}", jvm.displayName());
//                    return jvm.displayName().contains(applicationName);
//                })
//                .findFirst().get().id());
//
//        if(!jvmProcessOpt.isPresent()) {
//            logger.error("Target Application not found");
//            return;
//        }
        File agentFile = new File(agentFilePath);
        try {
//            String jvmPid = jvmProcessOpt.get();
            String jvmPid = jvms.id();
            logger.info("Attaching to target JVM with PID: " + jvmPid);
            VirtualMachine jvm = VirtualMachine.attach(jvmPid);
            jvm.loadAgent(agentFile.getAbsolutePath());
            jvm.detach();
            logger.info("Attached to target JVM and loaded Java agent successfully");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}


MyInstrumentationAgent.java

package com.hebh.demo.agent;



import java.lang.instrument.Instrumentation;
import java.util.logging.Logger;

/**
 * description:
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:13 PM
 */
public class MyInstrumentationAgent {
    private static Logger logger = Logger.getLogger(MyInstrumentationAgent.class+"");

    public static void agentmain(String agentArgs, Instrumentation inst) {
        logger.info("[Agent] In agentmain method");

        //需要监控的类
        String className = "com.hebh.demo.application.Runner";
        transformClass(className, inst);
    }

    private static void transformClass(String className, Instrumentation instrumentation) {
        Class<?> targetCls = null;
        ClassLoader targetClassLoader = null;
        // see if we can get the class using forName
        try {

            targetCls = Class.forName(className);
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            logger.info("see if we can get the class using forName");
            return;
        } catch (Exception ex) {
            logger.info(String.format("Class [%s] not found with Class.forName",className));
        }
        // otherwise iterate all loaded classes and find what we want
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetCls = clazz;
                targetClassLoader = targetCls.getClassLoader();
                transform(targetCls, targetClassLoader, instrumentation);
                logger.info("otherwise iterate all loaded classes and find what we want");
                return;
            }
        }
        throw new RuntimeException("Failed to find class [" + className + "]");
    }

    private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
        MyTransformer dt = new MyTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {

            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }

}


MyTransformer.java

package com.hebh.demo.agent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;



import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.logging.Logger;

/**
 * description:
 *
 * @author: he QQ:       905845006
 * @email: 905845006@qq.com
 * @date: 2020/6/29    5:14 PM
 */
public class MyTransformer implements ClassFileTransformer {
    private static Logger logger = Logger.getLogger(MyTransformer.class+"");

    //需要监控的方法
    private static final String WITHDRAW_MONEY_METHOD = "run";

    /** The internal form class name of the class to transform */
    private String targetClassName;
    /** The class loader of the class we want to transform */
    private ClassLoader targetClassLoader;

    public MyTransformer(String targetClassName, ClassLoader targetClassLoader) {
        this.targetClassName = targetClassName;
        this.targetClassLoader = targetClassLoader;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            logger.info("[Agent] Transforming class " + className);
            try {
                //这里必须用对应的jdk版本支持的javassist
                ClassPool cp = ClassPool.getDefault();

                CtClass cc = cp.get(targetClassName);

                CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);
                // 开始时间
                m.addLocalVariable("startTime", CtClass.longType);
                m.insertBefore("startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                // 结束时间
                m.addLocalVariable("endTime", CtClass.longType);
                endBlock.append("endTime = System.currentTimeMillis();");

                // 时间差
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append("opTime = endTime-startTime;");

                // 打印方法耗时
                endBlock.append("logger.info(\"completed in:\" + opTime + \" millis!\");");

                m.insertAfter(endBlock.toString());
                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception e) {
                //jdk版本和javassist得对应,不然这里报错,不支持,因为操作的jvm指令不一样,框架没做兼容
                logger.info("Exception :"+e.toString());
            }
        }
        return byteCode;
    }
}

这里给出我3.5和3.1依赖的仓库

maven3.5 settings.xml

    <!-- 阿里云仓库 -->
<mirror>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <mirrorOf>central</mirrorOf>       
</mirror>

<!-- 中央仓库1 -->
<mirror>
    <id>repo1</id>
    <mirrorOf>central</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://repo1.maven.org/maven2/</url>
</mirror>

<!-- 中央仓库2 -->
<mirror>
    <id>repo2</id>
    <mirrorOf>central</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://repo2.maven.org/maven2/</url>
</mirror>

因为jdk1.6必须用3.1以下的maven编译所以给出setting配置文件,其中配置这里有坑,已填。(不知道是maven3.1之前的jar包编译奇葩,还是我之前maven3.5配置插件已经加载过了,反正3.5不用加第一个仓库maven就能编译通过)

maven3.1 settings.xml

      <mirror>
		<id>mirrorId</id>
		<mirrorOf>central</mirrorOf>
		<name>Human Readable Name </name>
		<url>https://repo1.maven.org/maven2</url>
	</mirror>
  <mirror>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <mirrorOf>central</mirrorOf>       
</mirror>

<!-- 中央仓库1 -->
<mirror>
    <id>repo1</id>
    <mirrorOf>central</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://repo1.maven.org/maven2/</url>
</mirror>

<!-- 中央仓库2 -->
<mirror>
    <id>repo2</id>
    <mirrorOf>central</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://repo2.maven.org/maven2/</url>
</mirror>

如果是mac jdk1.6得加软连接文章开头已经给出。如果linux不熟悉最好用root用户操作。不然配置权限什么的容易没权限无法执行。

参考: https://www.pianshen.com/article/4363249456/

访问者模式:https://www.jianshu.com/p/1f1049d0a0f4

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!