中文版 | User Guide | Benchmark Results
A high-performance dynamic proxy library for Java. OpenProxy generates proxy classes at runtime with ASM and dispatches every intercepted call through a hashCode-driven switch with a direct INVOKESPECIAL super call — no reflection, no
MethodHandle, JIT-inlinable. Interface proxies run at java.lang.reflect.Proxy
parity; default methods are ~6.5× faster.
- Direct
superdispatch —invokeSupercompiles to a directsuper.method(args); no reflection, noMethodHandle, JIT-inlinable. - Beats CGLib by ~3–5× on class proxies; interface proxies at
java.lang.reflect.Proxyparity and ~6× faster on default methods. - One API for classes and interfaces —
OpenProxy.proxy(...)with generic type inference, no casts. - GC-safe — proxy classes use
Lookup.defineHiddenClass(), so there is noClassLoaderleak.
Core
- Unified
OpenProxy.proxy(target, interceptor)entry point for classes and interfaces - Functional
InterceptorAPI — a single-method interface, use a lambda invokeSuper(proxy, method, args)for zero-overhead super dispatchWeakCache-backed proxy-class caching keyed on the method-to-interceptor mapping
Selective interception
Group.of(predicate, interceptor)+Group.otherwise(...)— first-match-wins with zero hot-path overhead- Methods matching no group pass through with zero interception cost
Proxy capabilities
- Interface proxy — runtime interface implementations without reflection
- Multi-interface proxy — one object, several interfaces, with conflict detection
- Non-public interface proxy — package-private interfaces, defined in the interface's own package
- Constructor arguments — proxy classes without a no-arg constructor
- Constructor interception —
ConstructorInterceptorhooks before/after the superclass constructor, with argument rewriting and veto - Static method proxy —
proxyStaticreturns a class shadowingpublic staticmethods - Annotation-driven API —
@Intercept/@Arounddeclarative matching at lambda speed - Hot reload / hot swap —
evict/evictClassLoaderfor hot-deployed classes,rebindto swap interceptors on a live instance
Greeter proxy = OpenProxy.proxy(Greeter.class, (obj, method, args) -> {
System.out.println("before " + method.getName());
Object result = OpenProxy.invokeSuper(obj, method, args);
System.out.println("after " + method.getName());
return result;
});
String greeting = proxy.hello("World");
// before hello
// after hello
// greeting == "Hello, World"Calculator calc = OpenProxy.proxy(Calculator.class, (obj, method, args) -> {
System.out.println("calling " + method.getName());
return (int) args[0] + (int) args[1];
});
int result = calc.add(10, 20); // 30Greeter proxy = OpenProxy.proxy(Greeter.class,
Group.of(m -> m.getName().startsWith("get"), getterInterceptor),
Group.of(m -> m.getName().startsWith("set"), setterInterceptor),
Group.otherwise(fallbackInterceptor));@Intercept
class MetricsInterceptor {
@Around("get*")
Object measure(Object proxy, Method method, Object[] args) throws Throwable {
return OpenProxy.invokeSuper(proxy, method, args);
}
}
Greeter proxy = OpenProxy.intercept(Greeter.class, new MetricsInterceptor());JMH benchmarks on Java 25 (all scores in ns/op, lower is better). Full tables, methodology, and run instructions: docs/benchmark-results.md.
- Class proxies beat CGLib by ~3–5× on scenarios with actual work; unmatched methods run at direct-call speed.
- Interface proxies run at parity with
java.lang.reflect.Proxyand are ~6× faster on default methods. - Multi-interceptor (
Group) has byte-identical hot paths to the single-interceptor API — zero degradation. - Annotation-driven interception reaches hand-written-lambda parity.
OpenProxy.proxy(...)matches each proxyable method to an interceptor via aGroupchain.- A generator emits bytecode: one
_interceptor$Nfield per distinct interceptor, one override per method, and adispatch(Method, Object[])method. - On each call, the override boxes the arguments and calls
Interceptor.intercept(...). If the interceptor callsinvokeSuper,dispatch()branches onmethod.hashCode()and jumps straight toINVOKESPECIAL super.method(...).
The key insight: dispatch uses a deterministic Method.hashCode() to build an if-else chain whose branches are direct super calls — no reflection, no
MethodHandle, fully JIT-inlinable. See the user guide
for the full picture.
- Java 25+
- ASM 9.7.1 (compile dependency)
Class proxies are defined in the target's package via MethodHandles.privateLookupIn. If the target lives in a strongly encapsulated module (any non-open package, including java.base packages such as java.util), proxy() fails fast with an actionable --add-opens hint. Interface proxies use a public lookup and support
public interfaces only (same as java.lang.reflect.Proxy). See
JPMS.
git clone https://github.com/lamspace/openproxy.git
cd openproxy
mvn install -DskipTestsMaven Central publishing is in progress; until then, depend on the artifact from your local repository:
<dependency>
<groupId>io.github.lamspace</groupId>
<artifactId>openproxy</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>| Feature | OpenProxy | CGLib | java.lang.reflect.Proxy |
|---|---|---|---|
| Proxies concrete classes | ✅ | ✅ | ❌ |
| Super-call mechanism | direct INVOKESPECIAL |
MethodProxy + FastClass |
N/A |
| GC-safe (hidden class) | ✅ | ❌ | ✅ |
| Selective interception | ✅ Group.of |
✅ CallbackFilter |
❌ |
| Multi-interface proxy | ✅ | ❌ | ✅ |
| Constructor interception | ✅ | ✅ | ❌ |
| Static method proxy | ✅ | ❌ | ❌ |
| Hot reload / rebind | ✅ | ❌ | ❌ |
| Annotation-driven API | ✅ | ❌ | ❌ |
| Functional API | ✅ lambda | ✅ | ✅ |
| Maven Central | Coming soon | ✅ | Built-in |
- User Guide — 13 chapters with runnable examples
- Benchmark Results (EN) / 中文
- Migration Guide
Apache License 2.0