From 972ffdbdb2617d41558ce998ec5c00bef42fc403 Mon Sep 17 00:00:00 2001 From: Muhammad Rifqi Fatchurrahman Date: Fri, 21 Aug 2026 01:37:15 +0700 Subject: [PATCH 1/2] Add failing test for composed annotated workflow auto-discovery NPE --- .../AutoDiscoveryComposedAnnotationTest.java | 57 +++++++++++++++++++ .../ComposedActivityImpl.java | 12 ++++ .../ComposedAnnotatedActivity.java | 8 +++ .../ComposedAnnotatedActivityImpl.java | 10 ++++ .../ComposedAnnotatedWorkflow.java | 11 ++++ .../ComposedAnnotatedWorkflowImpl.java | 20 +++++++ .../ComposedWorkflowImpl.java | 12 ++++ .../src/test/resources/application.yml | 12 ++++ 8 files changed, 142 insertions(+) create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryComposedAnnotationTest.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedActivityImpl.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivity.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivityImpl.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflow.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflowImpl.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedWorkflowImpl.java diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryComposedAnnotationTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryComposedAnnotationTest.java new file mode 100644 index 0000000000..224ba77770 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryComposedAnnotationTest.java @@ -0,0 +1,57 @@ +package io.temporal.spring.boot.autoconfigure; + +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.spring.boot.autoconfigure.composedannotation.ComposedAnnotatedActivityImpl; +import io.temporal.spring.boot.autoconfigure.composedannotation.ComposedAnnotatedWorkflow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = AutoDiscoveryComposedAnnotationTest.Configuration.class) +@ActiveProfiles(profiles = "auto-discovery-composed-annotation") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AutoDiscoveryComposedAnnotationTest { + @Autowired ConfigurableApplicationContext applicationContext; + + @Autowired WorkflowClient workflowClient; + + @BeforeEach + void setUp() { + applicationContext.start(); + } + + @Test + @Timeout(value = 10) + public void testAutoDiscoveryViaComposedAnnotation() { + ComposedAnnotatedWorkflow workflow = + workflowClient.newWorkflowStub( + ComposedAnnotatedWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue("UnitTest").build()); + Assertions.assertEquals("composed:composed-activity:hi", workflow.execute("hi")); + } + + @ComponentScan( + excludeFilters = + @ComponentScan.Filter( + pattern = + "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.(bytaskqueue|byworkername)\\..*", + type = FilterType.REGEX)) + public static class Configuration { + + // Not using @Component so that it stays scoped to this test + @Bean + public ComposedAnnotatedActivityImpl composedAnnotatedActivityImpl() { + return new ComposedAnnotatedActivityImpl(); + } + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedActivityImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedActivityImpl.java new file mode 100644 index 0000000000..0f8cc40008 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedActivityImpl.java @@ -0,0 +1,12 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +import io.temporal.spring.boot.ActivityImpl; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ActivityImpl(taskQueues = "UnitTest") +public @interface ComposedActivityImpl {} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivity.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivity.java new file mode 100644 index 0000000000..0c0e88cdf3 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivity.java @@ -0,0 +1,8 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface ComposedAnnotatedActivity { + String execute(String input); +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivityImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivityImpl.java new file mode 100644 index 0000000000..790f8ce110 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedActivityImpl.java @@ -0,0 +1,10 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +@ComposedActivityImpl +public class ComposedAnnotatedActivityImpl implements ComposedAnnotatedActivity { + + @Override + public String execute(String input) { + return "composed-activity:" + input; + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflow.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflow.java new file mode 100644 index 0000000000..aad215878e --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflow.java @@ -0,0 +1,11 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface ComposedAnnotatedWorkflow { + + @WorkflowMethod + String execute(String input); +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflowImpl.java new file mode 100644 index 0000000000..10011822a6 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedAnnotatedWorkflowImpl.java @@ -0,0 +1,20 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.Workflow; +import java.time.Duration; + +@ComposedWorkflowImpl +public class ComposedAnnotatedWorkflowImpl implements ComposedAnnotatedWorkflow { + + @Override + public String execute(String input) { + ComposedAnnotatedActivity activity = + Workflow.newActivityStub( + ComposedAnnotatedActivity.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(1)) + .validateAndBuildWithDefaults()); + return "composed:" + activity.execute(input); + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedWorkflowImpl.java new file mode 100644 index 0000000000..77b97c4540 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/composedannotation/ComposedWorkflowImpl.java @@ -0,0 +1,12 @@ +package io.temporal.spring.boot.autoconfigure.composedannotation; + +import io.temporal.spring.boot.WorkflowImpl; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@WorkflowImpl(taskQueues = "UnitTest") +public @interface ComposedWorkflowImpl {} diff --git a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml index d33d50b46d..afc8654c92 100644 --- a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml +++ b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml @@ -109,6 +109,18 @@ spring: register-activity-beans: true register-nexus-service-beans: true +--- +spring: + config: + activate: + on-profile: auto-discovery-composed-annotation + temporal: + workers-auto-discovery: + enabled: true + workflow-packages: + - io.temporal.spring.boot.autoconfigure.composedannotation + register-activity-beans: true + --- spring: config: From 08bc7383e1445579840627317cee18e5222fa06b Mon Sep 17 00:00:00 2001 From: Muhammad Rifqi Fatchurrahman Date: Fri, 21 Aug 2026 01:39:58 +0700 Subject: [PATCH 2/2] Fix NPE by using AnnotationUtils for lookups --- .../spring/boot/autoconfigure/template/WorkersTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java index 6fb972cf0c..dfbc18d7f6 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java @@ -224,7 +224,7 @@ private void configureWorkflowImplementationsByTaskQueue( Workers workers, Collection> autoDiscoveredWorkflowImplementationClasses) { for (Class clazz : autoDiscoveredWorkflowImplementationClasses) { - WorkflowImpl annotation = clazz.getAnnotation(WorkflowImpl.class); + WorkflowImpl annotation = AnnotationUtils.findAnnotation(clazz, WorkflowImpl.class); for (String taskQueue : annotation.taskQueues()) { taskQueue = environment.resolvePlaceholders(taskQueue); Worker worker = workerFactory.tryGetWorker(taskQueue); @@ -303,7 +303,7 @@ private void configureNexusServiceBeansByTaskQueue( private void configureWorkflowImplementationsByWorkerName( Workers workers, Collection> autoDiscoveredWorkflowImplementationClasses) { for (Class clazz : autoDiscoveredWorkflowImplementationClasses) { - WorkflowImpl annotation = clazz.getAnnotation(WorkflowImpl.class); + WorkflowImpl annotation = AnnotationUtils.findAnnotation(clazz, WorkflowImpl.class); for (String workerName : annotation.workers()) { Worker worker = workers.getByName(workerName);