Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bytebuddy-proxy-support/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
`java-conventions`
`kotlin-conventions`
`java-library`
`library-publishing-conventions`
}

description = "ByteBuddy proxy support"

dependencies {
compileOnly(libs.jspecify)

implementation(project(":common"))
implementation(libs.bytebuddy)
implementation(libs.objenesis)
}

tasks.withType<Javadoc> { isFailOnError = false }
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.bytebuddy.proxysupport;

import dev.restate.common.reflections.ProxyFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.TypeCache;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import net.bytebuddy.matcher.ElementMatchers;
import org.jspecify.annotations.Nullable;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

/**
* ByteBuddy-based proxy factory that supports both interfaces and concrete classes. This
* implementation can create proxies for any class that is not final. Uses Objenesis to instantiate
* objects without calling constructors, which allows proxying classes that don't have a no-arg
* constructor. Uses TypeCache to cache generated proxy classes for better performance
* (thread-safe).
*/
public final class ByteBuddyProxyFactory implements ProxyFactory {

private static final String INTERCEPTOR_FIELD_NAME = "$$interceptor$$";

private final Objenesis objenesis = new ObjenesisStd();
private final TypeCache<Class<?>> proxyClassCache =
new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);

@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T createProxy(Class<T> clazz, MethodInterceptor interceptor) {
// Cannot proxy final classes
if (Modifier.isFinal(clazz.getModifiers())) {
return null;
}

try {
// Find or create the proxy class (cached)
Class<? extends T> proxyClass =
(Class<? extends T>)
proxyClassCache.findOrInsert(
clazz.getClassLoader(), clazz, () -> generateProxyClass(clazz), proxyClassCache);

// Instantiate the proxy class using Objenesis (no constructor call)
T proxyInstance = objenesis.newInstance(proxyClass);

// Set the interceptor field
Field interceptorField = proxyClass.getDeclaredField(INTERCEPTOR_FIELD_NAME);
interceptorField.setAccessible(true);
interceptorField.set(proxyInstance, interceptor);

return proxyInstance;

} catch (Exception e) {
// Could not create or instantiate the proxy
return null;
}
}

private <T> Class<?> generateProxyClass(Class<T> clazz) {
ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.DISABLED);

var builder =
clazz.isInterface()
? byteBuddy.subclass(Object.class).implement(clazz)
: byteBuddy.subclass(clazz);

try (var unloaded =
builder
// Add a field to store the interceptor
.defineField(INTERCEPTOR_FIELD_NAME, MethodInterceptor.class, Visibility.PUBLIC)
// Intercept all methods
.method(ElementMatchers.any())
.intercept(
InvocationHandlerAdapter.of(
(proxy, method, args) -> {
// Get the interceptor from the field
Field field = proxy.getClass().getDeclaredField(INTERCEPTOR_FIELD_NAME);
field.setAccessible(true);
MethodInterceptor interceptor = (MethodInterceptor) field.get(proxy);

if (interceptor == null) {
throw new IllegalStateException("Interceptor not set on proxy instance");
}

MethodInvocation invocation =
new MethodInvocation() {
@Override
public Object[] getArguments() {
return args != null ? args : new Object[0];
}

@Override
public Method getMethod() {
return method;
}
};
return interceptor.invoke(invocation);
}))
.make()) {
return unloaded.load(clazz.getClassLoader()).getLoaded();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev.restate.bytebuddy.proxysupport.ByteBuddyProxyFactory
1 change: 1 addition & 0 deletions client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description = "Restate Client to interact with services from within other Java a

dependencies {
compileOnly(libs.jspecify)
compileOnly(libs.jetbrains.annotations)

api(project(":common"))
api(project(":sdk-serde-jackson"))
Expand Down
100 changes: 100 additions & 0 deletions client/src/main/java/dev/restate/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.client;

import static dev.restate.common.reflections.ReflectionUtils.mustHaveAnnotation;

import dev.restate.common.Output;
import dev.restate.common.Request;
import dev.restate.common.Target;
import dev.restate.common.WorkflowRequest;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.annotation.VirtualObject;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.serde.SerdeFactory;
import dev.restate.serde.TypeTag;
import java.time.Duration;
Expand Down Expand Up @@ -525,6 +530,101 @@ default Response<Output<Res>> getOutput() throws IngressException {
}
}

/**
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate service from the ingress. This
* API may change in future releases.
*
* <p>You can invoke the service in three ways:
*
* <pre>{@code
* Client client = Client.connect("http://localhost:8080");
*
* // 1. Create a client proxy and call it directly (returns output directly)
* var greeterProxy = client.service(Greeter.class).client();
* GreetingResponse output = greeterProxy.greet(new Greeting("Alice"));
*
* // 2. Use call() with method reference and wait for the result
* Response<GreetingResponse> response = client.service(Greeter.class)
* .call(Greeter::greet, new Greeting("Alice"));
*
* // 3. Use send() for one-way invocation without waiting
* SendResponse<GreetingResponse> sendResponse = client.service(Greeter.class)
* .send(Greeter::greet, new Greeting("Alice"));
* }</pre>
*
* @param clazz the service class annotated with {@link Service}
* @return a reference to invoke the service
*/
@org.jetbrains.annotations.ApiStatus.Experimental
default <SVC> ClientServiceReference<SVC> service(Class<SVC> clazz) {
mustHaveAnnotation(clazz, Service.class);
return new ClientServiceReferenceImpl<>(this, clazz, null);
}

/**
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate Virtual Object from the
* ingress. This API may change in future releases.
*
* <p>You can invoke the virtual object in three ways:
*
* <pre>{@code
* Client client = Client.connect("http://localhost:8080");
*
* // 1. Create a client proxy and call it directly (returns output directly)
* var counterProxy = client.virtualObject(Counter.class, "my-counter").client();
* int count = counterProxy.increment();
*
* // 2. Use call() with method reference and wait for the result
* Response<Integer> response = client.virtualObject(Counter.class, "my-counter")
* .call(Counter::increment);
*
* // 3. Use send() for one-way invocation without waiting
* SendResponse<Integer> sendResponse = client.virtualObject(Counter.class, "my-counter")
* .send(Counter::increment);
* }</pre>
*
* @param clazz the virtual object class annotated with {@link VirtualObject}
* @param key the key identifying the specific virtual object instance
* @return a reference to invoke the virtual object
*/
@org.jetbrains.annotations.ApiStatus.Experimental
default <SVC> ClientServiceReference<SVC> virtualObject(Class<SVC> clazz, String key) {
mustHaveAnnotation(clazz, VirtualObject.class);
return new ClientServiceReferenceImpl<>(this, clazz, key);
}

/**
* <b>EXPERIMENTAL API:</b> Create a reference to invoke a Restate Workflow from the ingress. This
* API may change in future releases.
*
* <p>You can invoke the workflow in three ways:
*
* <pre>{@code
* Client client = Client.connect("http://localhost:8080");
*
* // 1. Create a client proxy and call it directly (returns output directly)
* var workflowProxy = client.workflow(OrderWorkflow.class, "order-123").client();
* OrderResult result = workflowProxy.start(new OrderRequest(...));
*
* // 2. Use call() with method reference and wait for the result
* Response<OrderResult> response = client.workflow(OrderWorkflow.class, "order-123")
* .call(OrderWorkflow::start, new OrderRequest(...));
*
* // 3. Use send() for one-way invocation without waiting
* SendResponse<OrderResult> sendResponse = client.workflow(OrderWorkflow.class, "order-123")
* .send(OrderWorkflow::start, new OrderRequest(...));
* }</pre>
*
* @param clazz the workflow class annotated with {@link Workflow}
* @param key the key identifying the specific workflow instance
* @return a reference to invoke the workflow
*/
@org.jetbrains.annotations.ApiStatus.Experimental
default <SVC> ClientServiceReference<SVC> workflow(Class<SVC> clazz, String key) {
mustHaveAnnotation(clazz, Workflow.class);
return new ClientServiceReferenceImpl<>(this, clazz, key);
}

/**
* Create a default JDK client.
*
Expand Down
Loading