请看完文章再抄。~~~~~~~~~~~~~~··
在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
- 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的问题了。
- 全局查找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。
- 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
来源:oschina
链接:https://my.oschina.net/u/3730149/blog/4407015