文章目录
- 简介
- agent与attach
- agent
- attach
- 如何attach
- pom.xml
简介
javaagent是什么?
从名字agent也可以看出,是一种代理。
javaagent用来做什么?
本质上是对class的一种增强,用来实现一些通用功能,例如链路追踪等。
和AOP有什么区别?
AOP和javaagent本质上都是通过修改class来实现额外功能,对代码逻辑本身无侵入,在运行时侵入。
AOP通常是项目内的代理增强,通常是增强业务逻辑,例如:公用授权检查逻辑。
 javaagent是项目外独立的增强项目,通常是非业务逻辑,例如:arthas相关功能、debug、线上运行参数、返回值等数据临时打印等。
javaagent可能我们基本不会用,但是我们最好理解其原理,知道它能做什么,这样我们可以更好理解jacoco、arthas这些工具的原理。
可以丰富工具箱,在我们自己要做项目的时候,也有更多的工具可供选择。
本文重点介绍流程,具体的逻辑本质上还是对字节码的操作,可以看asm、javaassist、cglib、bytebuddy等字节码操作工具。
agent与attach
agent是在启动的时候就指定,在类加载到jvm之前就完成了类的增强,如jacoco
attach可以对已经启动项目,已经加载到jvm中的类进行增强,例如arthas。(这点非常有用,可以不用重启项目,甚至可以做热更新)
agent
首先,我们准备一个需要增强的类,简单点:
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class Start {
    public static final Random random = new Random();
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Start开始执行...");
        while (true) {
            doBusiness();
        }
    }
    public static void doBusiness() throws InterruptedException {
        System.out.println("doBusiness 执行开始");
        int time = random.nextInt(10) + 1;
        TimeUnit.SECONDS.sleep(time);
    }
}
就一个doBusiness业务方法,用来增强。
agent需要一个静态的premain方法,方法签名如下:
public static void premain(String arg, Instrumentation instrumentation)
这个方法在哪个类中不重要,重要的是方法签名要一样。
import vip.meet.transformer.LogTransformer;
import java.lang.instrument.Instrumentation;
public class PreMain {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("执行premain 方法");
        System.out.println("执行premain参数:" + arg);
        instrumentation.addTransformer(new LogTransformer());
    }
}
LogTransformer类使用了javaassist,依赖看后面的pom文件
transform方法是在jvm加载类之前执行。
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class LogTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String realClassName = className.replaceAll("/", ".");
        if (realClassName.equals("vip.meet.Start")) {
            CtClass ctClass;
            try {
                ClassPool classPool = ClassPool.getDefault();
                ctClass = classPool.get(realClassName);
                CtMethod ctMethod = ctClass.getDeclaredMethod("doBusiness");
                ctMethod.addLocalVariable("inject_start", CtClass.longType);
                ctMethod.insertBefore("System.out.println(\"---doBusiness agent 开始执行---\");");
                ctMethod.insertBefore("inject_start = System.currentTimeMillis();");
                ctMethod.insertAfter("System.out.println(\"---doBusiness agent 结束执行---\");");
                ctMethod.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - inject_start));");
                return ctClass.toBytecode();
            } catch (Throwable e) {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }
}
javaagent如何知道代理类入口呢?
答案是MANIFEST.MF的Premain-Class:
Premain-Class: vip.meet.agent.PreMain
可以在maven-jar-plugin、maven-assembly-plugin插件中配置,参考后面pom文件配置。
mvn clean package
java -javaagent:agent-learn-1.0.0-jar-with-dependencies.jar=hello,abc=123 -jar agent-learn-1.0.0-jar-with-dependencies.jar

attach
attach的和agent非常相似,只是入口不一样。
attach方式的入口方法签名如下:
public static void agentmain(String arg, Instrumentation instrumentation)
attach通常就不使用ClassFileTransformer,因为这个是jvm加载类之前的调用。
而attach的时候,jvm已经启动了。
所以,我们需要获取已经加载的类:
Class<?>[] classes = instrumentation.getAllLoadedClasses();
修改类之后,再redefineClasses:
ClassDefinition classDefinition = new ClassDefinition(cls, ctClass.toBytecode());
instrumentation.redefineClasses(classDefinition);
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
public class AgentMain {
    public static void agentmain(String arg, Instrumentation instrumentation) {
        System.out.println("agentmain启动");
        System.out.println("agentmain参数:" + arg);
        Class<?>[] classes = instrumentation.getAllLoadedClasses();
        for (Class<?> cls : classes) {
            String name = cls.getName();
            if (name.equals("vip.meet.Start")) {
                CtClass ctClass;
                try {
                    ClassPool classPool = ClassPool.getDefault();
                    ctClass = classPool.get(name);
                    CtMethod ctMethod = ctClass.getDeclaredMethod("doBusiness");
                    ctMethod.addLocalVariable("inject_start", CtClass.longType);
                    ctMethod.insertBefore("System.out.println(\"---doBusiness agent 开始执行---\");");
                    ctMethod.insertBefore("inject_start = System.currentTimeMillis();");
                    ctMethod.insertAfter("System.out.println(\"---doBusiness agent 结束执行---\");");
                    ctMethod.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - inject_start));");
                    ClassDefinition classDefinition = new ClassDefinition(cls, ctClass.toBytecode());
                    instrumentation.redefineClasses(classDefinition);
                } catch (Throwable e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }
}
如何配置attach入口呢?
答案是MANIFEST.MF的Agent-Class:
Agent-Class: vip.meet.attach.AgentMain
如何attach
现在,我们有attach了,如何attach到已经运行的jvm进程上呢?
可以通过VirtualMachine来实现,注意VirtualMachine是sun的私有实现接口,依赖tools.jar
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AttachUseMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, InterruptedException {
        System.out.println("AttachUseMain启动");
        for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
            String name = vmd.displayName();
            System.out.println(name);
            if (name.equals("agent-learn-1.0.0-jar-with-dependencies.jar")) {
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                vm.loadAgent("E:\\app\\me\\learn\\agent-learn\\target\\agent-learn-1.0.0-jar-with-dependencies.jar=hello,ok,aa,bb,cc=3");
                TimeUnit.MINUTES.sleep(1);
                vm.detach();
            }
        }
    }
}
上面的代码,首先列出所有jvm进程,然后匹配到需要attach的pid,然后执行attach。
首先运行,需要被代理的项目:
java -jar agent-learn-1.0.0-jar-with-dependencies.jar
然后启动AttachUseMain:

注意:项目运行的java的版本和AttachUseMain一样,否则会出错:Non-numeric value found - int expected

其实jstack、jmap都是通过attach方式实现,不过没有使用jar包,而是通过JVMTI的其他接口实现。
pom.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>vip.meet</groupId>
    <artifactId>agent-learn</artifactId>
    <version>1.0.0</version>
    <name>agent-learn</name>
    <description>agent-learn</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>D:/Env/JDK/Java8/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <archive>
                                <manifest>
                                    <mainClass>vip.meet.Start</mainClass>
                                </manifest>
                                <manifestEntries>
                                    <Menifest-Version>1.0</Menifest-Version>
                                    <Premain-Class>vip.meet.agent.PreMain</Premain-Class>
                                    <Agent-Class>vip.meet.attach.AgentMain</Agent-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </archive>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>



















