Skip to content

Commit 412aec9

Browse files
New Reflection API (#571)
* A solution that should be all wired up, not tested yet * Working solution + testing this in the simple exmamples here * Expose the other context's here * Add option to disable annotation processing for specific classes * Improve error message for bad handler signature * Javadocs * Lil refactor of ProxySupport, added test with spring
1 parent 45c9421 commit 412aec9

60 files changed

Lines changed: 5967 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
`java-conventions`
3+
`kotlin-conventions`
4+
`java-library`
5+
`library-publishing-conventions`
6+
}
7+
8+
description = "ByteBuddy proxy support"
9+
10+
dependencies {
11+
compileOnly(libs.jspecify)
12+
13+
implementation(project(":common"))
14+
implementation(libs.bytebuddy)
15+
implementation(libs.objenesis)
16+
}
17+
18+
tasks.withType<Javadoc> { isFailOnError = false }
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.bytebuddy.proxysupport;
10+
11+
import dev.restate.common.reflections.ProxyFactory;
12+
import java.lang.reflect.Field;
13+
import java.lang.reflect.Method;
14+
import java.lang.reflect.Modifier;
15+
import net.bytebuddy.ByteBuddy;
16+
import net.bytebuddy.TypeCache;
17+
import net.bytebuddy.description.modifier.Visibility;
18+
import net.bytebuddy.dynamic.scaffold.TypeValidation;
19+
import net.bytebuddy.implementation.InvocationHandlerAdapter;
20+
import net.bytebuddy.matcher.ElementMatchers;
21+
import org.jspecify.annotations.Nullable;
22+
import org.objenesis.Objenesis;
23+
import org.objenesis.ObjenesisStd;
24+
25+
/**
26+
* ByteBuddy-based proxy factory that supports both interfaces and concrete classes. This
27+
* implementation can create proxies for any class that is not final. Uses Objenesis to instantiate
28+
* objects without calling constructors, which allows proxying classes that don't have a no-arg
29+
* constructor. Uses TypeCache to cache generated proxy classes for better performance
30+
* (thread-safe).
31+
*/
32+
public final class ByteBuddyProxyFactory implements ProxyFactory {
33+
34+
private static final String INTERCEPTOR_FIELD_NAME = "$$interceptor$$";
35+
36+
private final Objenesis objenesis = new ObjenesisStd();
37+
private final TypeCache<Class<?>> proxyClassCache =
38+
new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
39+
40+
@Override
41+
@SuppressWarnings("unchecked")
42+
public <T> @Nullable T createProxy(Class<T> clazz, MethodInterceptor interceptor) {
43+
// Cannot proxy final classes
44+
if (Modifier.isFinal(clazz.getModifiers())) {
45+
return null;
46+
}
47+
48+
try {
49+
// Find or create the proxy class (cached)
50+
Class<? extends T> proxyClass =
51+
(Class<? extends T>)
52+
proxyClassCache.findOrInsert(
53+
clazz.getClassLoader(), clazz, () -> generateProxyClass(clazz), proxyClassCache);
54+
55+
// Instantiate the proxy class using Objenesis (no constructor call)
56+
T proxyInstance = objenesis.newInstance(proxyClass);
57+
58+
// Set the interceptor field
59+
Field interceptorField = proxyClass.getDeclaredField(INTERCEPTOR_FIELD_NAME);
60+
interceptorField.setAccessible(true);
61+
interceptorField.set(proxyInstance, interceptor);
62+
63+
return proxyInstance;
64+
65+
} catch (Exception e) {
66+
// Could not create or instantiate the proxy
67+
return null;
68+
}
69+
}
70+
71+
private <T> Class<?> generateProxyClass(Class<T> clazz) {
72+
ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.DISABLED);
73+
74+
var builder =
75+
clazz.isInterface()
76+
? byteBuddy.subclass(Object.class).implement(clazz)
77+
: byteBuddy.subclass(clazz);
78+
79+
try (var unloaded =
80+
builder
81+
// Add a field to store the interceptor
82+
.defineField(INTERCEPTOR_FIELD_NAME, MethodInterceptor.class, Visibility.PUBLIC)
83+
// Intercept all methods
84+
.method(ElementMatchers.any())
85+
.intercept(
86+
InvocationHandlerAdapter.of(
87+
(proxy, method, args) -> {
88+
// Get the interceptor from the field
89+
Field field = proxy.getClass().getDeclaredField(INTERCEPTOR_FIELD_NAME);
90+
field.setAccessible(true);
91+
MethodInterceptor interceptor = (MethodInterceptor) field.get(proxy);
92+
93+
if (interceptor == null) {
94+
throw new IllegalStateException("Interceptor not set on proxy instance");
95+
}
96+
97+
MethodInvocation invocation =
98+
new MethodInvocation() {
99+
@Override
100+
public Object[] getArguments() {
101+
return args != null ? args : new Object[0];
102+
}
103+
104+
@Override
105+
public Method getMethod() {
106+
return method;
107+
}
108+
};
109+
return interceptor.invoke(invocation);
110+
}))
111+
.make()) {
112+
return unloaded.load(clazz.getClassLoader()).getLoaded();
113+
}
114+
}
115+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dev.restate.bytebuddy.proxysupport.ByteBuddyProxyFactory

client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ description = "Restate Client to interact with services from within other Java a
99

1010
dependencies {
1111
compileOnly(libs.jspecify)
12+
compileOnly(libs.jetbrains.annotations)
1213

1314
api(project(":common"))
1415
api(project(":sdk-serde-jackson"))

client/src/main/java/dev/restate/client/Client.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.client;
1010

11+
import static dev.restate.common.reflections.ReflectionUtils.mustHaveAnnotation;
12+
1113
import dev.restate.common.Output;
1214
import dev.restate.common.Request;
1315
import dev.restate.common.Target;
1416
import dev.restate.common.WorkflowRequest;
17+
import dev.restate.sdk.annotation.Service;
18+
import dev.restate.sdk.annotation.VirtualObject;
19+
import dev.restate.sdk.annotation.Workflow;
1520
import dev.restate.serde.SerdeFactory;
1621
import dev.restate.serde.TypeTag;
1722
import java.time.Duration;
@@ -525,6 +530,101 @@ default Response<Output<Res>> getOutput() throws IngressException {
525530
}
526531
}
527532

533+
/**
534+
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate service from the ingress. This
535+
* API may change in future releases.
536+
*
537+
* <p>You can invoke the service in three ways:
538+
*
539+
* <pre>{@code
540+
* Client client = Client.connect("http://localhost:8080");
541+
*
542+
* // 1. Create a client proxy and call it directly (returns output directly)
543+
* var greeterProxy = client.service(Greeter.class).client();
544+
* GreetingResponse output = greeterProxy.greet(new Greeting("Alice"));
545+
*
546+
* // 2. Use call() with method reference and wait for the result
547+
* Response<GreetingResponse> response = client.service(Greeter.class)
548+
* .call(Greeter::greet, new Greeting("Alice"));
549+
*
550+
* // 3. Use send() for one-way invocation without waiting
551+
* SendResponse<GreetingResponse> sendResponse = client.service(Greeter.class)
552+
* .send(Greeter::greet, new Greeting("Alice"));
553+
* }</pre>
554+
*
555+
* @param clazz the service class annotated with {@link Service}
556+
* @return a reference to invoke the service
557+
*/
558+
@org.jetbrains.annotations.ApiStatus.Experimental
559+
default <SVC> ClientServiceReference<SVC> service(Class<SVC> clazz) {
560+
mustHaveAnnotation(clazz, Service.class);
561+
return new ClientServiceReferenceImpl<>(this, clazz, null);
562+
}
563+
564+
/**
565+
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate Virtual Object from the
566+
* ingress. This API may change in future releases.
567+
*
568+
* <p>You can invoke the virtual object in three ways:
569+
*
570+
* <pre>{@code
571+
* Client client = Client.connect("http://localhost:8080");
572+
*
573+
* // 1. Create a client proxy and call it directly (returns output directly)
574+
* var counterProxy = client.virtualObject(Counter.class, "my-counter").client();
575+
* int count = counterProxy.increment();
576+
*
577+
* // 2. Use call() with method reference and wait for the result
578+
* Response<Integer> response = client.virtualObject(Counter.class, "my-counter")
579+
* .call(Counter::increment);
580+
*
581+
* // 3. Use send() for one-way invocation without waiting
582+
* SendResponse<Integer> sendResponse = client.virtualObject(Counter.class, "my-counter")
583+
* .send(Counter::increment);
584+
* }</pre>
585+
*
586+
* @param clazz the virtual object class annotated with {@link VirtualObject}
587+
* @param key the key identifying the specific virtual object instance
588+
* @return a reference to invoke the virtual object
589+
*/
590+
@org.jetbrains.annotations.ApiStatus.Experimental
591+
default <SVC> ClientServiceReference<SVC> virtualObject(Class<SVC> clazz, String key) {
592+
mustHaveAnnotation(clazz, VirtualObject.class);
593+
return new ClientServiceReferenceImpl<>(this, clazz, key);
594+
}
595+
596+
/**
597+
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate Workflow from the ingress. This
598+
* API may change in future releases.
599+
*
600+
* <p>You can invoke the workflow in three ways:
601+
*
602+
* <pre>{@code
603+
* Client client = Client.connect("http://localhost:8080");
604+
*
605+
* // 1. Create a client proxy and call it directly (returns output directly)
606+
* var workflowProxy = client.workflow(OrderWorkflow.class, "order-123").client();
607+
* OrderResult result = workflowProxy.start(new OrderRequest(...));
608+
*
609+
* // 2. Use call() with method reference and wait for the result
610+
* Response<OrderResult> response = client.workflow(OrderWorkflow.class, "order-123")
611+
* .call(OrderWorkflow::start, new OrderRequest(...));
612+
*
613+
* // 3. Use send() for one-way invocation without waiting
614+
* SendResponse<OrderResult> sendResponse = client.workflow(OrderWorkflow.class, "order-123")
615+
* .send(OrderWorkflow::start, new OrderRequest(...));
616+
* }</pre>
617+
*
618+
* @param clazz the workflow class annotated with {@link Workflow}
619+
* @param key the key identifying the specific workflow instance
620+
* @return a reference to invoke the workflow
621+
*/
622+
@org.jetbrains.annotations.ApiStatus.Experimental
623+
default <SVC> ClientServiceReference<SVC> workflow(Class<SVC> clazz, String key) {
624+
mustHaveAnnotation(clazz, Workflow.class);
625+
return new ClientServiceReferenceImpl<>(this, clazz, key);
626+
}
627+
528628
/**
529629
* Create a default JDK client.
530630
*

0 commit comments

Comments
 (0)