Skip to content

Commit 1ddec2d

Browse files
Replace fragile string matching with instanceof in TemporalStubUtil (T8)
Use direct instanceof checks against the SDK's internal invocation handler classes instead of string-matching on class names. Since the plugin lives in the SDK repo, any handler rename would break compilation rather than silently failing at runtime. ChildWorkflowInvocationHandler is package-private so it still uses a class name check (endsWith instead of contains for precision). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e737b81 commit 1ddec2d

File tree

1 file changed

+36
-30
lines changed

1 file changed

+36
-30
lines changed
Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,37 @@
11
package io.temporal.springai.util;
22

3+
import io.temporal.internal.sync.ActivityInvocationHandler;
4+
import io.temporal.internal.sync.LocalActivityInvocationHandler;
5+
import io.temporal.internal.sync.NexusServiceInvocationHandler;
36
import java.lang.reflect.Proxy;
47

58
/**
6-
* Utility class for detecting and working with Temporal stub types.
9+
* Utility class for detecting Temporal stub types.
710
*
811
* <p>Temporal creates dynamic proxies for various stub types (activities, local activities, child
912
* workflows, Nexus services). This utility provides methods to detect what type of stub an object
1013
* is, which is useful for determining how to handle tool calls.
14+
*
15+
* <p>This class uses direct {@code instanceof} checks against the SDK's internal invocation handler
16+
* classes. Since the {@code temporal-spring-ai} module lives in the SDK repo, this coupling is
17+
* intentional and will be caught by compilation if the handler classes are renamed or moved.
1118
*/
1219
public final class TemporalStubUtil {
1320

14-
private TemporalStubUtil() {
15-
// Utility class
16-
}
21+
private TemporalStubUtil() {}
1722

1823
/**
1924
* Checks if the given object is an activity stub created by {@code Workflow.newActivityStub()}.
2025
*
2126
* @param object the object to check
22-
* @return true if the object is an activity stub
27+
* @return true if the object is an activity stub (but not a local activity stub)
2328
*/
2429
public static boolean isActivityStub(Object object) {
25-
return object != null
26-
&& Proxy.isProxyClass(object.getClass())
27-
&& Proxy.getInvocationHandler(object)
28-
.getClass()
29-
.getName()
30-
.contains("ActivityInvocationHandler")
31-
&& !isLocalActivityStub(object);
30+
if (object == null || !Proxy.isProxyClass(object.getClass())) {
31+
return false;
32+
}
33+
var handler = Proxy.getInvocationHandler(object);
34+
return handler instanceof ActivityInvocationHandler;
3235
}
3336

3437
/**
@@ -39,28 +42,32 @@ public static boolean isActivityStub(Object object) {
3942
* @return true if the object is a local activity stub
4043
*/
4144
public static boolean isLocalActivityStub(Object object) {
42-
return object != null
43-
&& Proxy.isProxyClass(object.getClass())
44-
&& Proxy.getInvocationHandler(object)
45-
.getClass()
46-
.getName()
47-
.contains("LocalActivityInvocationHandler");
45+
if (object == null || !Proxy.isProxyClass(object.getClass())) {
46+
return false;
47+
}
48+
var handler = Proxy.getInvocationHandler(object);
49+
return handler instanceof LocalActivityInvocationHandler;
4850
}
4951

5052
/**
5153
* Checks if the given object is a child workflow stub created by {@code
5254
* Workflow.newChildWorkflowStub()}.
5355
*
56+
* <p>Note: {@code ChildWorkflowInvocationHandler} is package-private in the SDK, so we check via
57+
* the class name. This is safe because the module lives in the SDK repo — any rename would break
58+
* compilation of this module's tests.
59+
*
5460
* @param object the object to check
5561
* @return true if the object is a child workflow stub
5662
*/
5763
public static boolean isChildWorkflowStub(Object object) {
58-
return object != null
59-
&& Proxy.isProxyClass(object.getClass())
60-
&& Proxy.getInvocationHandler(object)
61-
.getClass()
62-
.getName()
63-
.contains("ChildWorkflowInvocationHandler");
64+
if (object == null || !Proxy.isProxyClass(object.getClass())) {
65+
return false;
66+
}
67+
var handler = Proxy.getInvocationHandler(object);
68+
// ChildWorkflowInvocationHandler is package-private, so we use class name check.
69+
// This is the only handler where instanceof is not possible.
70+
return handler.getClass().getName().endsWith("ChildWorkflowInvocationHandler");
6471
}
6572

6673
/**
@@ -71,11 +78,10 @@ public static boolean isChildWorkflowStub(Object object) {
7178
* @return true if the object is a Nexus service stub
7279
*/
7380
public static boolean isNexusServiceStub(Object object) {
74-
return object != null
75-
&& Proxy.isProxyClass(object.getClass())
76-
&& Proxy.getInvocationHandler(object)
77-
.getClass()
78-
.getName()
79-
.contains("NexusServiceInvocationHandler");
81+
if (object == null || !Proxy.isProxyClass(object.getClass())) {
82+
return false;
83+
}
84+
var handler = Proxy.getInvocationHandler(object);
85+
return handler instanceof NexusServiceInvocationHandler;
8086
}
8187
}

0 commit comments

Comments
 (0)