diff --git a/examples/src/main/java/io/dapr/examples/actors/DemoActor.java b/examples/src/main/java/io/dapr/examples/actors/DemoActor.java index 4396598b4e..5eee1ed5a6 100644 --- a/examples/src/main/java/io/dapr/examples/actors/DemoActor.java +++ b/examples/src/main/java/io/dapr/examples/actors/DemoActor.java @@ -17,6 +17,7 @@ public interface DemoActor { void registerReminder(); + @ActorMethod(name = "echo_message") String say(String something); void clock(String message); diff --git a/examples/src/main/java/io/dapr/examples/actors/README.md b/examples/src/main/java/io/dapr/examples/actors/README.md index 3fd65bbc08..4f6855dea5 100644 --- a/examples/src/main/java/io/dapr/examples/actors/README.md +++ b/examples/src/main/java/io/dapr/examples/actors/README.md @@ -105,6 +105,8 @@ public class DemoActorImpl extends AbstractActor implements DemoActor, Remindabl An actor inherits from `AbstractActor` and implements the constructor to pass through `ActorRuntimeContext` and `ActorId`. By default, the actor's name will be the same as the class' name. Optionally, it can be annotated with `ActorType` and override the actor's name. The actor's methods can be synchronously or use [Project Reactor's Mono](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html) return type. Finally, state management is done via methods in `super.getActorStateManager()`. The `DemoActor` interface is used by the Actor runtime and also client. See how `DemoActor` interface can be annotated as Dapr Actor. ```java +import io.dapr.actors.ActorMethod; + /** * Example of implementation of an Actor. */ @@ -113,6 +115,7 @@ public interface DemoActor { void registerReminder(); + @ActorMethod(name = "echo_message") String say(String something); void clock(String message); @@ -123,7 +126,10 @@ public interface DemoActor { ``` -The `@ActorType` annotation indicates the Dapr Java SDK that this interface is an Actor Type, allowing a name for the type to be defined. Some methods can return a `Mono` object. In these cases, the `@ActorMethod` annotation is used to hint the Dapr Java SDK of the type encapsulated in the `Mono` object. You can read more about Java generic type erasure [here](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html). +The `@ActorType` annotation indicates the Dapr Java SDK that this interface is an Actor Type, allowing a name for the type to be defined. + +The `@ActorMethod` annotation can be applied to an interface method to specify configuration for that method. In this example, the `say` method, is renamed to `echo_message` - this can be used when invoking an actor method implemented in a different programming language (like C# or Python) and the method name does not match Java's naming conventions. +Some methods can return a `Mono` object. In these cases, the `@ActorMethod` annotation is used to hint the Dapr Java SDK of the type encapsulated in the `Mono` object. You can read more about Java generic type erasure [here](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html). Now, execute the following script in order to run DemoActorService: diff --git a/sdk-actors/src/main/java/io/dapr/actors/ActorMethod.java b/sdk-actors/src/main/java/io/dapr/actors/ActorMethod.java index 424a41ccce..74f380ca17 100644 --- a/sdk-actors/src/main/java/io/dapr/actors/ActorMethod.java +++ b/sdk-actors/src/main/java/io/dapr/actors/ActorMethod.java @@ -21,5 +21,13 @@ * * @return Actor's method return type. */ - Class returns(); + Class returns() default Undefined.class; + + /** + * Actor's method name. This is optional and will override the method's default name for actor invocation. + * + * @return Actor's method name. + */ + String name() default ""; + } diff --git a/sdk-actors/src/main/java/io/dapr/actors/Undefined.java b/sdk-actors/src/main/java/io/dapr/actors/Undefined.java new file mode 100644 index 0000000000..7375493bba --- /dev/null +++ b/sdk-actors/src/main/java/io/dapr/actors/Undefined.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +package io.dapr.actors; + +/** + * Internal class to represent the undefined value for an optional Class attribute. + */ +final class Undefined { + + private Undefined() { + } + +} diff --git a/sdk-actors/src/main/java/io/dapr/actors/client/ActorProxyImpl.java b/sdk-actors/src/main/java/io/dapr/actors/client/ActorProxyImpl.java index c8724412ee..8b97fd7cb5 100644 --- a/sdk-actors/src/main/java/io/dapr/actors/client/ActorProxyImpl.java +++ b/sdk-actors/src/main/java/io/dapr/actors/client/ActorProxyImpl.java @@ -21,6 +21,8 @@ */ class ActorProxyImpl implements ActorProxy, InvocationHandler { + private static final String UNDEFINED_CLASS_NAME = "io.dapr.actors.Undefined"; + /** * Actor's identifier for this Actor instance. */ @@ -136,29 +138,33 @@ public Object invoke(Object proxy, Method method, Object[] args) { throw new UnsupportedOperationException("Actor methods can only have zero or one arguments."); } + ActorMethod actorMethodAnnotation = method.getDeclaredAnnotation(ActorMethod.class); + String methodName = method.getName(); + if ((actorMethodAnnotation != null) && !actorMethodAnnotation.name().isEmpty()) { + methodName = actorMethodAnnotation.name(); + } + if (method.getParameterCount() == 0) { if (method.getReturnType().equals(Mono.class)) { - ActorMethod actorMethodAnnotation = method.getDeclaredAnnotation(ActorMethod.class); - if (actorMethodAnnotation == null) { - return invokeMethod(method.getName()); + if ((actorMethodAnnotation == null) || UNDEFINED_CLASS_NAME.equals(actorMethodAnnotation.returns().getName())) { + return invokeMethod(methodName); } - return invokeMethod(method.getName(), actorMethodAnnotation.returns()); + return invokeMethod(methodName, actorMethodAnnotation.returns()); } - return invokeMethod(method.getName(), method.getReturnType()).block(); + return invokeMethod(methodName, method.getReturnType()).block(); } if (method.getReturnType().equals(Mono.class)) { - ActorMethod actorMethodAnnotation = method.getDeclaredAnnotation(ActorMethod.class); - if (actorMethodAnnotation == null) { - return invokeMethod(method.getName(), args[0]); + if ((actorMethodAnnotation == null) || UNDEFINED_CLASS_NAME.equals(actorMethodAnnotation.returns().getName())) { + return invokeMethod(methodName, args[0]); } - return invokeMethod(method.getName(), args[0], actorMethodAnnotation.returns()); + return invokeMethod(methodName, args[0], actorMethodAnnotation.returns()); } - return invokeMethod(method.getName(), args[0], method.getReturnType()).block(); + return invokeMethod(methodName, args[0], method.getReturnType()).block(); } /** diff --git a/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorMethodInfoMap.java b/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorMethodInfoMap.java index dc41fd627a..f3f43b53bd 100644 --- a/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorMethodInfoMap.java +++ b/sdk-actors/src/main/java/io/dapr/actors/runtime/ActorMethodInfoMap.java @@ -5,6 +5,8 @@ package io.dapr.actors.runtime; +import io.dapr.actors.ActorMethod; + import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; @@ -35,7 +37,12 @@ class ActorMethodInfoMap { if (methodInfo.getParameterCount() <= 1) { // If Actor class uses overloading, then one will win. // Document this behavior, so users know how to write their code. - methods.put(methodInfo.getName(), methodInfo); + String methodName = methodInfo.getName(); + ActorMethod actorMethodAnnotation = methodInfo.getAnnotation(ActorMethod.class); + if ((actorMethodAnnotation != null) && !actorMethodAnnotation.name().isEmpty()) { + methodName = actorMethodAnnotation.name(); + } + methods.put(methodName, methodInfo); } } } diff --git a/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorNoStateTest.java b/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorNoStateTest.java index 1ed26a8693..2b01ea93bb 100644 --- a/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorNoStateTest.java +++ b/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorNoStateTest.java @@ -6,6 +6,7 @@ package io.dapr.actors.runtime; import io.dapr.actors.ActorId; +import io.dapr.actors.ActorMethod; import io.dapr.actors.ActorType; import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyForTestsImpl; @@ -43,6 +44,8 @@ public interface MyActor { Mono classInClassOut(MyData input); Mono registerBadCallbackName(); String registerTimerAutoName(); + @ActorMethod(name = "DotNetMethodASync") + Mono dotNetMethod(); } @ActorType(name = "MyActor") @@ -108,6 +111,11 @@ public Mono registerBadCallbackName() { public String registerTimerAutoName() { return super.registerActorTimer("", "anything", "state", Duration.ofSeconds(1), Duration.ofSeconds(1)).block(); } + + @Override + public Mono dotNetMethod() { + return Mono.empty(); + } } static class MyData { @@ -174,6 +182,12 @@ public void stringInVoidOutIntentionallyThrows() { actorProxy.invokeMethod("stringInVoidOutIntentionallyThrows", "hello world").block(); } + @Test + public void testMethodNameChange() { + MyActor actor = createActorProxy(MyActor.class); + actor.dotNetMethod(); + } + @Test public void classInClassOut() { ActorProxy actorProxy = createActorProxy(); diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java new file mode 100644 index 0000000000..70071b5724 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +package io.dapr.it.actors; + +import io.dapr.actors.ActorId; +import io.dapr.actors.client.ActorProxy; +import io.dapr.actors.client.ActorProxyBuilder; +import io.dapr.it.BaseIT; +import io.dapr.it.actors.app.MyActor; +import io.dapr.it.actors.app.MyActorService; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.dapr.it.Retry.callWithRetry; +import static org.junit.Assert.assertTrue; + +public class ActorMethodNameIT extends BaseIT { + + private static Logger logger = LoggerFactory.getLogger(ActorMethodNameIT.class); + + @Test + public void actorMethodNameChange() throws Exception { + // The call below will fail if service cannot start successfully. + startDaprApp( + ActorMethodNameIT.class.getSimpleName(), + MyActorService.SUCCESS_MESSAGE, + MyActorService.class, + true, + 60000); + + logger.debug("Creating proxy builder"); + ActorProxyBuilder proxyBuilder = deferClose(new ActorProxyBuilder("MyActorTest", MyActor.class)); + logger.debug("Creating actorId"); + ActorId actorId1 = new ActorId("1"); + logger.debug("Building proxy"); + MyActor proxy = proxyBuilder.build(actorId1); + + callWithRetry(() -> { + logger.debug("Invoking dotNetMethod from Proxy"); + boolean response = proxy.dotNetMethod(); + logger.debug("asserting true response: [" + response + "]"); + assertTrue(response); + }, 60000); + + logger.debug("Creating proxy builder 2"); + ActorProxyBuilder proxyBuilder2 = deferClose(new ActorProxyBuilder("MyActorTest", ActorProxy.class)); + logger.debug("Building proxy 2"); + ActorProxy proxy2 = proxyBuilder2.build(actorId1); + + callWithRetry(() -> { + logger.debug("Invoking DotNetMethodAsync from Proxy 2"); + boolean response = proxy2.invokeMethod("DotNetMethodAsync", boolean.class).block(); + logger.debug("asserting true response 2: [" + response + "]"); + assertTrue(response); + }, 60000); + + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActor.java b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActor.java index 00084b5dc0..4d082dd344 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActor.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActor.java @@ -5,6 +5,8 @@ package io.dapr.it.actors.app; +import io.dapr.actors.ActorMethod; + import java.util.ArrayList; import java.util.List; @@ -26,4 +28,7 @@ public interface MyActor { ArrayList getCallLog(); String getIdentifier(); + + @ActorMethod(name = "DotNetMethodAsync") + boolean dotNetMethod(); } \ No newline at end of file diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorImpl.java b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorImpl.java index 89679711ae..35b731a85a 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorImpl.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/app/MyActorImpl.java @@ -199,6 +199,11 @@ public String getIdentifier() { return System.getenv("DAPR_HTTP_PORT"); } + @Override + public boolean dotNetMethod() { + return true; + } + private void formatAndLog(boolean isEnter, String methodName) { Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));