温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何使用java-agent

发布时间:2021-10-12 15:13:35 来源:亿速云 阅读:159 作者:iii 栏目:编程语言

这篇文章主要讲解了“如何使用java-agent”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何使用java-agent”吧!

静态挂载

首先编写java-agent的监控程序,静态挂载的入口函数为premain。premain函数有两种,区别是传入参数不同。通常选择带有Instrumentation参数,可以使用该变量完成代码的热替换。

public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);

下面是一个简单的例子。在premain函数中,使用Instrumentation增加一个transformer。当监控的java应用每次加载class的时候都会调用transformer。DefineTransformer是一个transformer,是ClassFileTransformer的实现。在它的transform函数的入参中会给出当前加载的类名,类加载器等信息。样例中我们只是打印了加载的类名。

import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.*; public class PreMain {       public static void premain(String agentArgs, Instrumentation inst) {         System.out.println("agentArgs : " + agentArgs);         inst.addTransformer(new DefineTransformer(), true);     }       static class DefineTransformer implements ClassFileTransformer{           @Override         public byte[] transform(ClassLoader loader,                                 String className,                                 Class<?> classBeingRedefined,                                 ProtectionDomain protectionDomain,                                 byte[] classfileBuffer){             System.out.println("premain load Class:" + className);             return classfileBuffer;         }     } }

运行java-agent需要将上述程序打包成一个jar文件,在jar文件的MANIFEST.MF中需要包含以下几项

Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.huawei.PreMain

Premain-Class声明了这个jar的premain函数所在的类,java-agent加载jar包时会在PreMain类中寻找premain。Can-Redefine-Classes与Can-Retransform-Classes声明为true,表示允许这段程序修改java应用的代码。

如果你是使用Maven的项目,可以使用增加下面的插件来自动添加MANIFEST.MF

<plugin>     <groupId>org.apache.maven.plugins</groupId>     <artifactId>maven-assembly-plugin</artifactId>     <version>2.6</version>     <configuration>         <appendAssemblyId>false</appendAssemblyId>     <descriptorRefs>         <descriptorRef>jar-with-dependencies</descriptorRef>     </descriptorRefs>     <archive>         <manifest>             <addClasspath>true</addClasspath>         </manifest>         <manifestEntries>             <Premain-Class>com.huawei.PreMain</Premain-Class>             <Can-Redefine-Classes>true</Can-Redefine-Classes>             <Can-Retransform-Classes>true</Can-Retransform-Classes>         </manifestEntries>     </archive>     </configuration>     <executions>     <execution>         <id>assemble-all</id>         <phase>package</phase>         <goals>         <goal>single</goal>         </goals>     </execution>     </executions> </plugin>

输出jar文件之后,编写一个hello world的java应用编译为hello.class,在启动应用时使用如下命令

java -javaagent:/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar hello

在执行中就可以打印java虚拟机在运行hello.class所加载的所有类。

java-agent的功能不仅限于输出类的加载过程,通过下面这个样例可以实现代码的热替换。首先编写一个测试类。

public class App  {     public static void main( String[] args )     {         try{             System.out.println( "main start!" );               App test = new App();             int x1 = 1;             int x2 = 2;             while(true){                 System.out.println(Integer.toString(test.add(x1, x2)));                 Thread.sleep(2000);             }         } catch (InterruptedException e) {             e.printStackTrace();             System.out.println("main end");         }       }       private int add(int x1, int x2){         return x1+x2;     } }

然后我们修改PreMain类中transformer,并通过Instrumentation添加这个transformer。与DefineTransformer一样。

static class MyClassTransformer implements ClassFileTransformer {         @Override         public byte[] transform(final ClassLoader loader,                                 final String className,                                 final Class<?> classBeingRedefined,                                 final ProtectionDomain protectionDomain,                                 final byte[] classfileBuffer) {             // 如果当前加载的类是我们编写的测试类,进入修改。             if ("com/huawei/App".equals(className)) {                 try {                     // 从ClassPool获得CtClass对象                     final ClassPool classPool = ClassPool.getDefault();                     final CtClass clazz = classPool.get("com.huawei.App");                       //打印App类中的所有成员函数                     CtMethod[] methodList = clazz.getDeclaredMethods();                     for(CtMethod method: methodList){                         System.out.println("premain method: "+ method.getName());                     }                       // 获取add函数并替换,$1表示函数的第一个入参                     CtMethod convertToAbbr = clazz.getDeclaredMethod("add");                     String methodBody = "{return $1 + $2 + 11;}";                     convertToAbbr.setBody(methodBody);                       // 在add函数体之前增加一段代码,同理也可以在函数尾部添加                     String methodBody = "System.out.println(Integer.toString($1));";                     convertToAbbr.insertBefore(methodBody);                       // 返回字节码,并且detachCtClass对象                     byte[] byteCode = clazz.toBytecode();                     //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载                     clazz.detach();                     return byteCode;                 } catch (Exception ex) {                     ex.printStackTrace();                 }             }             // 如果返回null则字节码不会被修改             return null;         }     }

之后的步骤与之前相同,运行会发现add函数的逻辑已经被替换了。

动态挂载

动态挂载是在应用运行过程中动态的添加agent。技术原理是通过socket与目标进程通讯,发送load指令在目标进程挂载指定jar文件。agent执行过程中的功能与静态过载是完全相同的。在实施过程中,有几点不同。首先入口函数名不同,动态挂载的函数名是agentmain。与premain类似,有两种格式。但通常采用带有Instrumentation的那种。如下例所示

public class AgentMain {       public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {         instrumentation.addTransformer(new MyClassTransformer(), true);         instrumentation.retransformClasses(com.huawei.Test.class);     }       static class MyClassTransformer implements ClassFileTransformer {         @Override         public byte[] transform(final ClassLoader loader,                                 final String className,                                 final Class<?> classBeingRedefined,                                 final ProtectionDomain protectionDomain,                                 final byte[] classfileBuffer) {             // 如果当前加载的类是我们编写的测试类,进入修改。             if ("com/huawei/App".equals(className)) {                 try {                     // 从ClassPool获得CtClass对象                     final ClassPool classPool = ClassPool.getDefault();                     final CtClass clazz = classPool.get("com.huawei.App");                       //打印App类中的所有成员函数                     CtMethod[] methodList = clazz.getDeclaredMethods();                     for(CtMethod method: methodList){                         System.out.println("premain method: "+ method.getName());                     }                       // 获取add函数并替换,$1表示函数的第一个入参                     CtMethod convertToAbbr = clazz.getDeclaredMethod("add");                     String methodBody = "{return $1 + $2 + 11;}";                     convertToAbbr.setBody(methodBody);                       // 返回字节码,并且detachCtClass对象                     byte[] byteCode = clazz.toBytecode();                     //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载                     clazz.detach();                     return byteCode;                 } catch (Exception ex) {                     ex.printStackTrace();                 }             }             // 如果返回null则字节码不会被修改             return null;         }     } }

功能与静态加载相同。需要注意的是,Instrumentation增加了transformer之后,调用了retransformClasses函数。这是由于transformer只有在Java虚拟机加载class时才会调用。如果是通过动态加载的方式,需要监控的class文件可能已经加载完成了。所以需要调用retransformClasses重新加载。

另外一点不同是MANIFEST.MF文件需要添加Agent-Class,如下所示

Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.huawei.PreMain Agent-Class: com.huawei.AgentMain

最后一点不同是加载方式不同。动态挂载需要编写一个加载脚本。如下所示,在这段脚本中,首先遍历所有的java进程,通过启动类名辨识需要监控的进程。通过进程id获取VirtualMachine实例,并加载agentmain的jar文件。

import com.sun.tools.attach.*; import java.io.IOException; import java.util.List;   public class TestAgentMain {       public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{         //获取当前系统中所有 运行中的 虚拟机         System.out.println("running JVM start ");         List<VirtualMachineDescriptor> list = VirtualMachine.list();         for (VirtualMachineDescriptor vmd : list) {               System.out.println(vmd.displayName());             String aim = "com.huawei.App";             if (vmd.displayName().endsWith(aim)) {                 System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id()));                 VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                 virtualMachine.loadAgent("/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar");                 virtualMachine.detach();             }         }     } }

Scala程序监控

Scala与Java兼容性很好,所以使用java-agent监控scala应用也是可行的。但是仍然需要注意一些问题。第一点是程序替换只对class有作用,对object是无效的。第二个问题是,动态替换中是将程序编译为字节码之后再去替换的。java-agent使用的是java的编译规则,所以替换程序要使用java的语言规则,否则会出现编译错误。例如示例中使用System.out.println输出参数信息,如果使用scala的println会出现编译错误。

感谢各位的阅读,以上就是“如何使用java-agent”的内容了,经过本文的学习后,相信大家对如何使用java-agent这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI