深入字节码操作:使用ASM和Javassist创建审核日志
原文链接:https://blog.newrelic.com/2014/09/29/diving-bytecode-manipulation-creating-audit-log-asm-javassist/
在堆栈中使用Spring和Hibernate,您的应用程序的字节码可能会在运行时被增强或处理。 字节码是Java虚拟机(JVM)的指令集,所有在JVM上运行的语言都必须最终编译为字节码。 操作字节码原因如下:
- 程序分析:
- 查找应用bug
- 检查代码复杂性
- 查找特定注解的类
- 类生成:
- 使用代理从数据库中懒惰加载数据
- 安全性
- 特定API限制访问权限
- 代码混淆
- 无Java源码类转换
- 代码分析
- 代码优化
- 最后,添加日志
有几种可用于操作字节码的工具,从非常低级的工具(如需要字节码级别工作的ASM)到诸如AspectJ等高级框架(允许编写纯Java)。
本博文,我将演示分别使用Javassist和ASM实现一种审计日志的方法。
审计日志例子
假定我没有如下代码:
public class BankTransactions {
public static void main(String[] args) {
BankTransactions bank = new BankTransactions();
for (int i = 0; i < 100; i++) {
String accountId = "account" + i;
bank.login("password", accountId, "Ashley");
bank.unimportantProcessing(accountId);
bank.withdraw(accountId, Double.valueOf(i));
}
System.out.println("Transactions completed");
}
}
我们要记录重要的操作以及关键信息以确定操作。 以上,我将确定登录退出的重要动作。 对于登录,重要信息将是帐户ID和用户。 对于退出,重要信息将是帐户ID和撤回的金额。 记录重要操作的一种方法是将日志记录语句添加到每个重要的方法,但这将是乏味的。 相反,我们可以为重要的方法添加注释,然后使用工具来注入日志记录。 在这种情况下,该工具将是一个字节码操作框架。
@ImportantLog(fields = { "1", "2" })
public void login(String password, String accountId, String userName) {
// login logic
}
@ImportantLog(fields = { "0", "1" })
public void withdraw(String accountId, Double moneyToRemove) {
// transaction logic
}
@ImportantLog
注释表示我们要在每次调用该方法时记录一条消息,而@ImportantLog
注释中的fields参数表示应记录的每个参数的索引位置。 例如,对于登录,我们要记录第1位和第2位的输入参数。它们是accountId和userName。 我们不会记录第0位的密码参数。
使用字节码和注释来执行日志记录有两个主要优点:
- 日志记录与业务逻辑分离,这有助于保持代码清洁和简单。
- 在不修改源代码的情况下,轻松删除审核日志记录。
在哪里实际修改字节码?
我们可以使用1.5中引入的核心Java功能来操纵字节码。 此功能称为Java代理。
要了解Java代理,让我们来看一下典型的Java处理流程。
使用包含我们的main方法的类作为输入参数执行命令java
。 这将启动Java运行时环境,使用classloader
来加载输入类,并调用该类的main方法。 在我们具体的例子中,调用了BankTransactions
的main方法,这将导致一些处理发生,并打印“完成交易”。
现在来看一下使用Java代理的Java进程。
命令java
运行两个输入参数。第一个是JVM参数-javaagent
,指向代理jar。第二个是包含我们主要方法的类。javaagent标志告诉JVM首先加载代理。 代理的主类必须在代理jar的清单中指定。 一旦类被加载,类的premain方法被调用。 这个premain方法充当代理的安装钩子。 它允许代理注册一个类变换器。 当类变换器在JVM中注册时,该变换器将在类加载到JVM前接收每个类的字节。 这为类变换器提供了根据需要修改类的字节的机会。 一旦类变换器修改了字节,它将修改的字节返回给JVM。 这些字节接着由JVM验证和加载。
在我们具体的例子中,当BankTransaction
加载时,字节将首先进入类变换器进行潜在的修改。修改后的字节将被返回并加载到JVM中。 加载完之后,调用类中的main方法,进行一些处理,并打印“事务完成”。
让我们来看看代码。 下面我有代理的premain方法:
public class JavassistAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Starting the agent");
inst.addTransformer(new ImportantLogClassTransformer());
}
}
premain方法打印出一个消息,然后注册一个类变换器。 类变换器必须实现方法转换,加载到JVM中的每个类都会调用它。它以该类的字节数组作为方法的输入,然后返回修改后的字节数组。如果类变换器决定不修改特定类的字节,则可以返回null。
public class ImportantLogClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
// manipulate the bytes here
return modified bytes;
}
}
现在我们知道在哪里修改一个类的字节,接着需要知道如何修改字节。
如何使用Javassist修改字节码?
Javassist是一个具有高级和低级API的字节码操作框架。我将重点关注高级的面向对象的API,首先从Javassist中的对象的解释开始。接下来,我将实现审核日志应用程序的实际代码。
Javassist使用CtClass对象来表示一个类。 这些CtClass对象可以从ClassPool获得,用于修改Classes。ClassPool是一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径。因此,在某些情况下,可能需要向ClassPool添加类路径或类字节。
类似于包含字段,方法和