Java - Instrumentation
使用JRebel启动工程时加上VM参数时有一个参数是"-javaagent:D:\jrebel_5.6.0\jrebel.jar"。
javaagent是什么? java -help后看到如下信息:
Java代理不是应用程序中的一部分,instrument支持Java以代理的形式监控或重新定义运行中的服务。
我们可以在不修改程序代码的前提下通过Instrumentation API改变运行中的java程序。
而我们使用的"-javaagent:jarpath[=options] "参数则是一种方式,也就是在应用程序启动前执行代理。
参数值是个jar路径,先让我们试着写一个代理,再将其导出为jar.
鉴于没有什么实际目的,那就以java agent的使用为重点,尽量写得简单一些。
所谓agent的代码实现就是一个带premain()方法的类,但这个方法并不是根据某个抽象,而是硬生生地定义到类里。
premain方法的声明可以是public static void premain(String args, Instrumentation inst)或 public static void premain(String args)。
参数String args对应"-javaagent:jarpath[=options] "中的options。
参数Instrumentation inst则是运行时传入的Instrumentation实例。
package pac.testcase.basic.agent; import java.lang.instrument.Instrumentation; public class TestAgent { public static void premain(String agentArgs, Instrumentation inst){ System.out.println("Alvez Agent:::"); } }
将TestAgent导出为jar,假设我的导出路径是"D:/myAgent.jar"。
随便写一个main方法,加上参数"-javaagent:D:/myAgent.jar"并运行:
package pac.testcase.basic.agent; public class TestAgentMain { public static void main(String[] args) throws InterruptedException { System.out.println("code in runtime::"); } }
结果提示:
Error occurred during initialization of VM
agent library failed to init: instrument
Failed to find Premain-Class manifest attribute in D:/myAgent.jar
需要在MANIFEST.MF中在加一条属性,手动指定premain class,比如本例中改为如下:
Manifest-Version: 1.0
Premain-Class: pac.testcase.basic.agent.TestAgent
Can-Redefine-Classes: true
接下来就可以运行了,结果输出:
Alvez Agent:::
code in runtime::
但只是这样没什么意思,我们只能在应用程序运行之前指定premain class,结果还是没有做到运行时进行改变,有没有办法在运行后指定代理程序?
程序运行时发生不曾预料的情况,此时用代理临时解决突发状况才更加有意义,但解决这一问题的并不是premain而是agentmain。
和premain相似的是,方法声明几乎相同,即public static void agentmain(String args, Instrumentation inst)或 public static void agentmain(String args)。
package pac.testcase.basic.agent; import java.lang.instrument.Instrumentation; public class TestAgent { public static void agentmain(String agentArgs, Instrumentation inst){ System.out.println("Alvez Agent:::"); } }
另外,在MANIFEST指定的属性名也改为Agent-Class,如下:
Manifest-Version: 1.0
Agent-Class: pac.testcase.basic.agent.TestAgent
Can-Redefine-Classes: true
(Ps:即使同时指定了Premain-Class和Agent-Class也不会有问题。)
和Premain不同的是,使用Agentmain时我们需要使用tools.jar中的com.sun.tools.attach.VirtualMachine,将代理jar绑定到指定VM,比如:
VirtualMachine vm = VirtualMachine.attach("");
vm.loadAgent("");
获得相应的VM,并通过loadAgent指定agentmain jar。
两者都用Instrumentation
光看了下该方法的声明,没想太多,弄了个premain方法初始化Instrumentation:
public class ObjectSizeUtils { private static Instrumentation inst = null; public static void premain(String agentArgs, Instrumentation inst) { ObjectSizeUtils.inst = inst; } public static long sizeOf(Object o){ if(inst == null) throw new IllegalStateException("not initialized yet"); return inst.getObjectSize(o); } }
export jar并在MANIFEST.MF中加上:
Premain-class: pac.testcase.utils.ObjectSizeUtils
Can-Redefine-Classes: false
随便写一个实体类,用来测试:
class Person{ private String name; private String life; public String getName() {return name;} public void setName(String name) {this.name = name;} public String getLife() {return life;} public void setLife(String life) {this.life = life;} }
好了,在运行参数中加上-javaagent,执行看看会输出什么:
@Test public void testSizeUtils(){ Person person = new Person("Alvez","make me death before you make me old"); logger.info(String.valueOf(sizeOf(person))); logger.info(String.valueOf(sizeOf(person.getName()))); logger.info(String.valueOf(sizeOf(person.getLife()))); } static final Logger logger = LoggerFactory.getLogger(ObjectSizeUtils.class);
结果很奇怪...
为什么person的属性比person还大? 等等,name和life的大小好像也有问题....
回去翻教科书发现事情并不是我希望的那样,我需要把对象里面的元素的大小全部加起来才能达到我要的效果....
看来还需要亲自解决一些事情,于是:
public class ObjectSizeUtils { private static Instrumentation inst = null; public static void premain(String agentArgs, Instrumentation inst) { ObjectSizeUtils.inst = inst; } public static long sizeOf(Object o) throws IllegalAccessException { if(inst == null) throw new IllegalStateException("not initialized yet"); SizeCounter sizeCounter = new SizeCounter(); sizeOf(o, sizeCounter); return sizeCounter.getSize(); } public static void sizeOf(Object o,SizeCounter sizeCounter) throws IllegalAccessException { if(o!=null){ sizeCounter.addSize(inst.getObjectSize(o)); Class c = o.getClass(); Field[] fields = c.getDeclaredFields(); for (Field f : fields) { int mod = f.getModifiers(); if(Modifier.isStatic(mod) || f.getType().isPrimitive()){ continue; } f.setAccessible(true); Object o1 = f.get(o); sizeOf(o1,sizeCounter); } } } } class SizeCounter{ private long size; public long getSize() { return size; } public void setSize(long size) { this.size = size; } public void addSize(long size){ this.setSize(this.getSize() + size); } }
这段代码并不完整,还有很多因素并没有考虑进去,大体思路差不多就是这样。
以下是agent jar文件的Manifest Attributes:
Premain-Class
如果 JVM 启动时指定了代理,那么此属性指定代理类,即包含 premain 方法的类。如果 JVM 启动时指定了代理,那么此属性是必需的。如果该属性不存在,那么 JVM 将中止。注:此属性是类名,不是文件名或路径。
Agent-Class
如果实现支持 VM 启动之后某一时刻启动代理的机制,那么此属性指定代理类。 即包含 agentmain 方法的类。 此属性是必需的,如果不存在,代理将无法启动。 注:这是类名,而不是文件名或路径。
Boot-Class-Path
由引导类加载器搜索的路径列表。路径表示目录或库(在许多平台上通常作为 JAR 或 zip 库被引用)。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。此属性是可选的。
Can-Redefine-Classes
布尔值(true 或 false,与大小写无关)。是否能重定义此代理所需的类。true 以外的值均被视为 false。此属性是可选的,默认值为 false。
Can-Retransform-Classes
布尔值(true 或 false,与大小写无关)。是否能重转换此代理所需的类。true 以外的值均被视为 false。此属性是可选的,默认值为 false。
Can-Set-Native-Method-Prefix
布尔值(true 或 false,与大小写无关)。是否能设置此代理所需的本机方法前缀。true 以外的值均被视为 false。此属性是可选的,默认值为 false。