使用 Javassist 进行类转换

小夏 科技 更新 2024-02-01

使用 j**assist 将方法转换为字节码。

现在我们已经介绍了使用反射的 J**a 格式和运行时访问,现在是本系列进入更高级主题的时候了。 本月,我将开始本系列的第二部分,其中 J**A 信息只是应用程序操作的另一种形式的数据结构。 我会把这个话题的整个事情称为课堂工作。

我将开始讨论使用 j**assist 字节码操作库的课堂工作。 J**Assist 不仅是一个字节码处理库,而且它还具有另一个功能,使其成为尝试类工作的绝佳起点。 使用 j**assist 更改 j**a 类的字节码的功能不需要您真正了解字节码或 j**a 虚拟机 jvm 的任何信息。 这个功能在某些方面有利有弊——我通常不主张使用你不了解的技术——但它确实使字节码操作比在单个指令级别工作的框架更可行。

J**Assist 允许您检查、编辑和创建 J**A 二进制类。 检查方面与通过 Reflection API 直接在 J**a 中基本相同,但是当您想要修改类而不仅仅是执行它们时,另一种访问此信息的方法很有用。 这是因为 JVM 的设计目的不是为了在类加载到 JVM 中后提供任何访问原始类数据的方法,这需要在 JVM 之外完成。

J**Assist 使用j**assist.classpool类跟踪和控制它们所操作的类。 该类的工作方式与 JVM 类装入器非常相似,但有一个重要的区别是,类池不是将已加载的可执行类作为应用程序的一部分链接,而是通过 J**Assist API 使已加载的类作为数据可用。 您可以使用从 JVM 搜索路径挂载的缺省类池,也可以定义一个类池来搜索您自己的路径列表。 您甚至可以直接从字节数组或流加载二进制类,并从头开始创建新类。

装载到类池中的类由j**assist.ctclass实例表示形式。 使用标准 J**Aj**a.lang.class类相同ctclass提供检查类数据(如字段和方法)的方法。 不过,这只是ctclass它还定义了向类添加新字段、方法和构造函数以及更改类、父类和接口的方法。 奇怪的是,J**Assist 没有提供任何删除类中的字段、方法或构造函数的方法。

字段、方法和构造函数由j**assist.ctfield、 j**assist.ctmethodj**assist.ctconstructor的实例表示形式。 这些类定义用于修改它们所表示的对象的所有方法的方法,包括方法或构造函数中的实际字节码内容。

J**Assist 允许您完全替换方法或构造函数的字节码主体,或者选择在现有主体的开头或结尾添加字节码(以及构造函数中的一些其他变量)。 无论哪种情况,新字节码都会被声明为类 j**a 的源string块被传入 。 j**assist 方法有效地将您提供的源编译为 j**a 字节码,然后将它们插入到目标方法或构造函数的主体中。

J**Assist 接受的源与 J**a 的源并不完全相同,但主要区别在于添加了一些特殊的标识符来表示方法或构造函数参数、方法返回值以及插入的 ** 中可能使用的其他内容。 这些特殊标识符由符号表示所以他们不会干扰**中的其他任何事情。

在传递给 jassist 的源代码中可以执行的操作存在一些限制。 第一个限制是使用的格式,它必须是单个语句或块。 在大多数情况下,这不是一个限制,因为您可以将所需的任何语句序列放入一个块中。 下面是一个示例,说明如何使用特殊的 j**assist 标识符来表示方法中的前两个参数:

对源的更实质性限制是,您不能引用在添加或块外声明中声明的局部变量。 这意味着,如果在方法的开头和结尾添加方法,则通常无法将信息从开头添加的内容传递到末尾添加的信息。 可以绕过此限制,但它很复杂 - 通常需要尝试将单独插入的 ** 合并到单个块中。

作为使用 J**Assist 的示例,我将使用一个通常直接在源代码中处理的任务:测量执行方法所需的时间。 这可以通过在方法开始时记录当前时间,然后在方法结束时再次检查当前时间并计算两个值之间的差值,在源中轻松完成。 如果没有来源**,获取此时间信息将更加困难。 这就是课堂作业派上用场的地方——它允许您对任何方法进行此更改,而无需主动进行更改。

清单 1 显示了一个(糟糕的)示例方法,我将其用作定时试验的实验:stringbuilderbuildstring方法。 此方法使用的方法,所有 j**a 性能优化专家都会告诉您不要使用这种方法来构造具有任意长度的构造string— 它通过在字符串末尾重复附加单个字符来生成更长的字符串。 由于字符串是不可变的,因此这种方法意味着每个新字符串都是通过循环构造的:使用从旧字符串复制的数据并在末尾添加新字符。 最终结果是,当您使用此方法生成更长的字符串时,它变得越来越昂贵。

清单 1需要计时的方法。

public class stringbuilder return result; }public static void main(string ar**)
因为此方法有一个源,所以我将向您展示如何直接添加计时信息。 在使用 j**assist 时,它也可以用作模型。 清单 2 仅显示buildstring()方法,向其添加定时函数。 这里没有太大变化。 添加的 ** 只是将开始时间保存为局部变量,然后在方法结束时计算持续时间并将其打印到控制台。

清单 2计时方法。

private string buildstring(int length) system.out.println("call to buildstring took " + system.currenttimemillis()-start) +" ms."); return result; }
使用 j**assist 操作字节码应该不难。 j**assist 提供了一种在方法的开头和结尾添加 ** 的方法,别忘了,这正是我通过向方法添加时序信息所做的。

尽管如此,还是存在障碍。 在描述 J**Assist 如何允许您添加 ** 时,我提到 ** 添加的 ** 不能引用方法中其他地方定义的局部变量。 这个限制使我无法使用与源代码中使用的相同的方法在 jassist 中实现 time,在这种情况下,我在开头的加法中定义一个新的局部变量,并在末尾的加法中引用它。

那么有没有其他方法可以达到同样的效果呢? 是的,我可以向类添加一个新的成员字段,并使用此字段而不是局部变量。 不过,这是一个糟糕的解决方案,在一般使用中有一些局限性。 例如,考虑递归方法会发生什么。 每次方法调用自身时,上次保存的开始时间值都会被覆盖并丢失。

幸运的是,有一个更简洁的解决方案。 我可以保持原始方法不变,只需更改方法名称,然后添加一个具有原始方法名称的新方法。 拦截器方法可以使用与原始方法相同的签名,包括返回相同的值。 清单 3 显示了以这种方式调整源代码后的样子:

清单 3将方法添加到源。

private string buildstring$impl(int length) return result; }private string buildstring(int length)
J**Assist可以很好地利用这种使用***方法的方法。 因为整个方法是一个块,所以我可以在正文中定义和使用局部变量,而不会出现任何问题。 为方法生成源也很容易——对于任何可能的方法,只需要一些替换。

若要实现加法方法的计时,需要使用 J**Assist 基础知识中描述的一些 J**Assist API。 清单 4 显示了 **,这是一个带有两个命令行参数的应用程序,它们提供了要计时的类名和方法名。 main()方法的主体只是提供类信息,然后将其传递给addtiming()方法处理实际修改。 addtiming()该方法首先通过将 “ 附加到 name 来完成$impl”重命名现有方法,然后使用原始方法名称创建该方法的副本。 然后,它将 copy 方法的正文替换为一个计时器,该计时器包含对重命名的原始方法的调用。 清单 4使用 j**assist 添加 *** 方法。

public class jassisttiming else }catch (cannotcompileexception ex) catch (notfoundexception ex) catch (ioexception ex) else }private static void addtiming(ctclass clas, string mname) throws notfoundexception, cannotcompileexception body.append(nname + "($$n"); // finish body text generation with call to print the timing // information, and return s**ed value (if not void) body.append("system.out.println(\"call to method " + mname + " took \" + system.currenttimemillis()-start) +" + "\" ms.\");"); if (!"void".equals(type)) body.append("}"); // replace the body of the interceptor method with generated // code block and add it to class mnew.setbody(body.tostring())clas.addmethod(mnew); // print the generated code block just to show what was done system.out.println("interceptor method body:"); system.out.println(body.tostring())
在构造 *** 方法的主体时使用一个j**a.lang.stringbuffer以累积正文文本(这显示了处理过程。string正确的施工方式,用stringbuilder施工中使用的方法是相对的)。此更改取决于原始方法是否具有返回值。 如果它有一个返回值,那么构造的 ** 会将这个值保存在一个局部变量中,以便它可以在 *** 方法的末尾返回。 如果原始方法类型为void,则不需要保存任何内容,并且不需要在 *** 方法中返回任何内容。

除了对原始(重命名)方法的调用之外,实际的正文内容看起来就像标准的 j**a 在**中一样body.append(nname + "($$n")这条线,其中nname它是原始方法的修改方法的名称。 在通话中使用标识符是 j**assist 如何表示正在构造的方法的一系列参数。 通过在对原始方法的调用中使用此标识符,可以将调用 *** 方法时提供的参数传递给原始方法。

清单 5 显示了如何首先运行未修改的stringbuilder程序,然后运行jassisttiming程序添加时序信息,最后运行修改后的stringbuilder程序的结果。 您可以看到修改后的那个stringbuilder运行时报告执行时间,您还可以看到,由于字符串构造效率低下而增加的时间**比由于构造的字符串长度增加而增加的时间要快得多。

清单 5运行此应用。

[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000constructed string of length 1000constructed string of length 2000constructed string of length 4000constructed string of length 8000constructed string of length 16000[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000call to method buildstring took 37 ms.constructed string of length 1000call to method buildstring took 59 ms.constructed string of length 2000call to method buildstring took 181 ms.constructed string of length 4000call to method buildstring took 863 ms.constructed string of length 8000call to method buildstring took 4154 ms.constructed string of length 16000
J**Assist 允许您处理源代码而不是实际的字节码指令清单,从而使课堂工作变得容易。 但这种便利性也有缺点。 正如我在所有字节码的源代码中提到的,J**Assist 使用的源代码与 J**a 语言并不完全相同。 除了识别 j**a 中的特殊标识符外,j**assist 还实现了比 j**a 语言规范要求的更宽松的编译时检查。 所以,如果你不小心,你会从源代码生成字节码,可以产生令人惊讶的结果。 例如,清单 6 显示了方法开头使用的局部变量的类型long成为int次。 J**Assist 将获取源代码并将其转换为有效的字节码,但它获得的时间毫无意义。 如果尝试直接在 j**a 程序中编译此赋值,则会出现编译错误,因为它违反了 j**a 语言的规则:缩小赋值范围需要类型覆盖。

清单 6将是一个long存储到一个int中间。

[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000call to method buildstring took 1060856922184 ms.constructed string of length 1000call to method buildstring took 1060856922172 ms.constructed string of length 2000call to method buildstring took 1060856922382 ms.constructed string of length 4000call to method buildstring took 1060856922809 ms.constructed string of length 8000call to method buildstring took 1060856926253 ms.constructed string of length 16000
根据源代码中的内容,您甚至可能让 j**assist 生成无效的字节码。 清单 7 显示了一个这样的示例,我将在此示例中:jassisttiming**已修改为始终将计时方法视为返回一个int价值。 j**assist 也会毫无问题地接受这个源,但是当我尝试执行生成的字节码时,它无法验证。

清单 7将是一个string存储到一个int中间。

[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000exception in thread "main" j**a.lang.verifyerror:(class: stringbuilder, method: buildstring signature:(i)lj**a/lang/string;) expecting to find integer on stack
只要您小心提供给 j**assist 的源,这不是问题。 但是,重要的是要认识到 j**assist 不会捕获 ** 中的所有错误,因此您可能会遇到不可预见的错误结果。

J**Assist 比我们在本文中讨论的要广泛得多。 下个月,我们将仔细研究 J**Assist 提供的一些特殊功能,用于批量修改类,以及在运行时加载类时动态修改类。 这些功能使 J**Assist 成为在您的应用程序中实现的绝佳工具,因此请务必继续关注我们,了解这个强大的工具的全部内容。

相似文章

    当样式转换正在进行时

    当你对 没有希望时,往往 可能已经开始了,大大小小的板块转换,而且款式正在进行中。大牌已经连续三天强势,跌了三年的重量级股终于在年底开始出现上涨迹象,风格能否转为蓝筹股还有待观察,但至少这三天 股强于小盘股。没有人能踩着市场的所有节奏,买一个小盘口,至少这三年不难过,所以,有短期的不适要忍受,毕竟赚...

    如何用“切入核心”造句?

    绝对 是一个成语,描述了果断 果断 毫不犹豫和坚定不移的含义。这个成语起源于 孟子 全心全意 切钉,不顾困难 形容切铁钉的果断,不顾事情的艰辛和艰辛。在现代用法中,它常被用来描述人们做事的态度和风格,表达坚决果断的决定和行动。使用示例 他对自己的工作非常投入,目标明确,在推动项目发展方面具有决定性作...

    如何用“ambition”造句?

    野心 一词是指一个人对未来渴望和追求的目标 愿望或抱负。野心通常与个人的愿望 追求 职业或成就等有关。例如 小明有志于成为一名科学家,他想为世界做出重大贡献。.她的志向是成为一名成功的企业家,并在商界取得重大成功。.这个团队致力于环境保护的雄心壮志,他们希望创造一个更可持续的未来。有抱负 的常见用法...

    “there are boards and eyes”如何造句?

    有板子和眼睛 是形容一个有条理 有条不紊 有条理的人。这个成语通常用于描述未定义 非常有纪律 没有随意或随意的行为。它也可以用来描述事物的排列或组织。这个成语以多种方式用于描述一个人或一件事,通常用于赞美或描述某人的能力 经验或组织。例如 .他在工作中总是很清醒,从不惊慌失措,而且计划周密。.她把这...

    如何使用 AI 将 PDF 转换为 Excel?3 种方式

    Excel作为日常办公文档格式之一,广泛应用于财务 审计 采购等工作中。我们经常需要将Excel转换为PDF格式以确保文档的安全性,但是当我们必须再次编辑内容时,可能会很困难。传统方法是创建一个新的 Excel 文档并手动将数据输入到窗格中,但是如果您需要处理大量数据内容怎么办?今天,我想和大家分享...