有没有替代品可以替代cglib?

64

仅出于好奇,除了cglib之外,还有哪些(稳定的)开源项目用于运行时Java代码生成?为什么我应该使用它们?


1
你需要一个替代方案来完成cglib无法实现的功能吗? - skaffman
1
我有一些关于可扩展ORM的想法,但运行时代码生成是必不可少的,因为每个类都将完全生成。我还没有使用过cglib,但知道它被Hibernate和Spring使用,所以这是我的第一个想法。但如果有更适合或更容易使用的东西,我也很乐意尝试。 - Mauli
2
我很高兴看到这些离题的问题仍然被索引,因为这是一个非常好的参考资料。 - markdsievers
5个回答

115

ASM

CGLIB、ByteBuddy、kotlin编译器以及几乎所有其他库都是基于ASM构建的,ASM本身在非常底层上起作用。这对大多数人来说是个难题,因为你必须理解字节码和一点JVMS的知识才能正确使用它。但是掌握ASM肯定是非常有趣的。需要注意的是,虽然有一个ASM 4指南,但在API的某些部分中,如果有的话,Javadoc文档可能非常简洁,但正在改进中。它紧密跟随JVM版本以支持新功能,但它的缺点是不能很好地处理来自下一个JDK版本的字节码,因此可能需要等待支持这些字节码的发布版本。

然而,如果您需要完全控制,ASM是您的首选工具。
该项目定期更新;在此编辑版本时,于2023年9月30日发布了9.6版本,支持JDK 22操作码。

Byte Buddy

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's choice award。在此编辑时的最新版本是1.14.9,并搭载了ASM 9.6和初步的JDK 22支持。
请注意,在2.1.0版本中取代了CGLIB by Byte Buddy

JDK的类文件API(JEP-456)

JDK的作者一直在JDK内部开发一个API,JEP中提到了java.lang.classfile包。

目标是为JDK提供一个基于当今挑战的API,因为类文件的发展速度比2002年设计ASM时要快。JDK的作者在创建这个API时希望从一个全新的角度出发,而不是接管旧的ASM代码库(一个显著的区别可能是ASM几乎不进行分配,而类文件API可能会进行更多的分配,但这种权衡在其他方面得到了补偿(来源)。)。

在编辑时,类文件API JEP尚未定位到JDK 22,但很可能在某个时候进入JDK,并首先以预览形式发布。

这个API是ASM的一个替代品,可能支持额外的功能,但不会提供ByteBuddy提供的更高级别的功能,对于Android开发者来说肯定没有帮助。
Javassist的javadoc比CGLIB的好得多。类工程API还可以,但Javassist也不完美。特别是,ProxyFactory(相当于CGLIB的Enhancer)也有一些缺点,列举几个如下:
  • 桥接方法不完全支持(即为协变返回类型生成的方法)
  • ClassloaderProvider是一个静态字段,它适用于同一类加载器中的所有实例
  • 可以欢迎自定义命名(带有签名JAR的检查)
  • 没有扩展点,几乎所有感兴趣的方法都是私有的,如果我们想要改变某些行为,这是很麻烦的
  • 虽然Javassist在类中支持注解属性,但在ProxyFactory中不支持。

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

  • 方面代码是以普通的Java字符串编写的,它被编译成操作码
  • 没有类型检查
  • 没有泛型
  • 没有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,Spring提供了通过代理在加载时进行织入的功能。

CGLIB

关于CGLIB的一点更新,自那个问题被提出以来。

CGLIB非常快速,这是它仍然存在的主要原因之一,除此之外,CGLIB在2014-2015年之前几乎比任何其他替代方案都要好。

一般来说,允许在运行时重写类的库在重写相应类之前必须避免加载任何类型。因此,它们不能使用Java反射API,该API要求在反射中使用的任何类型都已加载。相反,它们必须通过IO读取类文件(这会降低性能)。这使得例如Javassist或Proxetta比Cglib慢得多,后者只需通过反射API读取方法并覆盖它们。

然而,CGLIB已经不再进行积极的开发。最近有一些版本发布,但许多人认为这些变化微不足道,并且大多数人从未升级到3.0版本,因为CGLIB在最近的发布中引入了一些严重的错误,这并没有真正建立起信心。3.1版本解决了3.0版本的许多问题(自4.0.3版本开始,Spring框架重新打包了3.1版本)。
此外,CGLIB的源代码质量相当差,以至于我们没有看到新的开发人员加入CGLIB项目。如果想了解CGLIB的活跃程度,请参阅他们的邮件列表。
请注意,根据guice邮件列表上的提议,CGLIB现在在github上可用,以便社区更好地帮助该项目,它似乎正在运作(多次提交和拉取请求,持续集成,更新的maven),但大部分问题仍然存在。
目前他们正在开发3.2.0版本,并且他们正在致力于Java 8,但到目前为止,希望使用Java 8支持的用户在构建时必须使用一些技巧。但进展非常缓慢。
而且CGLIB仍然被认为存在PermGen内存泄漏问题。但其他项目可能没有经过如此多年的实战测试。

编译时注解处理

这个当然不是运行时的,但它是生态系统中的重要部分,大多数代码生成的用途不需要运行时创建。

这始于Java 5,它带有一个单独的命令行工具来处理注解:apt,从Java 6开始,注解处理已集成到Java编译器中。

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

这种代码生成方法也有缺点,它需要大量的工作和对Java语言而非字节码的理解。这个API有点繁琐,作为编译器的插件,我们必须非常小心,使这段代码最具弹性和用户友好的错误消息。

这里最大的优势是在运行时避免了另一个依赖项,可以避免permgen内存泄漏。并且对生成的代码有完全控制。
请注意,JDK 22中的注解处理需要在javac命令行上明确指定(-processor,--processor-path,--processor-module-path,-proc:only,-proc:full)(JDK-8306819)。
结论
在2002年,CGLIB定义了一种新的标准,以便轻松操作字节码。许多现在我们拥有的工具和方法(CI,覆盖率,TDD等)在那个时候还不可用或不成熟。CGLIB设法保持了十多年的相关性;这是一个具有突破性的成就。它快速且具有易于使用的API,而不必直接操作操作码。
它定义了关于代码生成的新标准,但现在不再是这样,因为环境和要求已经发生了变化,标准和目标也发生了变化。
JVM在最近和未来的Java(8 ... 11 ... 17 ... 21 ...)版本中发生了变化(invokedynamic、default methods、value types、pattern matching、panama等)。ASM定期升级其API和内部以跟随这些变化,但CGLIB和其他库尚未使用它们。
虽然注解处理正在受到关注,但它不如运行时生成灵活。
截至2015年,尽管Byte Buddy在舞台上相对较新,但它提供了最具吸引力的运行时生成特点。它有一个不错的更新频率,并且作者对Java字节码内部有着深入的了解。
在JDK 22中,作为ASM的直接替代,另一种有趣的方法是类文件API。

4
不要被汇编语言的低级特性吓到。一个很好的入门方法是使用内置的ASM-ifier,它会检查一个类并输出当运行时会产生该类的ASM代码,而不需要太多的字节码知识。请注意,不要改变原文意思。 - CurtainDog
非常全面和有用的回答!你几乎涵盖了所有主要的Java字节码操作库,只是缺少了Apache BCEL。 - undefined

10

Javassist是一个可以在运行时编辑字节码的Java API。如果你需要生成代理,可以使用commons-proxy,它同时使用了CGLIB和Javassit。


10

我更喜欢使用原始的ASM,我相信cglib也在使用它。这是底层的东西,但文档非常好,并且一旦你习惯了它,你就会飞快。

回答你的第二个问题,当你感到反射和动态代理有点拼凑而成,需要一个非常可靠的解决方案时,你应该使用代码生成。过去,我甚至将代码生成步骤添加到Eclipse的构建过程中,从而让我在编译时报告任何事情和一切事情。


4

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

3
这是完全错误的。如果在运行时之前不知道这些Java类,你无法使用Groovy或任何语言创建它们的代理。这是不可能的,因为Java字节码是类型安全的。运行时代码生成对于JVM非常重要,甚至还有JCL支持。 - Rafael Winterhalter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接