有 cglib 的替代品吗?

发布于 2024-08-21 16:23:40 字数 1536 浏览 7 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

我爱人 2024-08-28 16:23:40

ASM

CGLIB、ByteBuddy、kotlin 编译器和几乎所有其他库都构建在 ASM 之上,而 ASM 本身作用于非常低的级别。对于大多数人来说,这是一个令人困惑的问题,因为您必须了解字节码和一点 JVMS 才能正确使用它。但掌握 ASM 无疑是非常有趣的。但请注意,虽然有一个很棒 ASM 4 指南 ,在 API 的某些部分,如果有 javadoc 文档,它可能会非常简洁,但它正在得到改进。它紧密遵循 JVM 版本来支持新功能,但它的缺点是无法很好地处理下一个 JDK 版本中的字节码,因此可能需要等待支持这些版本的版本。

但是,如果您需要完全控制,ASM 是您的首选武器。

该项目定期更新;截至本次编辑时,9.6 版本已于 2023 年 9 月 30 日发布,支持 JDK 22 操作码。

字节好友

Byte Buddy 是一个相当新的库,但提供了 CGLIB 或 Javassist 提供的任何功能以及更多功能。 Byte Buddy 可以完全自定义到字节代码级别,并附带一种富有表现力的领域特定语言,可实现非常可读的代码。

  • 它支持所有 JVM 字节码版本,包括有关默认方法的一些操作码的 Java 8 语义更改。

  • ByteBuddy 似乎没有遇到其他库的缺点

  • 高度可配置

  • 相当快(基准 代码)

  • 类型安全流畅的 API

  • 类型安全回调

    <块引用>

    Javassist 建议或自定义检测代码基于纯 String 中的代码,因此在此代码中不可能进行类型检查和调试,而 ByteBuddy 允许使用纯 Java 编写这些代码,因此强制执行类型检查并允许调试。

  • 注释驱动(灵活)

    <块引用>

    用户回调可以使用注释进行配置,以便在回调中接收所需的参数。

  • 可作为代理

    <块引用>

    漂亮的代理构建器允许 ByteBuddy 用作纯代理或附加代理。

    请注意,自 JDK 21 起,JDK 默认情况下倾向于完整性,这意味着默认情况下将禁用动态附件 (JEP-451),对于可服务性代理添加 -XX:+EnableDynamicAgentLoading,但是对于需要代理才能工作的库,必须声明它们在命令行上使用 -javaagent:path/to/jar

  • 有详细记录

  • 很多例子

  • 干净的代码,~94% 的测试覆盖率

  • Android DEX 支持

主要的缺点可能是 API 对于初学者来说有点冗长,但它被设计为一个选择加入的 API,形状像代理生成 DSL ;没有什么神奇的或有问题的默认值。当操作字节码时,它可能是最安全和最合理的选择。另外,有多个示例和一个大教程,这不是一个真正的问题。

2015 年 10 月,该项目获得了 Oracle Duke 选择奖。本次编辑时的最新版本是 1.14.9< /a>,并随 ASM 9.6 一起提供,并提供初步的 JDK 22 支持。

请注意 已替换 CGLIB by Byte 2.1.0 版本中的好友

JDK 的类文件 API (JEP-456)

JDK 的作者一直致力于开发JDK,JEP 提到 java.lang.classfile 包。

目标是根据当今 JDK 面临的挑战提供 API,因为类文件的发展速度比 2002 年设计 ASM 时更快。 JDK 作者在创建此 API 时希望从一个全新的角度开始,而不是获得旧的 ASM 代码库的所有权(一个显着的区别可能是 ASM 执行的分配很少,而 Classfile API 可能执行的分配要多得多,但权衡在其他地方获得(来源)。)。

在进行本次编辑时,Classfile API JEP 尚未针对 JDK 22,但很可能在某个时候(首先是预览版)登陆 JDK。

此 API 是 ASM 的替代方案,并且可能支持其他功能,但它不会提供 ByteBuddy 提供的更高级别的功能,并且肯定不会对 Android 开发人员有帮助。

Javassist

的 javadoc Javassist 比 CGLIB 好得多。类工程 API 还可以,但 Javassist 也并不完美。特别是,相当于 CGLIB 的 Enhancer 的 ProxyFactory 也有一些缺点,仅列出一些:

  • 不完全支持 Bridge 方法(即是为协变返回类型生成的)
  • ClassloaderProvider 是一个静态字段,然后它适用于同一类加载器中的所有实例
  • 自定义命名可能会受到欢迎(检查签名的 jar)
  • 没有扩展点并且几乎所有感兴趣的方法都是私有的,如果我们想要改变某些行为,这会很麻烦。
  • 虽然 Javassist 提供对类中注释属性的支持,但 ProxyFactory 不支持它们。

在面向方面方面,可以在代理中注入代码,但 Javassist 中的这种方法是有限的,并且有点容易出错:

  • 方面代码是用纯 Java 字符串编写的,该字符串在操作码
  • no 中编译类型检查
  • 没有泛型
  • 没有 lambda
  • 没有自动(取消)装箱

另外,Javassist 被认为比 Cglib 慢。这主要是由于它读取类文件的方法,而不是像 CGLIB 那样读取加载的类。以及 实现 公平地说,它本身很难读;如果需要对 Javassist 代码进行更改,则很可能会破坏某些内容。

Javassist 也遭受了不活跃的困扰,他们在 2013 年左右迁移到 github 似乎已经事实证明它很有用,因为它显示了来自社区的定期提交和拉取请求。

这些限制在 3.17.1 版本中仍然存在。版本已升至 3.20.0 版本,但 Javassist 似乎在 Java 8 支持方面仍然存在问题。

JiteScript

JiteScript 确实看起来像是一个为 ASM 很好地塑造 DSL 的新部分,这是基于最新的ASM 版本 (4.0)。代码看起来很干净。

但是该项目仍处于早期阶段,因此 API/行为可能会发生变化,而且文档也很糟糕。而且更新很少甚至被放弃。

Proxetta

这是一个相当新的工具,但它提供了迄今为止最好的人性化 API。它允许不同类型的代理,例如子类代理(cglib 方法)或编织或委托。

虽然这种情况相当罕见,但如果它运作良好,则没有任何信息。处理字节码时有很多极端情况需要处理。

AspectJ

AspectJ 是一个非常强大的工具面向方面的编程(仅限)。 AspectJ 操作字节代码来实现其目标,这样您就可以用它来实现您的目标。然而,这需要在编译时进行操作;自版本 2.54.1.x

CGLIB

关于自提出该问题以来,CGLIB 已经更新。

CGLIB 速度相当快,这是它仍然存在的主要原因之一,此外,CGLIB 的工作效果几乎比迄今为止的任何替代方案都要好(2014 年) -2015)。

一般来说,允许在运行时重写类的库必须避免在重写相应的类之前加载任何类型。因此,它们无法使用 Java 反射 API,因为 Java 反射 API 需要加载反射中使用的任何类型。相反,他们必须通过 IO 读取类文件(这会破坏性能)。这使得 Javassist 或 Proxetta 等明显慢于 Cglib,后者只是通过反射 API 读取方法并覆盖它们。

但是,CGLIB 不再处于积极开发状态。最近发布了一些版本,但许多人认为这些更改无关紧要,并且大多数人从未更新到版本 3,因为 CGLIB 引入了一些 上一个版本中的严重错误并没有真正建立信心。版本 3.1 修复了版本 3.0 的许多问题(自版本 4.0.3 Spring 框架重新打包版本 3.1)。

另外,CGLIB源代码相当很差质量,因此我们没有看到新的开发人员加入 CGLIB 项目。要了解 CGLIB 的活跃程度,请参阅他们的邮件列表

请注意,根据guice 邮件列表上的提案,CGLIB现在可以在 github 上启用为了更好地帮助该项目,它似乎正在发挥作用(多次提交和拉取请求、CI、更新的 Maven),但大多数问题仍然存在。

目前正在开发 3.2.0 版本,他们的重点是 Java 8,但到目前为止,想要 Java 8 支持的用户必须在构建时使用技巧。但进展非常缓慢。

众所周知,CGLIB 仍然受到 PermGen 内存泄漏的困扰。但其他项目可能多年来都没有经过实战考验。

编译时注释处理

这当然不是运行时,但它是生态系统的重要组成部分,大多数代码生成用法都没有不需要运行时创建。

从 Java 5 开始,Java 5 附带了单独的命令行工具来处理注释:apt,从 Java 6 开始注释处理被集成到 Java 编译器中。

有时您需要显式传递处理器,现在使用 ServiceLoader 方法(只需将此文件 META-INF/services/javax.annotation.processing.Processor 添加到jar)编译器可以自动检测注释处理器。

这种代码生成方法也有缺点,它需要大量的工作和对 Java 语言而不是字节码的理解。这个 API 有点麻烦,而且由于它是编译器中的插件,因此必须格外小心,以使此代码成为最具弹性和用户友好的错误消息。

这里最大的优点是它避免了运行时的另一个依赖,你可以避免 permgen 内存泄漏。并且可以完全控制生成的代码。

请注意,JDK 22 中的注释处理需要是显式的(-processor--processor-path--processor-module-path-proc:only-proc:full)在 javac 命令行上(JDK-8306819)。

结论

2002 CGLIB 定义了一个新的标准来轻松操作字节码。我们现在拥有的很多工具和方法(CI、覆盖率、TDD 等)当时还没有或者还不成熟。 CGLIB 成功地保持了十多年的相关性;这是一项突破性的成就。它速度很快,并且具有易于使用的 API,而不必直接操作操作码。

它定义了有关代码生成的新标准,但现在它不再是了,因为环境和需求已经改变,标准和目标也发生了变化。

JVM 在最近和未来的 Java (8 ... 11 ... 17 ... 21 ...) 版本中发生了变化,并将发生变化(动态调用、默认方法、值类型、模式匹配、巴拿马等)。 ASM 定期升级他的 API 和内部结构以遵循这些变化,但 CGLIB 和其他人尚未使用它们。

虽然注释处理越来越受到关注,但它不如运行时生成那么灵活。

截至 2015 年,Byte Buddy(尽管是新出现的)为运行时生成提供了最引人注目的卖点。更新速度不错,而且作者对 Java 字节码内部结构有深入的了解。

从 JDK 22 开始,另一种直接替代 ASM 的有趣方法是类文件 API。

ASM

CGLIB, ByteBuddy, kotlin compiler and almost all other libraries are built on top of ASM which itself acts on a very low level. This is a show-stopper for most people as you have to understand the byte code and a little bit of the JVMS to use it properly. But mastering ASM is most certainly very interesting. Note however that while there is a great ASM 4 guide, in some part of the API the javadoc documentation can be very concise if it is present at all, but it is being improved. It closely follows JVM versions to support new features, however it has a downside is cannot handle well byte codes from the next JDK release and as such may require to wait for a release supporting those.

However, if you need full control, ASM is your weapon of choice.

This project sees regular updates ; at the time of this edit version 9.6 was released on 30 September 2023, with supports for JDK 22 opcodes.

Byte Buddy

Byte Buddy is a rather new library but provides any functionality that CGLIB or Javassist provides and much more. Byte Buddy can be fully customised down to the byte code level and comes with an expressive domain specific language that allows for very readable code.

  • It supports all JVM bytecode versions, including Java 8 semantic changes of some opcodes regarding default methods.

  • ByteBuddy don't seem to suffer from the drawbacks other libraries have

  • Highly configurable

  • Quite fast (benchmark code)

  • Type safe fluent API

  • Type safe callbacks

    Javassist advices or custom instrumentation code is based on code in a plain String thus type check and debugging is impossible within this code, while ByteBuddy allows to write those with pure Java hence enforces type checks and allows debugging.

  • Annotation driven (flexible)

    The user callbacks can be configured with annotations allowing to receive the wanted parameters in the callback.

  • Available as an agent

    The nifty agent builder allows ByteBuddy to be used as a pure agent or as attaching agent.

    Note that since JDK 21, the JDK is gearing toward integrity by default, meaning that dynamic attachment will be disabled by default (JEP-451), for serviceability agent the adding -XX:+EnableDynamicAgentLoading, however for libraries that require an agent to work, they must be declared on the command line using -javaagent:path/to/jar.

  • Very well documented

  • Lots of example

  • Clean code, ~94% test coverage

  • Android DEX support

The main downside perhaps, would the API is a bit verbose for a beginner but it is designed as an opt-in API shaped as a proxy generation DSL ; there's no magic or questionable defaults. When manipulating byte code it is probably the most safe and the most reasonable choice. Also with multiple examples and a big tutorial this is not a real issue.

In October 2015 this projects received the Oracle Duke's choice award. The latest release at the time of this edit is 1.14.9, and ships with ASM 9.6 with preliminary JDK 22 support.

Note that has replaced CGLIB by Byte Buddy in version 2.1.0.

The JDK's classfile API (JEP-456)

The authors of the JDK have been working on an API within the JDK, the JEP mentions the java.lang.classfile package.

The goal is to provide an API based on today's challenge for the JDK, as classfile is evolving faster than in 2002 when ASM was designed. THE JDK authors wanted to start from a fresh perspective when they created this API, instead of taking ownership of the old ASM codebase (A significant difference might be that ASM does very few allocations, while Classfile API might do a lot more, but the tradeoff are gained elsewhere (source).).

At the time of this edit the Classfile API JEP is not yet targeted for JDK 22, but is likely to land on the JDK at some point, in preview at first.

This API is an alternative to ASM, and may support additional features, but it won;t offer the higher level features that ByteBuddy provides, and certainly won't help for Android developers.

Javassist

The javadoc of Javassist is way better than that of CGLIB. The class engineering API is OK, but Javassist is not perfect either. In particular, the ProxyFactory which is the equivalent of the CGLIB's Enhancer suffer from some drawbacks too, just to list a few :

  • Bridge method are not fully supported (ie the one that are generated for covariant return types)
  • ClassloaderProvider is a static field instead, then it applies to all instances within the same classloader
  • Custom naming could have been welcome (with checks for signed jars)
  • There is no extension point and almost all methods of interest are private, which is cumbersome if we want to change some behavior
  • While Javassist offer support for annotation attributes in classes, they are not supported in ProxyFactory.

On the aspect oriented side, one can inject code in a proxy, but this approach in Javassist is limited and a bit error-prone :

  • aspect code is written in a plain Java String that is compiled in opcodes
  • no type check
  • no generics
  • no lambda
  • no auto-(un)boxing

Also Javassist is recognized to be slower than Cglib. This is mainly due to its approach of reading class files instead of reading loaded classes such as CGLIB does. And the implementation itself is hard to read to be fair ; if one requires to make changes in the Javassist code there's many chances to break something.

Javassist suffered from inactivity as well, their move to github circa 2013 seem to have proven useful as it shows regular commits and pull requests from the community.

These limitations still stand in the version 3.17.1. Version has been bumped to version 3.20.0, yet it seems Javassist may still have issues with Java 8 support.

JiteScript

JiteScript does seem like a new piece of nicely shaping up DSL for ASM, this is based on the latest ASM release (4.0). The code looks clean.

But the project is still in his early age so API / behavior can change, plus the documentation is dire. And updates scarce if not abandoned.

Proxetta

This is a rather new tool but it offers the by far best human API. It allows for different types of proxies such as subclass proxies (cglib approach) or weaving or delegation.

Although, this one is rather rare, no information exists if it works well. There are so many corner case to deal with when dealing with bytecode.

AspectJ

AspectJ is a very powerful tool for aspect-oriented programming (only). AspectJ manipulates byte code to achieve its goals such that you might be able to achieve your goals with it. However, this requires manipulation at compile-time; spring offer weaving at load time via an agent since version 2.5, 4.1.x.

CGLIB

A word about CGLIB that has been updated since that question has been asked.

CGLIB is quite fast, it is one of the main reason why it is still around, along with the fact that CGLIB worked almost better than any alternatives until now (2014-2015).

Generally speaking libraries that allow the rewriting of classes at run time have to avoid loading any types before the corresponding class is rewritten. Therefore, they cannot make use of the Java reflection API which requires that any type used in reflection is loaded. Instead, they have to read the class files via IO (which is a performance-breaker). This makes for example Javassist or Proxetta significantly slower than Cglib which simply reads the methods via the reflection API and overrides them.

However, CGLIB is no longer under active development. There were recent releases but those changes were seen as insignificant by many and most people did never update to version 3 since CGLIB introduced some severe bugs in the last releases what did not really build up confidence. Version 3.1 fixed a lot of the woes of version 3.0 (since version 4.0.3 Spring framework repackages version 3.1).

Also, the CGLIB source code is of rather poor quality such that we do not see new developers joining the CGLIB project. For an impression of CGLIB's activeness, see their mailing list.

Note that following a proposition on the guice mailing list, CGLIB is now available on github to enable the community to better help the project, it appears to be working (multiple commits and pull requests, ci, updated maven), yet most concerns still remain.

At this time there are working on version 3.2.0, and they are focusing effort on Java 8, but so far users that want that java 8 support have to use tricks at build time. But progress is very slow.

And CGLIB is still known to be plagued for PermGen memory leak. But other projects may not have been battle tested for so many years.

Compile time annotation Processing

This one is not runtime of course, but is an important part of the ecosystem, and most code generation usage don't need runtime creation.

This started with Java 5 that came with the separate command line tool to process annotations : apt, and starting from Java 6 annotation processing is integrated into the Java compiler.

At some time you were required to explicitly pass the processor, now with the ServiceLoader approach (just add this file META-INF/services/javax.annotation.processing.Processor to the jar) the compiler can detect automatically the annotation processor.

This approach at code generation has drawbacks too it require a lot of work and understanding of the Java language not bytecode. This API is a bit cumbersome, and as one is plugin in the compiler one must take extreme care to make this code the most resilient and user friendly error message.

The biggest advantage here is that it avoids another dependency at runtime, you may avoid permgen memory leak. And one has full control on the generated code.

Note that annotation processing in JDK 22 will need to be explicit (-processor, --processor-path, --processor-module-path, -proc:only, -proc:full) on the javac command line (JDK-8306819).

Conclusion

In 2002 CGLIB defined a new standard to manipulate bytecode with ease. Many tools and methodology we have nowadays (CI, coverage, TDD, etc.) were not available or not mature at that time. CGLIB managed to be relevant for more than a decade ; that's a groundbreaking achievement. It was fast and with an easy API to use rather than having to manipulate opcodes directly.

It defined new standard regarding code generation but nowadays it isn't anymore because environment and requirements have changed, so have the standards and goals.

The JVM changed and will change in recent and future Java (8 ... 11 ... 17 ... 21 ...) versions (invokedynamic, default methods, value types, pattern matching, panama, etc). ASM upgraded his API and internals regularly to follow these changes but CGLIB and others have yet to use them.

While annotation processing is getting traction, it is not as flexible as runtime generation.

As of 2015, Byte Buddywhile rather new on the scene — offer the most compelling selling points for runtime generation. A decent update rate, and the author has an intimate knowledge of the Java byte code internals.

As of JDK 22, another interesting approach as a direct replacement of ASM would be the classfile API.

玩心态 2024-08-28 16:23:40

Javassist

如果您需要创建代理,请查看 commons-proxy - 它同时使用 CGLIB 和 Javassit。

Javassist.

If you need to make proxies, take a look at commons-proxy - it uses both CGLIB and Javassit.

爱给你人给你 2024-08-28 16:23:40

我更喜欢原始的 ASM,我相信 cglib 无论如何都使用它。它的级别较低,但文档非常棒,一旦你习惯了它,你就会飞起来。

要回答你的第二个问题,当你的反射和动态代理开始感觉有点拼凑在一起并且你需要一个坚如磐石的解决方案时,你应该使用代码生成。过去,我什至在 Eclipse 的构建过程中添加了代码生成步骤,有效地为我提供了所有内容的编译时间报告。

I prefer raw ASM, which I believe is used by cglib anyway. It's low level, but the documentation is brilliant, and once you get used to it you'll be flying.

To answer your second question, you should use code generation when your reflection and dynamic proxies are beginning to feel a bit cobbled together and you need a rock solid solution. In the past I've even added a code generation step into the build process in Eclipse, effectively giving me compile time reporting of anything and everything.

夜唯美灬不弃 2024-08-28 16:23:40

我认为使用 Javassist 而不是 cglib 更有意义。例如,与 cglib 不同,javasist 与签名 jar 完美配合。此外,像 Hibernate 这样的大型项目决定停止使用 cglib,转而使用 Javassist

I think it's more sense to use Javassist instead of cglib. E.g. javasist perfectly works with signed jars unlike cglib. Besides, such grand as Hibernate project decided to stop using cglib in favor of Javassist.

痕至 2024-08-28 16:23:40

CGLIB是十多年前在AOP和ORM时代设计和实现的。
目前,我认为没有理由使用它,并且我不再维护这个库(除了我的遗留应用程序的错误修复)。
实际上,我见过的所有 CGLIB 用例都是现代编程中的反模式。
通过任何 JVM 脚本语言(例如 groovy)实现相同的功能应该很简单。

CGLIB was designed and implemented more than ten years ago in AOP and ORM era.
Currently I see no reasons to use it and I do not maintain this library anymore (except bug fixes for my legacy applications ).
Actually all of CGLIB use cases I have ever saw are anti patterns in modern programming.
It should be trivial to implement the same functionality via any JVM scripting language e.g. groovy.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文