尽管Java初学者很快学会了键入public static void main
来运行他们的应用程序,但是即使是经验丰富的开发人员也常常不知道JVM对Java流程的两个附加入口点的支持: premain
和agentmain
方法。 这两种方法都允许所谓的Java代理在驻留在其自己的jar文件中的同时对现有Java程序做出贡献,即使没有被主应用程序显式链接。 这样做,有可能与托管它们的应用程序完全独立地开发,发行和发布Java代理,同时仍在同一Java进程中运行它们。
最简单的Java代理先于实际应用程序运行,例如执行一些动态设置。 代理可以例如安装特定的SecurityManager
或以编程方式配置系统属性。 下面的类是一个不太有用的代理,仍然可以作为良好的入门演示:在将控制权传递给实际应用程序的main
方法之前,该类仅将一行打印到控制台:
<pre class= "wp-block-syntaxhighlighter-code" >package sample; public class SimpleAgent<?> { public static void premain(String argument) { System.out.println( "Hello " + argument); } }< /pre >
要将此类用作Java代理,需要将其包装在jar文件中。 除常规Java程序外,无法从文件夹加载Java代理的类。 另外,需要指定一个清单条目,该清单条目引用包含premain
方法的类:
Premain-Class: sample.SimpleAgent
通过此设置,现在可以在命令行上添加Java代理,方法是指向捆绑代理的文件系统位置,并可以选择在等号后添加单个参数,如下所示:
java -javaagent:/location/of/agent.jar=世界some.random.Program
现在在some.random.Program
执行main方法之前,将打印出Hello World ,其中第二个单词是所提供的参数。
仪表API
如果抢占式代码执行是Java代理的唯一功能,那么它们当然将没有多大用处。 实际上,大多数Java代理仅是有用的,因为Java代理可以通过将类型为Instrumentation
的第二个参数添加到代理的入口点方法来请求Java代理请求。 仪器API提供对Java代理专有的JVM提供的较低级别功能的访问,而JVM从不提供给常规Java程序。 工具API的核心是允许在Java类加载之前或之后对其进行修改。
任何已编译的Java类都存储为.class文件,该文件在首次加载时以字节数组的形式呈现给Java代理。 通过将一个或多个ClassFileTransformer
注册到检测API来通知代理,该API会针对当前JVM进程的ClassLoader
加载的任何类得到通知:
package sample; public class ClassLoadingAgent { public static void premain(String argument, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(Module module, ClassLoader loader, String name, Class<?> typeIfLoaded, ProtectionDomain domain, byte[] buffer) { System.out.println( "Class was loaded: " + name); return null; } }); } }
在上面的示例中,代理通过从转换器返回null
来保持不运行状态,这使转换过程中止,但是仅将带有最近加载的类的名称的消息打印到控制台。 但是,通过转换buffer
参数提供的字节数组,代理可以在加载任何类之前更改其行为。
转换已编译的Java类可能听起来很复杂。 但是幸运的是, Java虚拟机规范(JVMS)详细说明了代表类文件的每个字节的含义。 为了修改一种方法的行为,因此将识别该方法代码的偏移量,然后向该方法添加所谓的Java字节代码指令,以表示所需的已更改行为。 通常,这种转换不是手动应用的,而是通过使用字节码处理器(最著名的是ASM库)将类文件拆分为组件的应用。 这样,就可以孤立地查看字段,方法和注释,从而可以应用更有针对性的转换并节省一些记账。
无干扰的代理
尽管ASM使类文件转换更安全,更简单,但它仍然依赖于库用户对字节码及其特征的良好理解。 但是,其他通常基于ASM的库允许在更高级别上表达字节码转换,这使得这种理解成为必然。 此类库的一个示例是Byte Buddy ,它由本文的作者开发和维护。 Byte Buddy旨在将字节码转换映射到大多数Java开发人员已经知道的概念,以使代理开发更容易上手。
为了编写Java代理,Byte Buddy提供了AgentBuilder
API,该API在ClassFileTransformer
创建并注册ClassFileTransformer
。 字节好友ClassFileTransformer
直接注册ClassFileTransformer
,而是允许指定ElementMatcher
来首先标识感兴趣的类型。 对于每种匹配类型,然后可以指定一个或多个转换。 然后,Byte Buddy将这些指令转换为可以安装到Instrumentation API中的转换器的高性能实现。 例如,以下代码在Byte Buddy的API中重新创建了先前的非运行转换器:
package sample; public class ByteBuddySampleAgent { public static void premain(String argument, Instrumentation instrumentation) { new AgentBuilder.Default() . type (ElementMatchers.any()) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { System.out.println( "Class was loaded: " + name); return builder; }).installOn(instrumentation); } }
应该提到的是,与前面的示例相反,Byte Buddy将转换所有发现的类型,而无需应用更改,而后者将完全忽略那些不需要的类型,效率较低。 另外,如果没有另外指定,默认情况下它将忽略Java核心库的类。 但是实质上,可以达到相同的效果,从而可以使用上述代码演示使用Byte Buddy的简单代理。
使用Byte Buddy建议测量执行时间
字节伙伴不是将类文件公开为字节数组,而是尝试将常规Java代码编织或链接到已检测类中。 这样,Java代理的开发人员无需直接产生字节码,而可以依赖于Java编程语言及其与之已有关系的现有工具。 对于使用Byte Buddy编写的Java代理,行为通常由建议类表示,在这些类中,带注释的方法描述了添加到现有方法的开头和结尾的行为。 例如,以下建议类用作模板,该模板将方法的执行时间打印到控制台:
public class TimeMeasurementAdvice { @Advice.OnMethodEnter public static long enter() { return System.currentTimeMillis(); } @Advice.OnMethodExit(onThrowable = Throwable.class) public static void exit (@Advice.Enter long start, @Advice.Origin String origin) { long executionTime = System.currentTimeMillis() - start; System.out.println(origin + " took " + executionTime + " to execute" ); } }
在上面的建议类中,enter方法仅记录当前时间戳,并返回该时间戳以使其在方法末尾可用。 如图所示,在实际方法主体之前执行输入建议。 在方法结束时,将应用退出建议,在该建议中,将从当前时间戳中减去所记录的值,以确定该方法的执行时间。 然后将执行时间打印到控制台。
为了利用建议,需要将其应用在先前示例中仍未运行的变压器中。 为避免打印任何方法的运行时,我们将建议的应用程序条件MeasureTime
自定义的,保留了运行时的注释MeasureTime
,应用程序开发人员可以将其添加到其类中。
package sample; public class ByteBuddyTimeMeasuringAgent { public static void premain(String argument, Instrumentation instrumentation) { Advice advice = Advice.to(TimeMeasurementAdvice.class); new AgentBuilder.Default() . type (ElementMatchers.isAnnotatedBy(MeasureTime.class)) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { return builder.visit(advice.on(ElementMatchers.isMethod()); }).installOn(instrumentation); } }
给定上述代理程序的应用程序之后,如果通过MeasureTime
注释了一个类,则现在将所有方法执行时间打印到控制台。 实际上,以更结构化的方式收集此类指标当然更有意义,但是在已经完成打印输出之后,这不再是要完成的复杂任务。
动态代理附件和类重新定义
在Java 8之前,这要归功于JDK的tools.jar中存储的实用程序,该实用程序可以在JDK的安装文件夹中找到。 从Java 9开始,此jar已分解到jdk.attach模块中,该模块现在可在任何常规JDK发行版中使用。 使用包含的工具API,可以使用以下代码将JAR文件附加到具有给定进程ID的JVM:
VirtualMachine vm = VirtualMachine.attach(processId); try { vm.loadAgent( "/location/of/agent.jar" ); } finally { vm.detach(); }
当调用上述API时,JVM将使用给定的ID定位进程,并在该远程虚拟机内的专用线程中执行agent agentmain
方法。 此外,此类代理可能会要求有权在其清单中重新转换类,以更改已加载的类的代码:
Agentmain-Class: sample.SimpleAgent Can-Retransform-Classes: true
给定这些清单条目之后,代理现在可以请求考虑将任何已加载的类进行重新转换, ClassFileTransformer
可以使用附加的布尔参数来注册先前的ClassFileTransformer
,从而指示需要在重新转换尝试时得到通知:
package sample; public class ClassReloadingAgent { public static void agentmain(String argument, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(Module module, ClassLoader loader, String name, Class<?> typeIfLoaded, ProtectionDomain domain, byte[] buffer) { if (typeIfLoaded == null) { System.out.println( "Class was loaded: " + name); } else { System.out.println( "Class was re-loaded: " + name); } return null; } }, true ); instrumentation.retransformClasses( instrumentation.getAllLoadedClasses()); } }
为了表明已经加载了一个类,现在将已加载类的实例提供给转换器,对于之前未加载的类,该实例为null
。 在以上示例的末尾,请求仪表API获取所有已加载的类,以提交任何此类类进行重新转换,从而触发转换器的执行。 和以前一样,出于演示工具API的目的,将类文件转换器实现为不可操作。
当然,Byte Buddy还通过注册重新转换策略在其API中涵盖了这种转换形式,在这种情况下,Byte Buddy还将考虑所有类别以便进行重新转换。 这样做,可以调整以前的时间测量代理程序,使其在动态连接的情况下也考虑加载的类:
package sample; public class ByteBuddyTimeMeasuringRetransformingAgent { public static void agentmain(String argument, Instrumentation instrumentation) { Advice advice = Advice.to(TimeMeasurementAdvice.class); new AgentBuilder.Default() .with(AgentBuilder.RetransformationStrategy.RETRANSFORMATION) .disableClassFormatChanges() . type (ElementMatchers.isAnnotatedBy(MeasureTime.class)) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { return builder.visit(advice.on(ElementMatchers.isMethod()); }).installOn(instrumentation); } }
为了最终方便,Byte Buddy还提供了一个用于附加到JVM的API,该API对JVM版本和供应商进行了抽象,以使附加过程尽可能地简单。 给定一个进程ID,Byte Buddy可以通过执行一行代码将代理附加到JVM:
ByteBuddyAgent.attach(processId, "/location/of/agent.jar" );
此外,甚至可以将当前正在运行的同一虚拟机进程附加到测试代理程序时特别方便的进程:
Instrumentation instrumentation = ByteBuddyAgent. install ();
此功能可以作为其自己的工件byte-buddy-agent使用 ,由于使用Instrumentation
实例可以直接(例如,从一个单元中直接调用premain或agentmain
方法)成为可能,因此自己尝试尝试自定义代理很简单。测试,无需任何其他设置。
翻译自: https://www.javacodegeeks.com/2019/12/a-beginners-guide-to-java-agents.html