3年前 (2021-09-21)  Java系列 |   抢沙发  2740 
文章评分 1 次,平均分 5.0

虽然Java初学者很快就学会了键入public static void main来运行他们的应用程序,但即使是经验丰富的开发人员也常常不知道JVM对Java进程的另外两个入口点的支持:premainagentmain方法。这两种方法都允许所谓的Java代理在驻留在自己的jar文件中时对现有Java程序作出贡献,即使主应用程序没有显式链接。这样,就可以完全独立于承载Java代理的应用程序开发、发布和发布Java代理,同时仍在同一Java进程中运行它们。

最简单的Java代理在实际应用程序之前运行,例如执行一些动态设置。例如,代理可以安装特定的SecurityManager或以编程方式配置系统属性。一个不太有用的代理仍然是一个很好的介绍性演示,它是下面的类,在将控制权传递给实际应用程序的主方法之前,它只是将一行打印到控制台:

package sample;
public class SimpleAgent<?> {
  public static void premain(String argument) {
    System.out.println("Hello " + argument);
  }
}

要将此类用作Java代理,需要将其打包到jar文件中。除了常规Java程序之外,不可能从文件夹加载Java代理的类。此外,还需要指定引用包含premain方法的类的清单条目:

Premain-Class: sample.SimpleAgent

通过此设置,现在可以在命令行上添加Java agent,方法是指向捆绑代理的文件系统位置,并可以选择在等号后添加单个参数,如中所示:

java-javaagent:/location/of/agent.jar=World some.random.Program

现在,在some.random.Program中执行main方法之前将打印Hello World,其中第二个单词是提供的参数。

THE INSTRUMENTATION API

如果抢占式代码执行是Java agent的唯一功能,那么它们当然用处不大。实际上,大多数Java agent之所以有用,只是因为Java agent可以通过向代理的入口点方法添加第二个Instrumentation类型的参数来请求Instrumentation API。插装API提供了对JVM提供的低级功能的访问,JVM是Java agent所独有的,并且从来没有提供给常规Java程序。作为其核心,instrumentation API允许在加载Java类之前甚至之后修改它们。

任何编译后的Java类都存储为.class文件,每当第一次加载该类时,该文件将作为字节数组呈现给Java agent。通过将一个或多个ClassFileTransformer注册到instrumentation API中来通知代理,对于当前JVM进程的类加载器加载的任何类,都会通知这些类:

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;
       }
    });
  }
}

在上面的示例中,代理通过从transformer返回null保持不运行,what中止转换过程,但只向控制台打印带有最近加载的类名称的消息。但通过转换缓冲区参数提供的字节数组,代理可以在加载任何类之前更改其行为。

转换已编译的Java类听起来像是一项复杂的任务。但幸运的是,Java虚拟机规范(JVM)详细说明了表示类文件的每个字节的含义。因此,要修改方法的行为,需要识别方法代码的偏移量,然后向该方法添加所谓的Java字节码指令,以表示所需的已更改行为。通常,这种转换不是手动应用的,而是使用字节码处理器,最著名的是ASM库,它将类文件拆分为其组件。这样,就可以孤立地查看字段、方法和注释,从而可以应用更有针对性的转换,并节省一些簿记。

DISTRACTION-FREE AGENT DEVELOPMENT

虽然ASM使类文件转换更安全、更简单,但它仍然依赖于库用户对字节码及其特性的良好理解。然而,通常基于ASM的其他库允许在更高的级别上表达字节码转换,这使得这种理解成为可能。这类库的一个例子是Byte Buddy,它是由本文作者开发和维护的。Byte Buddy旨在将字节码转换映射到大多数Java开发人员已经知道的概念,以便使代理开发更容易实现。

为了编写Java代理,Byte Buddy提供了AgentBuilder API,它在封面下创建并注册ClassFileTransformer。与直接注册ClassFileTransformer不同,Byte Buddy允许指定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测量执行时间

byte Buddy尝试将常规Java代码编织或链接到插入指令的类中,而不是将类文件公开为字节数组。通过这种方式,Java  agent的开发人员不需要直接生成字节码,而是可以依赖Java编程语言及其与之有关系的现有工具。对于使用Byte-Buddy编写的Java  agent,行为通常由advice类表示,其中带注释的方法描述添加到现有方法开头和结尾的行为。例如,以下通知类用作模板,其中方法的执行时间打印到控制台:

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");
  }
}

在上面的advice类中,enter方法只记录当前时间戳并返回它,以便在方法末尾使其可用。如图所示,enteradvice在实际方法体之前执行。在方法的末尾,退出建议被应用于从当前时间戳中减去记录的值以确定方法的执行时间。然后将此执行时间打印到控制台。

为了利用建议,需要在前一示例中保持不运行的变压器内应用建议。为了避免打印任何方法的运行时,我们将通知的应用程序设置为自定义的、由运行时保留的注释度量时间,应用程序开发人员可以将该时间添加到他们的类中。

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注释,则所有方法执行时间现在都打印到控制台。事实上,以更结构化的方式收集此类指标当然更有意义,但在已经实现打印输出后,这不再是一项需要完成的复杂任务。

动态代理连接和类重新定义

在Java8之前,由于存储在JDK的tools.jar中的实用程序(可以在JDK的安装文件夹中找到),这是可能的。自Java9以来,这个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的进程,并在该远程虚拟机内的专用线程中执行agents-agentmain方法。此外,此类代理可能会请求在其清单中重新转换类的权利,以更改已加载的类的代码:

Agentmain-Class: sample.SimpleAgent
Can-Retransform-Classes: true

给定这些清单条目,代理现在可以请求考虑重新传输任何加载的类,以便使用附加的布尔参数注册以前的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。在上述示例的末尾,请求instrumentation API获取所有加载的类,以提交任何此类类,以便重新传输触发转换器执行的内容。与前面一样,为了演示instrumentation 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");

此外,甚至可以连接到当前正在运行的虚拟机进程,该进程在测试agents时特别方便:

Instrumentation instrumentation = ByteBuddyAgent.install();

此功能可作为其自己的byte-buddy-agent使用,并且应该使您自己尝试自定义代理变得简单,因为检测实例可以直接调用premainagentmain方法,例如从单元测试,而无需任何额外设置。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2363.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册