Making Java More Dynamic: Runtime Code Generation for JVM

 
Making Java more dynamic:
runtime code generation
for the JVM
 
interface
 Framework 
{
  
<
T
>
 Class
<? extends 
T
>
 secure
(
Class
<
T
>
 type
);
}
 
class
 Service 
{
  
@Secured
(
user 
=
 
"ADMIN"
)
  void
 deleteEverything
()
 
{
    
// delete everything...
  
}
}
@interface
 Secured 
{
  String user
();
}
class
 UserHolder 
{
  
static
 String 
user
 
=
 
"ANONYMOUS"
;
}
 
does not know about
 
depends on
 
discovers at runtime
 
class
 Service 
{
  @Secured
(
user 
=
 
"ADMIN"
)
  void
 deleteEverything
()
 
{
    if
(!
"ADMIN"
.
equals
(
UserHolder
.
user
))
 
{
      
throw
 
new
 IllegalStateException
(
"Wrong user"
);
    
}
    
// delete everything...
  
}
}
 
redefine class
(build time, agent)
 
create subclass
(Liskov substitution)
 
class
 SecuredService 
extends
 Service 
{
 
 @Override
  
void
 deleteEverything
()
 
{
   
 
if
(!
"ADMIN"
.
equals
(
UserHolder
.
user
))
 
{
      
throw
 
new
 IllegalStateException
(
"Wrong user"
);
    
}
    
super
.
deleteEverything
();
  
}
}
class
 Service 
{
  
@Secured
(
user 
=
 
"ADMIN"
)
  void
 deleteEverything
()
 
{
    
// delete everything...
  
}
}
 
0xCAFEBABE
source code
 
byte code
 
JVM
 
javac
 
scalac
 
groovyc
 
jrubyc
 
JIT compiler
 
interpreter
 
class loader
 
creates
 
reads
 
runs
class
 Method 
{
  Object invoke
(
Object obj
,
                Object
...
 args
)
    
 
throws
 IllegalAccessException
,
            IllegalArgumentException
,
            InvocationTargetException
;
}
class
 Class 
{
  Method getDeclaredMethod
(
String name
,
                           Class
<
?
>...
 parameterTypes
)
    
 
throws
 NoSuchMethodException
,
            SecurityException
;
}
Isn’t reflection meant for this?
 
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
))
 
{
      
throw
 
new
 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 ...
  
}
}
 
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.
No type, no problem.
(“duck typing”)
 
The performance myth.
int
 compute
()
 
{
  
return
 i 
*
 ConstantHolder
.
value
;
}
There is no point in “byte code optimization”.
 
It’s not true that “reflection is slower than generated code”.
Method::invoke
NativeMethodAccessor
GeneratedMethodAccessor###
 
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.
 
-
Dsun
.
reflect
.
inflationThreshold
=
#
int
 foo
()
 
{
 
  
return
 
1
 
+
 
2
;
 
}
 
ICONST_1
ICONST_2
IADD
 
operand stack
 
1
 
2
 
1
 
3
 
IRETURN
 
0x04
0x05
0x60
0xAC
Java source code
Java byte code
MethodVisitor methodVisitor 
= ...
methodVisitor
.
visitInsn
(
Opcodes
.
ICONST_1
);
methodVisitor
.
visitInsn
(
Opcodes
.
ICONST_2
);
methodVisitor
.
visitInsn
(
Opcodes
.
IADD
);
methodVisitor
.
visitInsn
(
Opcodes
.
IRETURN
);
 
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
));
ASM / BCEL
Javassist
cglib
Byte Buddy
visitor API
tree API
 
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
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
 
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
  
}
}
ASM / BCEL
Javassist
cglib
Byte Buddy
 
class
 SecuredService 
extends
 Service 
{
 
 @Override
  
void
 deleteEverything
()
 
{
   
 
if
(!
"ADMIN"
.
equals
(
UserHolder
.
user
))
 
{
      
throw
 
new
 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
 MyInterceptor 
{
  
static
 String intercept
()
 
{
    
return
 
"Hello World"
;
  
}
}
 
identifies best match
Class
<?>
 dynamicType 
=
 
new
 ByteBuddy
()
  
.
subclass
(
Object
.
class
)
  .
method
(
named
(
"toString"
))
  .
intercept
(
to
(
MyInterceptor
.
class
))
  
.
make
()
  
.
load
(
getClass
().
getClassLoader
(),
        ClassLoadingStrategy
.
Default
.
WRAPPER
)
  
.
getLoaded
();
ASM / BCEL
Javassist
cglib
Byte Buddy
 
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.
class
 MyInterceptor 
{
  
static
 String intercept
(
@Origin 
Method m
)
 
{
    
return
 
"Hello World from "
 
+
 m
.
getName
();
  
}
}
Class
<?>
 dynamicType 
=
 
new
 ByteBuddy
()
  
.
subclass
(
Object
.
class
)
  .
method
(
named
(
"toString"
))
  .
intercept
(
to
(
MyInterceptor
.
class
))
  
.
make
()
  
.
load
(
getClass
().
getClassLoader
(),
        ClassLoadingStrategy
.
Default
.
WRAPPER
)
  
.
getLoaded
();
provides arguments
 
ASM / BCEL
 
Javassist
 
cglib
 
Byte Buddy
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?
 
“Plain old Java applications” (POJAs)
Working with POJOs reduces complexity. Reducing infrastructure code as a goal
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.
Java virtual
machine
[stack, JIT]
Dalvik virtual
machine
[register, JIT]
Android
runtime
[register, AOT]
Android makes things more complicated.
 
Solution: Embed the Android SDK’s dex compiler (Apache 2.0 license).
Unfortunately, only subclass instrumentation possible.
 
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
Slide Note
Embed
Share

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.

  • Java
  • Runtime Code Generation
  • JVM
  • Dynamic Behavior
  • Reflection

Uploaded on Feb 15, 2025 | 0 Views


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


  1. Making Java more dynamic: runtime code generation for the JVM

  2. 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... } }

  3. 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... } }

  4. source code javac scalac groovyc jrubyc creates reads 0xCAFEBABE byte code class loader interpreter JIT compiler runs JVM

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

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

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

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

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

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

  11. 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)

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

  13. 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 }

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

  15. 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!"));

  16. 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"; } }

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

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

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

  20. 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); }

  21. 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));

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

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

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

  25. http://rafael.codes @rafaelcodes http://documents4j.com https://github.com/documents4j/documents4j http://bytebuddy.net https://github.com/raphw/byte-buddy

More Related Content

giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#