什么是Instrumentation? Java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。 Java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理机制,支持独立于JVM应用程序之外的程序以代理的方式连接和访问JVM。java.lang.instrument是在JVM TI的基础上提供的Java版本的实现。 Instrumentation提供的主要功能是修改jvm中类的行为。 Java SE6中由两种应用Instrumentation的方式,premain和agentmain(运行时)
premain方式 在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)的main方法启动前启动一个代理程序。例如使用如下命令:java -javaagent:agent_jar_path[=options] java_app_name 可以在启动名为java_app_name的应用之前启动一个agent_jar_path指定位置的agent jar。 实现这样一个agent jar包,必须满足两个条件: 1. 在这个jar包的manifest文件中包含Premain-Class属性,并且该属性的值为代理类全路径名。 2. 代理类必须提供一个 public static void premain(String args, Instrumentation inst) 或 public static void premain(String args) 方法。
当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()获得的加载器)加载代理类。在执行main方法前执行premain()方法。如果premain(String args, Instrumentation inst)和premain(String args)同时存在时,优先使用前者。其中方法参数args即命令中的options,类型为String(注意不是String[]),因此如果需要多个参数,需要在方法中自行处理(比如用”;”分割多个参数之类);inst是运行时由VM自动传入的Instrumentation实例,可以用于获取VM信息。
premain实例-打印所有的方法调用 premain方式的agent类必须提供premain方法,代码如下:
1 2 3 4 5 6 public class Agent { public static void premain(String args, Instrumentation inst){ System.out.println("Hi, I'm agent!"); inst.addTransformer(new Transformer()); } }
premain有两个参数,args为自定义传入的代理类参数,inst为VM自动传入的Instrumentation实例。Transformer是自定义的类的转换器,用于转换类的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Transformer implements ClassFileTransformer { public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { if (className.startsWith("java" ) || className.startsWith("javax" ) || className.startsWith("sun" ) || className.startsWith("jdk" ) || className.startsWith("com/sun" ) || className.startsWith("com/intellij" )) { return null ; } ClassReader cr = new ClassReader (classfileBuffer); ClassNode cn = new ClassNode (); cr.accept(cn, 0 ); for (Object obj : cn.methods) { MethodNode md = (MethodNode) obj; if ("<init>" .endsWith(md.name) || "<clinit>" .equals(md.name)) { continue ; } InsnList insns = md.instructions; InsnList il = new InsnList (); il.add(new FieldInsnNode (Opcodes.GETSTATIC, "java/lang/System" , "out" , "Ljava/io/PrintStream;" )); il.add(new LdcInsnNode ("invoke -> " + cn.name + "." + md.name)); il.add(new MethodInsnNode (Opcodes.INVOKEVIRTUAL, "java/io/PrintStream" , "println" , "(Ljava/lang/String;)V" )); insns.insert(il); md.maxStack += 3 ; } ClassWriter cw = new ClassWriter (0 ); cn.accept(cw); return cw.toByteArray(); } }
Transformer实现了ClassFileTransformer接口,该接口只有一个transform方法,参数传入包括该类的类加载器,类名,原字节码字节流等,返回被转换后的字节码字节流。 当方法返回后,Java虚拟机会使用所返回的byte数组,来完成接下来的类加载工作;返回null或者抛出异常,那么Java虚拟机将使用原来的byte数组完成类加载工作。 上面的例子,主要使用ASM实现在所有的类定义的方法中,在方法开始出添加了一段打印该类名和方法名的字节码。
设置MANIFEST.MF文件中的属性,文件内容如下:
1 2 3 4 5 6 Manifest-Version: 1.0 Premain-Class: com.zero.AgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_171
maven配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <version > 3.1.0</version > <configuration > <archive > <manifestEntries > <Project-name > ${project.name}</Project-name > <Project-version > ${project.version}</Project-version > <Agent-Class > com.zero.AgentMain</Agent-Class > <Premain-Class > com.zero.AgentMain</Premain-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > </manifestEntries > </archive > </configuration > </plugin >
agentmain方式 premain时Java SE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有不便,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较困难的。 比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。 为解决运行时启动代理类的问题,Java SE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与Permain类似,agent方式同样需要提供一个agent jar,并且这个jar需要满足:
在manifest中指定Agent-Class属性,值为代理类全路径
代理类需要提供public static void agentmain(String args, Instrumentation inst)或public static void agentmain(String args)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。
不过如此设计的再运行时进行代理有个问题——如何在应用程序启动之后再开启代理程序呢? JDK6中提供了Java Tools API,其中Attach API可以满足这个需求。Attach API中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar。
agentmain实例-打印当前已加载的类
1 2 3 4 5 6 7 8 9 10 11 12 13 package loaded; import java.lang.instrument.Instrumentation; public class AgentMain { @SuppressWarnings("rawtypes") public static void agentmain (String args, Instrumentation inst) { Class[] classes = inst.getAllLoadedClasses(); for (Class cls :classes){ System.out.println(cls.getName()); } } }
设置MANIFEST.MF文件,指定Agent-Class:
1 2 3 4 5 6 Manifest-Version: 1.0 Agent-Class: com.zero.AgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_171
将agent类和MANIFEST.MF文件编译打成agent.jar后,由于agent main方式无法向pre main方式那样在命令行指定代理jar,因此需要借助Attach Tools API。
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException { VirtualMachine vm = VirtualMachine.attach(args[0 ]); vm.loadAgent("/Users/xxx/agent.jar" ); } }
该程序接受一个参数为目标应用程序的进程id,通过Attach Tools API的VirtualMachine.attach方法绑定到目标VM,并向其中加载代理jar。
在 agent 的 manifest 里加入 Boot-Class-Path 其实一样可以在动态地载入 agent 的同时加入自己的 boot class 路径。
附:agent jar中manifest的属性 Premain-Class: 当在VM启动时,在命令行中指定代理jar时,必须在manifest中设置Premain-Class属性,值为代理类全类名,并且该代理类必须提供premain方法。否则JVM会异常终止。 Agent-Class: 当在VM启动之后,动态添加代理jar包时,代理jar包中manifest必须设置Agent-Class属性,值为代理类全类名,并且该代理类必须提供agentmain方法,否则无法启动该代理。 Boot-Class-Path: Bootstrap class loader加载类时的搜索路径,可选。 Can-Redefine-Classes: true/false;标示代理类是否能够重定义类。可选。 Can-Retransform-Classes: true/false;标示代理类是否能够转换类定义。可选。 Can-Set-Native-Prefix:true/false;标示代理类是否需要本地方法前缀,可选。
当一个代理jar包中的manifest文件中既有Premain-Class又有Agent-Class时,如果以命令行方式在VM启动前指定代理jar,则使用Premain-Class;反之如果在VM启动后,动态添加代理jar,则使用Agent-Class
Caused by: java.lang.UnsupportedOperationException: redefineClasses is not supported in this environment 通过在MANIFEST.MF增加配置Can-Redefine-Classes: true解决
Caused by: java.lang.UnsupportedOperationException: adding retransformable transformers is not supported in this environment 通过在MANIFEST.MF增加配置Can-Retransform-Classes: true解决
应用场景:
监控,例如APM
代码注入
故障模拟,例如对某个method改造抛异常
流量录制
在线dug,例如arthas
转载自:https://blog.csdn.net/productshop/article/details/50623626