Javaagent学习
来源:互联网 发布:苹果远程桌面连接软件 编辑:程序博客网 时间:2024/06/07 13:20
最近团队在搭建开源的监控系统,使用到了这个工具,突然发现这个工具设计很优雅,对要监控的JAVA项目是无侵入的,只需要在被监控的应用的启动参数中,增加一段代码即可,实现的原理就是利用javaagent特性。
以前很少接触过javaagent的知识,项目中也很少有这方面实践的机会,于是想自己亲自动手实践下,并且加深对java agent的理解。
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理,以往对原有的类实现代理功能,都是通过AOP实现方法的拦截,并增加一些增强的逻辑,或者通过cglib、javaassit动态生成类的字节码来实现代理,但是这种实现方式都需要对本地工程的代码作一定程度的修改,如:增加一些配置来定义要代理哪些目标类,或者直接在当前工程中直接扩展目标类的实现等等。这种做法虽然使用起来很方便,但是对已有的工程代码都是有侵入性,那么想做到无侵入,还想实现代理功能,如何实现呢?JavaAgent应运而生。
java.lang.instrument是Java SE 5 的新特性,你可以由此实现一个java agent。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的更松耦合的AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
想了解JavaAgent原理的同学请移步:
https://yq.aliyun.com/articles/2946?spm=5176.100239.yqblog1.45
不多废话了,进入正题吧:)
实现Java Agent大概需要如下几个步骤:
1、编写 premain 函数
编写一个 Java 类,包含如下两个方法当中的任何一个
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
其中,[1] 的优先级比 [2] 高,不能就是说[1] 和 [2] 同时存在时,[1]将会被优先执行,而[2] 被忽略。
在这个 premain 函数中,开发者可以进行对类的各种操作。
agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序需要自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
代理类的代码很简单,其中的instrumentation.addTransformer(new Transformer());这行先忽略,下文继续会提到。
public class PremainAgent { public static void premain(String args, Instrumentation instrumentation) { System.out.println("代理类开始执行...,参数:" + args); // instrumentation.addTransformer(new Transformer()); System.out.println("代理类执行结束..."); }}
2、将你的应用程序打包
由于javaagent只能通过java -jar的方式运行,因此需要将你的应用程序打成jar包,且你的jar包中的manifest文件中须包含Premain-Class属性,并且值为代理类全限定名。
当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()获得的加载器)加载代理类。在执行main方法前执行premain()方法。
MANIFEST文件:
需要额外添加premain-classs: 你的代理类的全路径名
Manifest-Version: 1.0premain-class: com.ws.demo.javaagent.agent.PremainAgentArchiver-Version: Plexus ArchiverBuilt-By: AdministratorClass-Path: lib/javassist-3.8.0.GA.jarCreated-By: Apache Maven 3.0.5Build-Jdk: 1.8.0_144Main-Class: com.ws.demo.javaagent.Main
如果你使用的是Maven,可以使用maven的插件来实现,这样MANIFEST.MF文件中自动添加了相应的配置。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.6</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.ws.demo.javaagent.Main</mainClass> </manifest> <manifestEntries> <premain-class>com.ws.demo.javaagent.agent.PremainAgent</premain-class> </manifestEntries> </archive> </configuration></plugin>
附上工程中使用到的类:
一个简单的业务接口:
package com.ws.demo.javaagent.biz;public interface Cache { void put(String key, String value);}
实现类:
package com.ws.demo.javaagent.biz.impl;import com.ws.demo.javaagent.biz.Cache;public class RedisCache implements Cache { @Override public void put(String key, String value) { System.out.println(String.format("RedisCache put [ Key:%s | Value:%s ]", key, value)); }}
应用程序入口类:Main
package com.ws.demo.javaagent;import com.ws.demo.javaagent.biz.Cache;import com.ws.demo.javaagent.biz.impl.GuavaCache;public class Main { public static void main(String[] args) { Cache cache = new GuavaCache(); cache.put("demo", "abc"); }}
3、运行代理程序
**执行如下命令,会在你的应用程序(包含main方法的程序)启动前启动一个代理程序。
java -javaagent:agent_jar_path[=options] java_app_name
比如我当前的工程打包后生成了javaagent-1.0-SNAPSHOT.jar,为了方便起见,我把我的代理类和Main类在一个工程,所以执行javaagent命令时,后面跟的jarpath是一个,贴上代码:
java -javaagent:javaagent-1.0-SNAPSHOT.jar=demo -jar javaagent-1.0-SNAPSHOT.jar
执行成功, 输出:
代理类开始执行...,参数:demo代理类执行结束...GuavaCache put [ Key:demo | Value:abc ]
可以看到,先执行了代理,之后才执行了主应用程序,在这里注意一下,javaagent支持多个代理类,即:
java -javaagent:agent1.jar -javaagent:agent2.jar java_app_name -javaagent:agent3.jar
但是如果在你的app_name后面加的代理类是不生效的,也就是agent1、agent2可以正常被执行,但是执行不了agent3
下面我们补充之前说的关于instrumentation.addTransformer(new Transformer());
这段代码,我们把注释放开。
public class PremainAgent { public static void premain(String args) { } public static void premain(String args, Instrumentation instrumentation) { System.out.println("代理类开始执行...,参数:" + args); instrumentation.addTransformer(new Transformer()); System.out.println("代理类执行结束..."); }}
上文已经提到参数是JVM自动实例化并传入的,并且使用了和你的Main类同一个ClassLoader,这就使得应用中的类加载的时候, Transformer.transform方法都会被调用,并且可以利用这个特性动态的改变加载的主程序中类的逻辑,接下来我们利用这个特性来写个小DEMO。
其中Transformer是我的一个实现类,上代码:
package com.ws.demo.javaagent.transformer;import javassist.*;import javassist.expr.ExprEditor;import javassist.expr.MethodCall;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class Transformer implements ClassFileTransformer { // 实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构 // loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到) // className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer" // classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null // protectionDomain:要定义或重定义的类的保护域 // classfileBuffer:类文件格式的输入字节缓冲区(不得修改) // 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。 @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { byte[] transformed = null; ClassPool pool = ClassPool.getDefault(); CtClass cl = null; try { cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer)); if (cl.isInterface() == false) { CtBehavior[] methods = cl.getDeclaredBehaviors(); for (CtBehavior ctBehavior : methods) { if (!ctBehavior.isEmpty()) { doMethod(ctBehavior); } } transformed = cl.toBytecode(); } } catch (Exception e) { System.err.println("Could not instrument " + className + ", exception : " + e.getMessage()); } finally { if (cl != null) { cl.detach(); } } return transformed; } private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException { method.instrument(new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { m.replace("{ long stime = System.currentTimeMillis(); $_ = $proceed($$); System.out.println(\"" + m.getClassName() + "." + m.getMethodName() + " cost:\" + (System.currentTimeMillis() - stime) + \" ms\");}"); } }); }}
通过阅读代码,不难发现,其主要的逻辑就是使用Javassist动态的修改加载的类的字节码,在执行完方法后,以输出了这个方法的耗时。
关于java字节码的处理,目前有很多工具,如bcel,asm(cglib只是对asm又封装了一层),javassist,这里我们使用最后一个来作演示。打包执行下:
java -javaagent:javaagent-1.0-SNAPSHOT.jar -jar javaagent-1.0-SNAPSHOT.jar
这里要特殊说明下,因为我们的程序使用到了Javassist这个包,所以需要指定依赖的类的路径,否则会因为找不到类导致你的代理程序无法正确执行,方法是在MANIFEST.MF中增加下边的代码:
Boot-Class-Path: E:/.m2/repository/jboss/javassist/3.8.0.GA/javassist-3.8.0.GA.jar
当然你可以使用MAVEN提供的插件功能来实现,方法在POM.XML中配置:
<manifestEntries> <premain-class>com.ws.demo.javaagent.agent.PremainAgent</premain-class> <Boot-Class-Path>E:/.m2/repository/jboss/javassist/3.8.0.GA/javassist-3.8.0.GA.jar</Boot-Class-Path> </manifestEntries>
结果:
看,所有方法的执行时间都被打印出来了,我们在没修改主程序的任何代码就实现了AOP,是不是很神奇。如果大家想更多地了解Instrumentation,可以参考:
http://blog.csdn.net/songshuaiyang/article/details/50732345
~~~好了,先写这么多,自己学习的同时,也希望可以对刚接触javaagent的同学起到一个启蒙的作用。
如有写的不妥当的地方,还望指正,谢谢!
- Javaagent学习
- javaagent
- javaagent
- javaAgent 参数
- 配置javaagent
- javaagent 参数使用
- javaagent引发的ClassDefNotFoundException
- javaagent 参数使用
- javaagent 使用以及陷阱
- ClassLoader, JavaAgent, Aspectj Weaving
- JavaAgent(转载)
- JavaAgent 简单例子
- instrumentation 功能介绍(javaagent)
- javaAgent说明和例子
- javaAgent简单使用
- Instrumentation 功能介绍(javaagent)
- ClassLoader, JavaAgent, Aspectj Weaving一站式扫盲帖
- ClassLoader, JavaAgent, Aspectj Weaving一站式扫盲帖
- Struts数据处理(获取request对象)
- Servlet3.0研究之ServletContainerInitializer接口
- LIBSVM-3.22典型安装图解
- regex--python
- Java中继承与组合
- Javaagent学习
- mysql 五大常见高可用方案
- ngx_process_events_and_timers
- 【51单片机】INT0及INT1中断计数
- Container With Most Water--LeetCode
- Opencv 2.4.9在Ubuntu下的配置安装
- 四大组件之broadcastReceiver
- clang忽略警告
- MySQL中select * for update锁表的问题 由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例