Making Java More Dynamic: Runtime Code Generation for JVM
In Java, runtime code generation is a powerful technique that allows for dynamic behavior through manipulating classes and methods at runtime. By employing frameworks and annotations, such as @Secured, developers can enhance security and functionality in their applications. Leveraging classes like UserHolder and Service, and techniques like method redefinition, the Java codebase can be made more flexible and secure. While reflection offers some capabilities, runtime code generation proves beneficial for user type enhancement, paving the way for more dynamic Java programming. This approach, despite its challenges, provides insights into improving code modularity and testing efficiency.
Download Presentation

Please find below an Image/Link to download the presentation.
The content on the website is provided AS IS for your information and personal use only. It may not be sold, licensed, or shared on other websites without obtaining consent from the author.If you encounter any issues during the download, it is possible that the publisher has removed the file from their server.
You are allowed to download the files provided on this website for personal or commercial use, subject to the condition that they are used lawfully. All files are the property of their respective owners.
The content on the website is provided AS IS for your information and personal use only. It may not be sold, licensed, or shared on other websites without obtaining consent from the author.
E N D
Presentation Transcript
Making Java more dynamic: runtime code generation for the JVM
interface Framework { <T> Class<? extends T> secure(Class<T> type); } @interface Secured { String user(); } discovers at runtime class UserHolder { static String user= "ANONYMOUS"; } depends on does not know about class Service { @Secured(user = "ADMIN") void deleteEverything(){ // delete everything... } }
class Service { @Secured(user = "ADMIN") void deleteEverything(){ if(!"ADMIN".equals(UserHolder.user)){ thrownew IllegalStateException("Wrong user"); } // delete everything... } } } class SecuredService extends Service { @Override void deleteEverything(){ if(!"ADMIN".equals(UserHolder.user)){ thrownew IllegalStateException("Wrong user"); } super.deleteEverything(); } redefine class (build time, agent) create subclass (Liskov substitution) class Service { @Secured(user = "ADMIN") void deleteEverything(){ // delete everything... } }
source code javac scalac groovyc jrubyc creates reads 0xCAFEBABE byte code class loader interpreter JIT compiler runs JVM
Isnt reflection meant for this? class Class { Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException; } class Method { Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; } Reflection implies neither type-safety nor a notion of fail-fast. Note: there are no performance gains when using code generation over reflection! Thus, runtime code generation only makes sense for user type enhancement: While the framework code is less type safe, this type-unsafety does not spoil the user s code.
Do-it-yourself as an alternative? class Service { void deleteEverything(){ if(!"ADMIN".equals(UserHolder.user)){ thrownew IllegalStateException("Wrong user"); } // delete everything... } } At best, this makes testing an issue. Maybe still the easiest approach for simple cross-cutting concerns. In general, declarative programming often results in readable and modular code.
The black magic prejudice. var service ={ /* @Secured(user = "ADMIN") */ deleteEverything:function (){ // delete everything ... } } No type, no problem. ( duck typing ) function run(service){ service.deleteEverything(); } In dynamic languages (also those running on the JVM) this concept is applied a lot! For framework implementors, type-safety is conceptually impossible. But with type information available, we are at least able to fail fast when generating code at runtime in case that types do not match.
The performance myth. There is no point in byte code optimization . int compute() { return i * ConstantHolder.value; } It s not true that reflection is slower than generated code . NativeMethodAccessor Method::invoke GeneratedMethodAccessor### -Dsun.reflect.inflationThreshold=# The JIT compiler knows its job pretty well. NEVER optimize byte code. Never use JNI for something you could also express as byte code. However, avoid reflective member lookup.
Java source code Java byte code int foo() { return 1 + 2; } ICONST_1 ICONST_2 IADD IRETURN 0x04 0x05 0x60 0xAC 2 1 1 3 operand stack
ASM / BCEL Javassist cglib Byte Buddy MethodVisitor methodVisitor = ... methodVisitor.visitInsn(Opcodes.ICONST_1); methodVisitor.visitInsn(Opcodes.ICONST_2); methodVisitor.visitInsn(Opcodes.IADD); methodVisitor.visitInsn(Opcodes.IRETURN); visitor API MethodNode methodNode = ... InsnList insnList = methodNode.instructions; insnList.add(new InsnNode(Opcodes.ICONST_1)); insnList.add(new InsnNode(Opcodes.ICONST_2)); insnList.add(new InsnNode(Opcodes.IADD)); insnList.add(new InsnNode(Opcodes.IRETURN)); tree API
ASM / BCEL Javassist cglib Byte Buddy Byte code-level API gives full freedom Requires knowledge of byte code (stack metaphor, JVM type system) Requires a lot of manual work (stack sizes / stack map frames) Byte code-level APIs are not type safe (jeopardy of verifier errors, visitor call order) Byte code itself is little expressive Low overhead (visitor APIs) ASM is currently more popular than BCEL (used by the OpenJDK, considered as public API) Versioning issues for ASM (especially v3 to v4)
ASM / BCEL Javassist cglib Byte Buddy int foo() { return 1 + 2; } "}" "int foo() {" + " return 1 + 2;" + Strings are not typed ( SQL quandary ) Specifically: Security problems! Makes debugging difficult (unlinked source code, exception stack traces) Bound to Java as a language The Javassist compiler lags behind javac Requires special Java source code instructions for realizing cross-cutting concerns
ASM / BCEL Javassist cglib Byte Buddy class SecuredService extends Service { @Override void deleteEverything(){ methodInterceptor.intercept(this, Service.class.getDeclaredMethod("deleteEverything"), new Object[0], new $MethodProxy()); } class $MethodProxy implements MethodProxy { // inner class semantics, can call super } } class SecuredService extends Service { @Override void deleteEverything(){ if(!"ADMIN".equals(UserHolder.user)){ thrownew IllegalStateException("Wrong user"); } super.deleteEverything(); } } generic delegation interface MethodInterceptor { Object intercept(Object object, Method method, Object[] arguments, MethodProxy proxy) throws Throwable }
ASM / BCEL Javassist cglib Byte Buddy Discards all available type information JIT compiler struggles with two-way-boxing (check out JIT-watch for evidence) Interface dependency of intercepted classes Delegation requires explicit class initialization (breaks build-time usage / class serialization) Subclass instrumentation only (breaks annotation APIs / class identity) Feature complete / little development Little intuitive user-API
ASM / BCEL Javassist cglib Byte Buddy Class<?> dynamicType =new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(value("Hello World!")) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
ASM / BCEL Javassist cglib Byte Buddy Class<?> dynamicType =new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(to(MyInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); identifies best match class MyInterceptor { static String intercept() { return "Hello World"; } }
ASM / BCEL Javassist cglib Byte Buddy Class<?> dynamicType =new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(to(MyInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); provides arguments class MyInterceptor { static String intercept(@Origin Method m) { return "Hello World from " + m.getName(); } } Annotations that are not visible to a class loader are ignored at runtime. Thus, Byte Buddy s classes can be used without Byte Buddy on the class path.
ASM / BCEL Javassist cglib Byte Buddy @Origin Method|Class<?>|String Provides caller information @SuperCall Runnable|Callable<?> Allows super method call @DefaultCall Runnable|Callable<?> Allows default method call @AllArguments T[] Provides boxed method arguments @Argument(index) T Provides argument at the given index @This T Provides caller instance @Super T Provides super method proxy
ASM / BCEL Javassist cglib Byte Buddy class Foo { String bar(){ return "bar"; } } Foo foo = new Foo(); new ByteBuddy() .redefine(Foo.class) .method(named("bar")) .intercept(value("Hello World!")) .make() .load(Foo.class.getClassLoader(), ClassReloadingStrategy.installedAgent()); assertThat(foo.bar(),is("Hello World!")); The instrumentation API does not allow introduction of new methods. This might change with JEP-159: Enhanced Class Redefiniton.
ASM / BCEL Javassist cglib Byte Buddy class Foo { String bar(){ return "bar"; } } assertThat(new Foo().bar(),is("Hello World!")); public static void premain(String arguments, Instrumentation instrumentation){ new AgentBuilder.Default() .rebase(named("Foo")) .transform( (builder, type) -> builder .method(named("bar")) .intercept(value("Hello World!")); ) .installOn(instrumentation); }
ASM / BCEL Javassist cglib Byte Buddy class Foo { @Qux void baz(List<Bar> list){ } } Method dynamicMethod =new ByteBuddy() .subclass(Foo.class) .method(named("baz")) .intercept(StubMethod.INSTANCE) .attribute(new MethodAttributeAppender .ForInstrumentedMethod()) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredMethod("baz", List.class); assertThat(dynamicMethod.isAnnotatedWith(Qux.class), is(true)); assertThat(dynamicMethod.getGenericParameterTypes()[0], instanceOf(ParameterizedType.class));
Reality check: Reinvent Java? Many applications are built around a central infrastructure. A lot of code does not solve domain problems but bridges between domain and infrastructure. Java agents allow to add a decentralized infrastructure at runtime. In the source code, the infrastructure is only declared. Plain old Java applications (POJAs) Working with POJOs reduces complexity. Reducing infrastructure code as a goal
Android makes things more complicated. Java virtual machine [stack, JIT] Dalvik virtual machine [register, JIT] Android runtime [register, AOT] Solution: Embed the Android SDK s dex compiler (Apache 2.0 license). Unfortunately, only subclass instrumentation possible.
Byte Buddy cglib Javassist Java proxy (1) 60.995 234.488 145.412 68.706 (2a) 153.800 804.000 706.878 973.650 (2b) 0.001 0.002 0.009 0.005 (3a) 172.126 1 850.567 1 480.525 625.778 n/a (3b) 0.002 0.003 0.019 0.027 n/a All benchmarks run with JMH, source code: https://github.com/raphw/byte-buddy (1) Extending the Object class without any methods but with a default constructor (2a) Implementing an interface with 18 methods, method stubs (2b) Executing a method of this interface (3a) Extending a class with 18 methods, super method invocation (3b) Executing a method of this class
http://rafael.codes @rafaelcodes http://documents4j.com https://github.com/documents4j/documents4j http://bytebuddy.net https://github.com/raphw/byte-buddy