前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >博文精译|使用代理(Agent)的Java Bytecode Instrumentation:在运行时侵入Java应用程序(1)

博文精译|使用代理(Agent)的Java Bytecode Instrumentation:在运行时侵入Java应用程序(1)

作者头像
java达人
发布2018-12-25 15:56:45
6450
发布2018-12-25 15:56:45
举报
文章被收录于专栏:java达人java达人

作者: Vadim Klimov

译者: java达人

来源: https://blogs.sap.com/2016/03/09/java-bytecode-instrumentation-using-agent-breaking-into-java-application-at-runtime/

介绍

这篇博客中,我想描述一种技术,它可以用来灵活地更改由Java应用服务器执行的应用程序逻辑—或者更准确地说,在其服务器节点的Java虚拟机(JVM)中执行的应用程序逻辑。JVM执行预先编译和部署的与平台无关的字节码(这是Java源代码编译的结果),下面描述的技术基于字节码操作的概念。使用这种技术,几乎可以通过在已经部署的Java应用程序的字节码级别(JVM在运行时对其进行解释)上操作而对其进行任何更改,而无需修改应用程序的源代码(因为后者意味着需要重新编译、重新组装和重新部署应用程序)。

本博客还将说明为什么从原始资源(如位于应用程序服务器上由Java类加载器加载的类文件)获得的反编译代码的静态分析有时会产生误导以及为什么Java应用程序静态逆向工程结果可能不同于其观察到的运行时行为。

这种技术可能有用,那为什么不简单地对Java应用程序的源代码进行必要的更改并将其部署到应用程序服务器呢?以下是一些例子:

  • 我们没有相同的原始开发项目——例如,如果原始项目不可获得,并且反编译无法再现可成功构建和组装的完整项目结构和制品;
  • 我们需要对已运行的应用程序生成临时的补丁/逻辑修改,以便在开发和组装完整的补丁之前进行快速测试;
  • 我们需要收集关于已执行类(所有或仅选择的类)的特定运行时信息;
  • 或者我们只是想侵入已经部署的应用程序并侵入其逻辑。

该博客主要包含说明字节码instrumentation和操作的示例。关于应用程序demo,有几点需要提前说明:

  • 为了避免不相关的复杂性,示例基于一个独立的Java应用程序。由于所描述的功能是JVM特性的一部分,并不特定于应用服务器实现,因此可以在实际场景中与各种应用服务器一起使用它(SAP应用服务器就是其中之一);
  • 所有的开发都被简化了,因此代码行数被减少到合理的最小值,让我们可以关注核心主题,虽然这会导致大量使用硬编码值和简单的类模型设计。在实际开发中,大部分硬编码的值应作为可配置参数;
  • 在一个独立的程序和补充开发的类,当调用相应的对象及查看其状态时,控制台的输出被大量使用,以使信息充分方便地展示。在实际开发中,这种详细输出应该禁用,或使用具有相应日志级别/重要性的应用服务器日志框架实现。

出于可读性和清晰度,在控制台输出相应的日志条目插入以下值:

  • 程序主类调用的输出前加“[Application - main]”;
  • 来自负责在控制台显示文本的类的调用,以及来自程序main类的调用的输出前面有“[Application - text display]”;
  • 来自后续instrumentation示例的调用的输出前面有“[instrumentation]”;
  • 来自后续代理(agent)示例的调用的输出前面有“[agent]”。

为了使演示中使用的功能隔离更加明显,开发的类位于以下包中:

  • 我们将要侵入的Java应用程序和工具,位于vadim.demo.jvm.app包中;
  • Java agent位于包vadim.demo.jvm.agent中,该agent通过agent加载来演示instrumentation;
  • Java agent loader应用程序位于包vadim.demo.jvm.agent.loader中,用于演示从外部应用程序连接运行中的JVM。

相应地,instrumented的Java应用程序、Java代理和Java代理加载程序位于三个不同的Java项目中,项目结构如下:

我将从一个基本的应用程序开始,逐步增强已实现的特性,以说明讨论的主题和技术的各个实际方面,所以项目和内容将在这个博客中将逐步变化。

应用程序Demo

让我们使用以下独立的小Java程序作为未来扩充和操作的起点。该程序由两个类组成:主类DemoApplication和从主类调用的类Text。

类DemoApplication实现方法main(),是被调用Java程序的入口点:

代码语言:javascript
复制
package vadim.demo.jvm.app;
public class DemoApplication {
  public static void main(String[] args) {
  System.out.println("[Application - Main] Start application");
  String value = "Demonstration of Java bytecode manipulation capabilities";
  Text text = new Text();
  System.out.println("[Application - Main] Value passed to text display: " + value);
  text.display(value);
  System.out.println("[Application - Main] Complete application");
  }
}

从主类调用Text类,等待一秒钟后将给定的文本发送到控制台输出:

代码语言:javascript
复制
package vadim.demo.jvm.app;
public class Text {
  public void display(String text) {
  long sleepTime = 1000;
  long sleepStartTime;
  long sleepEndTime;
  System.out.println("[Application - Text display] Text display is going to sleep for " + sleepTime + " ms");
  sleepStartTime = System.nanoTime();
  try {
  Thread.sleep(sleepTime);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  sleepEndTime = System.nanoTime();
  System.out.println("[Application - Text display] Text display wakes up");
  System.out.println("[Application - Text display] Text display sleep time: "
  + ((sleepEndTime - sleepStartTime) / 1000000) + " ms");
  System.out.println("[Application - Text display] Output: " + text);
  }
}

程序执行后控制台打印如下输出:

代码语言:javascript
复制
[Application - Main] Start application
[Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities
[Application - Text display] Text display is going to sleep for 1000 ms
[Application - Text display] Text display wakes up
[Application - Text display] Text display sleep time: 1000 ms
[Application - Text display] Output: Demonstration of Java bytecode manipulation capabilities
[Application - Main] Complete application

现在让我们讲讲什么是bytecode instrumentation。

字节码instrumentation和操作

JDK从Java 5开始为开发人员提供所谓的字节码instrumentation功能。这种技术的目标是修改加载到JVM并由其执行的字节码——例如,扩展附加指令或对原始字节码的其他更改。需要注意的是,字节码instrumentation不会对字节码的原始资源(类文件)造成任何更改。当类加载器试图访问并将相应的被查找的类的字节码装入JVM时,它会动态地操纵字节码,扩展或替换从原始资源中获得的字节码,并带有instrumented版本。用于instrumentation的主接口是java.lang.instrument.Instrumentation。

Instrumentation接口提供了添加自定义转换器实现类的功能,该实现类将在类字节码加载到JVM时被触发,并且可以用动态提交的自定义字节码扩展或替换类的原始字节码。请注意,如果类已经加载,则对其字节码进行操作是无关紧要的。在技术上instrument所需的类仍然是可行,但这意味着必须为该类开发增加版的类加载器逻辑,并使用可回调的类进行扩展以实现重加载或卸载——这可能不是一项轻松任务,因为标准类加载器不提供类卸载功能。

操作字节码不同于编辑原始Java源代码,因为我们需要对编译后的JVM指令进行操作,而不是使用原始Java语句。对字节码的低干扰要求对包含字节码的class文件的结构有很好的了解。幸运的是,有几个库可以简化对字节码操作—下面是其中最常用的几个库,按生成的字节码的抽象级别分类:

字节码抽象层次

描述

示例

库需要直接在字节码级别进行操作。它们通常提供最丰富的功能,但与其他字节码操作工具相比,它们的使用也最复杂。

ASM (ASM – Home Page) BCEL (https://commons.apache.org/proper/commons-bcel/)

库根据字节码提供了一定程度的抽象,并简化了对字节码的修改。例如,不必修改字节码,可以使用类似java的语法进行更改,然后将其编译为字节码,并由使用的库将其修改为原始字节码。通常,它们缺乏对被修改代码验证的功能——这意味着,错误可能在修改准备过程中被忽略,然后在运行时被观察到。

Javassist (Javassist by jboss-javassist)

库使用高级别的指令进行操作,通常配有用于语法验证的工具集。不幸的是,对被修改的字节码进行高级别的抽象通常会丢失一些特性,这些特性通过对字节码的直接修改才会获得。

AspectJ (The AspectJ Project)

在本博客的后面的示例中,我将在修改底层字节码的必要性和抽象之间作一个折衷,使用Javassist库

让我们增强演示程序的基本逻辑并对其instrument。所提供的示例合并了几个不同的instrumentation,并说明了我们如何实现以下修改:

  • 在被instrumented的类的给定方法执行之前插入额外的代码;
  • 在被instrumented的类的给定方法执行之后插入额外的代码;
  • 在被instrumented的类的给定方法中间注入额外的代码;
  • 修改被instrumented的类的给定方法的现有代码。

几个关键的地方需要考虑

  • Javassist提供了访问编译时类定义(它是字节码的呈现版本)的功能;
  • 然后就可以迭代类方法,通过名称和描述符访问方法。请注意方法描述符的表示法——它对应的是兼容字节码的表示法,而不是Java语言规范中定义的表示法;
  • 对于给定的方法,可以在方法之前或之后插入任意代码,或者在给定的代码行插入代码。请注意语法-注入的代码行是经过一些修改的类似java的字符串(如适当转义某些特殊字符、可能使用占位符等)。在调用System.output.println()之前,我们将另一个值的赋值注入到使用的变量中,这样控制台输出的值就与从程序主类传递的值不同;
  • 也可以通过引入所谓的表达式编辑器实现类改变已经存在的字节码,它可以拦截和取代构造函数和方法调用,访问类字段,异常处理,等。在这个例子中,我们废止sleep()调用,所以这个程序不需要等待就输出文本。

有关库功能的完整文档及其使用示例,请参阅官方网站上的API参考资料。

类DemoApplication得到了相应的增强——字节码instrumentation是在方法enableInstrumentation()中实现的

代码语言:javascript
复制
package vadim.demo.jvm.app;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
public class DemoApplication {
  public static void main(String[] args) {
  System.out.println("[Application - Main] Start application");
  String value = "Demonstration of Java bytecode manipulation capabilities";
  enableInstrumentation();
  Text text = new Text();
  System.out.println("[Application - Main] Value passed to text display: " + value);
  text.display(value);
  System.out.println("[Application - Main] Complete application");
  }
  private static void enableInstrumentation() {
  String instrumentedClassName = "vadim.demo.jvm.app.Text";
  String instrumentedMethodName = "display";
  String instrumentedMethodDescriptor = "(Ljava/lang/String;)V";
  try {
  ClassPool cPool = ClassPool.getDefault();
  CtClass ctClass = cPool.get(instrumentedClassName);
  CtMethod ctClassMethod = ctClass.getMethod(instrumentedMethodName, instrumentedMethodDescriptor);
  ctClassMethod.insertBefore("System.out.println(\"[Instrumentation] Entering instrumented method\");");
  ctClassMethod.insertAfter("System.out.println(\"[Instrumentation] Exiting instrumented method\");");
  ctClassMethod.insertAt(24, true, "text = \"Original text was replaced by instrumentation from agent\";");
  ExprEditor instrumentationExpressionEditor = new DemoExpressionEditor();
  ctClassMethod.instrument(instrumentationExpressionEditor);
  ctClass.toClass();
  } catch (NotFoundException e) {
  e.printStackTrace();
  } catch (CannotCompileException e) {
  e.printStackTrace();
  }
  }
}

(SCN中的语法高亮显示功能有时会废止空行,所以请记住,上面代码中提到的代码24行对应于System.out.println()调用之前的空行,System.out.println()输出给定的文本到控制台)

此外,类DemoExpressionEditor继承了Javassist的类Javassist.expr.ExprEditor。DemoExpressionEditor实现了instrument被调用方法的逻辑

代码语言:javascript
复制
package vadim.demo.jvm.app;
import javassist.CannotCompileException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
public class DemoExpressionEditor extends ExprEditor {
  @Override
  public void edit(MethodCall method) throws CannotCompileException {
  if (method.getMethodName().contains("sleep")) {
  System.out.println("[Instrumentation] Suppressing sleep for " + method.getClassName() + "."
  + method.getMethodName() + " called from " + method.getEnclosingClass().getName());
  method.replace("{}");
  }
  }
}

我们再次执行DemoApplication,比较和原来的版本有什么不同。

代码语言:javascript
复制
[Application - Main] Start application
[Instrumentation] Suppressing sleep for java.lang.Thread.sleep called from vadim.demo.jvm.app.Text
[Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities
[Instrumentation] Entering instrumented method
[Application - Text display] Text display is going to sleep for 1000 ms
[Application - Text display] Text display wakes up
[Application - Text display] Text display sleep time: 0 ms
[Application - Text display] Output: Original text was replaced by instrumentation from agent
[Instrumentation] Exiting instrumented method
[Application - Main] Complete application

从这个输出,可以看到什么时候instrumentation实现逻辑被调用,以及它如何影响执行程序——特别是负责显示文本的类:自定义代码在被instrumented方法之前和之后执行,线程没有运行进入睡眠状态,控制台输出不是最初演示程序设计的。这一切演示了我们如何不对该类源代码进行更改,在运行时引入对某个应用程序类逻辑的较大的更改。通常我们并不局限于让调用instrumented类的应用程序触发instrumentation逻辑——它可以是运行在相同JVM中的任何其他应用程序。这里让这个程序触发instrumentation逻辑,目的是为了简化,避免过分复杂。

Java Agent 与 Attach API

到目前为止,我们已经熟悉了字节码instrumentation的一些基本原理,但是上面提供的示例仍然不够灵活——我们需要将额外的逻辑嵌入到应用程序中,或者需要部署其他应用程序instrument所需的类字节码。让我们更进一步,探索如何将instrumenting应用程序与instrumented应用程序(上面使用的Java应用程序)解耦。这种概念在JVM中已经存在了一段时间,称为Java代理。Java agent是一种以特定方式捆绑的应用程序,通常作为一个独立的JAR文件(它可能还需要额外的依赖项)交付,它包含instrumentation逻辑的实现,并且可以为了instrumentation而附加到Java应用程序。

后续翻译敬请期待......

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 应用程序Demo
  • 字节码instrumentation和操作
  • Java Agent 与 Attach API
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档