仅出于好奇,除了cglib之外,还有哪些(稳定的)开源项目用于运行时Java代码生成?为什么我应该使用它们?
仅出于好奇,除了cglib之外,还有哪些(稳定的)开源项目用于运行时Java代码生成?为什么我应该使用它们?
CGLIB、ByteBuddy、kotlin编译器以及几乎所有其他库都是基于ASM构建的,ASM本身在非常底层上起作用。这对大多数人来说是个难题,因为你必须理解字节码和一点JVMS的知识才能正确使用它。但是掌握ASM肯定是非常有趣的。需要注意的是,虽然有一个ASM 4指南,但在API的某些部分中,如果有的话,Javadoc文档可能非常简洁,但正在改进中。它紧密跟随JVM版本以支持新功能,但它的缺点是不能很好地处理来自下一个JDK版本的字节码,因此可能需要等待支持这些字节码的发布版本。
然而,如果您需要完全控制,ASM是您的首选工具。它支持所有JVM字节码版本,包括Java 8语义更改中关于默认方法的一些操作码。
ByteBuddy似乎没有其他库所遇到的缺点。
高度可配置。
类型安全的流畅API。
类型安全的回调。
Javassist的建议或自定义的仪器代码是基于纯文本
String
的代码,因此在此代码中无法进行类型检查和调试,而ByteBuddy允许使用纯Java编写这些代码,因此强制进行类型检查并允许调试。
基于注解(灵活)。
用户回调可以通过注解进行配置,以便在回调中接收所需的参数。
可用作代理。
巧妙的代理构建器允许将ByteBuddy用作纯代理或附加代理。
请注意,自JDK 21以来,JDK正朝着默认情况下的默认完整性发展,这意味着动态附加将默认禁用(JEP-451),对于可服务性代理,需要添加-XX:+EnableDynamicAgentLoading
,但对于需要代理才能工作的库,必须在命令行中使用-javaagent:path/to/jar
进行声明。
非常完善的文档。
大量示例。
清晰的代码,测试覆盖率约为94%。
支持Android DEX。
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开发者来说肯定没有帮助。ClassloaderProvider
是一个静态字段,它适用于同一类加载器中的所有实例ProxyFactory
中不支持。在面向方面的一面,可以在代理中注入代码,但在Javassist中,这种方法有限且容易出错:
AspectJ是一个非常强大的工具,用于面向方面的编程(仅限于此)。AspectJ通过操作字节码来实现其目标,因此您可能能够通过它实现您的目标。然而,这需要在编译时进行操作;自从版本2.5,4.1.x,Spring提供了通过代理在加载时进行织入的功能。
关于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版本)。这个当然不是运行时的,但它是生态系统中的重要部分,大多数代码生成的用途不需要运行时创建。
这始于Java 5,它带有一个单独的命令行工具来处理注解:apt
,从Java 6开始,注解处理已集成到Java编译器中。
在某些时候,您需要显式传递处理器,现在使用ServiceLoader
方法(只需将此文件META-INF/services/javax.annotation.processing.Processor
添加到jar包中),编译器可以自动检测注解处理器。
这种代码生成方法也有缺点,它需要大量的工作和对Java语言而非字节码的理解。这个API有点繁琐,作为编译器的插件,我们必须非常小心,使这段代码最具弹性和用户友好的错误消息。
这里最大的优势是在运行时避免了另一个依赖项,可以避免permgen内存泄漏。并且对生成的代码有完全控制。我更喜欢使用原始的ASM,我相信cglib也在使用它。这是底层的东西,但文档非常好,并且一旦你习惯了它,你就会飞快。
回答你的第二个问题,当你感到反射和动态代理有点拼凑而成,需要一个非常可靠的解决方案时,你应该使用代码生成。过去,我甚至将代码生成步骤添加到Eclipse的构建过程中,从而让我在编译时报告任何事情和一切事情。
我认为使用Javassist比cglib更合理。例如,与cglib不同,javasist可以完美地处理已签名的jar包。此外,像Hibernate这样的大型项目决定停止使用cglib,转而使用Javassist。