Search code examples
javacode-generationcglib

Are there alternatives to cglib?


Just out of curiosity, are there any (stable) open source projects for runtime java code generation other than cglib? And why should I use them?


Solution

  • 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.