JavaAgent的使用总结

1.背景

Java Agent出现在JDK1.5版本以后,它允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序,先从下面这张图直观的看一下它都被应用在哪些场景:

2.Premain模式

Premain模式允许在主程序执行前执行一个agent代理,实现起来非常简单,下面我们分别实现两个组成部分。

2.1 agent

先写一个简单的功能,在主程序执行前打印一句话,并打印传递给代理的参数:

public class MyPreMainAgent { 
    public static void premain(String agentArgs, Instrumentation inst) { 
        System.out.println("Starting to invoke premain"); 
        System.out.println("args:"+agentArgs); 
    } 
} 

这里我们直接使用maven插件打包的方式,在打包前进行一些配置。

	  <plugin> 
	       <groupId>org.apache.maven.plugins</groupId> 
	       <artifactId>maven-jar-plugin</artifactId> 
	       <version>3.1.0</version> 
	       <configuration> 
	           <archive> 
	               <manifest> 
	                   <addClasspath>true</addClasspath> 
	               </manifest> 
	               <manifestEntries> 
	                   <Premain-Class>MyPreMainAgent</Premain-Class>                             
	                   <Can-Redefine-Classes>true</Can-Redefine-Classes> 
	                   <Can-Retransform-Classes>true</Can-Retransform-Classes> 
	                   <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix> 
	               </manifestEntries> 
	           </archive> 
	       </configuration> 
	   </plugin>

  • Premain-Class:包含premain方法的类,需要配置为类的全路径
  • Can-Redefine-Classes:为true时表示能够重新定义class
  • Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换
  • Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀

其中Premain-Class为必须配置,其余几项是非必须选项,默认情况下都为false,执行下面Maven命令完成打包,打包结果是javaagent_test-1.jar

mvn clean package 

2.2 主程序

创立一个独立的项目名字是MainProject, 在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。

public class AgentTest { 
    public static void main(String[] args) { 
        System.out.println("main project start"); 
    } 
} 

在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下:

java -javaagent:javaagent_test-1.jar -jar MainProject.jar 

并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:

java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar 

如果是用eclipse运行的,直接在vm的参数里加就可以,注意路径

 执行结果如下:

根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。

 3. Agentmain模式

agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。

3.1 agent

agent部分和上面一样,实现简单的打印功能:

public class MyAgentMain { 
	   public static void agentmain(String agentArgs, Instrumentation instrumentation) {
		   System.out.println("MyAgentMain start..");
	        instrumentation.addTransformer(new DefineTransformer(), true);
	    }
	    
	    static class DefineTransformer implements ClassFileTransformer {
	        @Override
	        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
	            System.out.println("premain load Class:" + className);
	            return classfileBuffer;
	        }
	    }
} 

修改maven插件配置,指定Agent-Class:

<plugin> 
    <groupId>org.apache.maven.plugins</groupId> 
    <artifactId>maven-jar-plugin</artifactId> 
    <version>3.1.0</version> 
    <configuration> 
        <archive> 
            <manifest> 
                <addClasspath>true</addClasspath> 
            </manifest> 
            <manifestEntries> 
                <Agent-Class>MyAgentMain</Agent-Class> 
                <Can-Redefine-Classes>true</Can-Redefine-Classes> 
                <Can-Retransform-Classes>true</Can-Retransform-Classes> 
            </manifestEntries> 
        </archive> 
    </configuration> 
</plugin> 

3.2 主程序

这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。

public class AgentmainTest { 
 
public static void main(String[] args) throws IOException { 
System.in.read(); 
} 
} 

attach机制

和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:

<dependency> 
    <groupId>com.sun</groupId> 
    <artifactId>tools</artifactId> 
    <version>1.8</version> 
    <scope>system</scope> 
    <systemPath>${JAVA_HOME}\lib\tools.jar</systemPath> 
</dependency> 

写下面类把agent加载到主程序上面

public class TestAgentMain {

    public static void main(String[] args) throws Exception {
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.AgentmainTest")) {
                System.out.println("helloworld");
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/javaagent_test-1.jar");
                virtualMachine.detach();
            }
        }
    }
}

list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

先执行AgentmainTest类,然后执行TestAgentMain类把agent加载的主程序中,执行后结果如下:

attach实现动态注入的原理如下:

通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

4. instrument原理

猜你喜欢

转载自blog.csdn.net/keeppractice/article/details/124204861