From 73cff965b98ed4e15b808de03a475935f77a3ff2 Mon Sep 17 00:00:00 2001 From: Samuel Vila Date: Mon, 12 Oct 2026 05:22:08 +0200 Subject: [PATCH] Resolve property placeholders in worker names The workers attribute of @WorkflowImpl, @ActivityImpl and @NexusServiceImpl was used verbatim, while taskQueues already went through the Spring environment's placeholder resolution. A worker name such as ${my.worker.name} was therefore looked up literally and startup failed with BeanDefinitionValidationException, which prevented sharing a workflow module between services with different worker names. Resolve placeholders in worker names before the worker lookup, the same way task queues are resolved, and document it on the annotations. Fixes #2747. --- CHANGELOG.md | 2 + .../io/temporal/spring/boot/ActivityImpl.java | 2 +- .../spring/boot/NexusServiceImpl.java | 2 +- .../io/temporal/spring/boot/WorkflowImpl.java | 3 +- .../template/WorkersTemplate.java | 3 + ...AutoDiscoveryByWorkerNameResolverTest.java | 65 +++++++++++++++++++ .../byworkername/OtherTestActivityImpl.java | 2 +- .../byworkername/OtherTestWorkflowImpl.java | 2 +- .../byworkername/TestActivityImpl.java | 2 +- .../byworkername/TestDynamicWorkflowImpl.java | 2 +- .../byworkername/TestNexusServiceImpl.java | 2 +- .../byworkername/TestWorkflowImpl.java | 2 +- .../src/test/resources/application.yml | 17 +++++ 13 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameResolverTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index af4bac6c19..2ce39e0abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ to docs, or any other relevant information. Child workflow overrides and one-time routing require Temporal Server 1.32.0 or later. - `WorkerFactoryOptions.Builder.setLoggerTagPrefix` that can be used to customized structured logging tags (MDC keys) set by Temporal SDK in worker context. +- Spring Boot: the `workers` attribute of `@WorkflowImpl`, `@ActivityImpl` and `@NexusServiceImpl` now resolves property + placeholders such as `${my.worker.name}`, as `taskQueues` already does. ### Changed - Release notes for all future releases are now in a single CHANGELOG.md file. `releases` directory with old release diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/ActivityImpl.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/ActivityImpl.java index a500009d8d..2e3275f9fa 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/ActivityImpl.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/ActivityImpl.java @@ -15,7 +15,7 @@ /** * @return names of Workers to register this activity bean with. Workers with these names must be * present in the application config. Worker is named by its task queue if its name is not - * specified. + * specified. Can be specified as a property key, e.g.: ${propertyKey}. */ String[] workers() default {}; diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java index 791b12da0f..c9e2dce8d7 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java @@ -15,7 +15,7 @@ /** * @return names of Workers to register this nexus service bean with. Workers with these names * must be present in the application config. Worker is named by its task queue if its name is - * not specified. + * not specified. Can be specified as a property key, e.g.: ${propertyKey}. */ String[] workers() default {}; diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/WorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/WorkflowImpl.java index ed1dc9f74d..e0986bd016 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/WorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/WorkflowImpl.java @@ -15,7 +15,8 @@ /** * @return names of Workers to register this workflow implementation with. Workers with these * names must be present in the application config or auto-discovered from {@link - * #taskQueues()}. Worker is named by its task queue if its name is not specified. + * #taskQueues()}. Worker is named by its task queue if its name is not specified. Can be + * specified by a property key, e.g.: ${propertyKey}. */ String[] workers() default {}; 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 dfbc18d7f6..46b0dc6dd5 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 @@ -306,6 +306,7 @@ private void configureWorkflowImplementationsByWorkerName( WorkflowImpl annotation = AnnotationUtils.findAnnotation(clazz, WorkflowImpl.class); for (String workerName : annotation.workers()) { + workerName = environment.resolvePlaceholders(workerName); Worker worker = workers.getByName(workerName); if (worker == null) { throw new BeanDefinitionValidationException( @@ -328,6 +329,7 @@ private void configureActivityBeansByWorkerName( ActivityImpl annotation = AnnotationUtils.findAnnotation(targetClass, ActivityImpl.class); if (annotation != null) { for (String workerName : annotation.workers()) { + workerName = environment.resolvePlaceholders(workerName); Worker worker = workers.getByName(workerName); if (worker == null) { throw new BeanDefinitionValidationException( @@ -353,6 +355,7 @@ private void configureNexusServiceBeansByWorkerName( AnnotationUtils.findAnnotation(targetClass, NexusServiceImpl.class); if (annotation != null) { for (String workerName : annotation.workers()) { + workerName = environment.resolvePlaceholders(workerName); Worker worker = workers.getByName(workerName); if (worker == null) { throw new BeanDefinitionValidationException( diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameResolverTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameResolverTest.java new file mode 100644 index 0000000000..ab5f1a9838 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameResolverTest.java @@ -0,0 +1,65 @@ +package io.temporal.spring.boot.autoconfigure; + +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow; +import io.temporal.testing.TestWorkflowEnvironment; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = AutoDiscoveryByWorkerNameResolverTest.Configuration.class) +@ActiveProfiles(profiles = "auto-discovery-by-worker-name-dynamic-name") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AutoDiscoveryByWorkerNameResolverTest { + @Autowired ConfigurableApplicationContext applicationContext; + + @Autowired TestWorkflowEnvironment testWorkflowEnvironment; + + @Autowired WorkflowClient workflowClient; + + Endpoint endpoint; + + @BeforeEach + void setUp() { + applicationContext.start(); + endpoint = + testWorkflowEnvironment.createNexusEndpoint( + "AutoDiscoveryByWorkerNameTestEndpoint", "PropertyResolverTest"); + } + + @AfterEach + void tearDown() { + testWorkflowEnvironment.deleteNexusEndpoint(endpoint); + } + + @Test + @Timeout(value = 10) + public void testAutoDiscovery() { + TestWorkflow testWorkflow = + workflowClient.newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue("PropertyResolverTest").build()); + testWorkflow.execute("nexus"); + + WorkflowStub dynamicStub = + workflowClient.newUntypedWorkflowStub( + "DynamicWorkflow", + WorkflowOptions.newBuilder().setTaskQueue("PropertyResolverTest").build()); + dynamicStub.start(); + Assertions.assertEquals("hello from dynamic workflow", dynamicStub.getResult(String.class)); + } + + @ComponentScan( + excludeFilters = + @ComponentScan.Filter( + pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.bytaskqueue\\..*", + type = FilterType.REGEX)) + public static class Configuration {} +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestActivityImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestActivityImpl.java index 128325d70a..cef95ded66 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestActivityImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestActivityImpl.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; @Component("TestActivityImpl") -@ActivityImpl(workers = "mainWorker") +@ActivityImpl(workers = "${default-worker.name:mainWorker}") @Profile("auto-discovery-with-profile") public class OtherTestActivityImpl implements TestActivity { @Override diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestWorkflowImpl.java index 2abffff02a..1170e75104 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestWorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/OtherTestWorkflowImpl.java @@ -7,7 +7,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Profile; -@WorkflowImpl(workers = "mainWorker") +@WorkflowImpl(workers = "${default-worker.name:mainWorker}") @Profile("auto-discovery-with-profile") public class OtherTestWorkflowImpl implements TestWorkflow { diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestActivityImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestActivityImpl.java index 8e68a1db4f..7b91e39827 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestActivityImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestActivityImpl.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; @Component("TestActivityImpl") -@ActivityImpl(workers = "mainWorker") +@ActivityImpl(workers = "${default-worker.name:mainWorker}") @Profile("!auto-discovery-with-profile") public class TestActivityImpl implements TestActivity { @Override diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestDynamicWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestDynamicWorkflowImpl.java index f1d3ddccd8..68f4d2c9ee 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestDynamicWorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestDynamicWorkflowImpl.java @@ -4,7 +4,7 @@ import io.temporal.spring.boot.WorkflowImpl; import io.temporal.workflow.DynamicWorkflow; -@WorkflowImpl(workers = "mainWorker") +@WorkflowImpl(workers = "${default-worker.name:mainWorker}") public class TestDynamicWorkflowImpl implements DynamicWorkflow { @Override public Object execute(EncodedValues args) { diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java index 949519504c..1786eeeefe 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component; @Component("TestNexusServiceImpl") -@NexusServiceImpl(workers = "mainWorker") +@NexusServiceImpl(workers = "${default-worker.name:mainWorker}") @ServiceImpl(service = TestNexusService.class) public class TestNexusServiceImpl { @OperationImpl diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java index 78ea99aa0a..797bd05d64 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java @@ -9,7 +9,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Profile; -@WorkflowImpl(workers = "mainWorker") +@WorkflowImpl(workers = "${default-worker.name:mainWorker}") @Profile("!auto-discovery-with-profile") public class TestWorkflowImpl implements TestWorkflow { diff --git a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml index afc8654c92..e9b8568161 100644 --- a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml +++ b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml @@ -109,6 +109,23 @@ spring: register-activity-beans: true register-nexus-service-beans: true +--- +spring: + config: + activate: + on-profile: auto-discovery-by-worker-name-dynamic-name + temporal: + workers: + - task-queue: PropertyResolverTest + name: propertyResolverWorker + workers-auto-discovery: + workflow-packages: + - io.temporal.spring.boot.autoconfigure.byworkername + register-activity-beans: true + register-nexus-service-beans: true +default-worker: + name: propertyResolverWorker + --- spring: config: