Skip to content

Commit e1e5413

Browse files
committed
Add explicit opt-in flags for activity/nexus auto-discovery in Spring Boot
Fixes #2780: @ActivityImpl and @NexusServiceImpl beans were silently not registered unless the workflow-scanning packages property was set. New properties under spring.temporal.workers-auto-discovery: - register-activities: true — opt-in to @ActivityImpl bean registration - register-nexus-operations: true — opt-in to @NexusServiceImpl registration - workflow-packages — replaces deprecated packages (workflow scanning only) - enable: true — convenience flag to register all Spring-bean implementations (@WorkflowImpl, @ActivityImpl, @NexusServiceImpl) without package scanning The deprecated packages property retains its old behavior (implies both register flags and acts as workflow-packages) but cannot be combined with any of the new properties (throws at startup if mixed).
1 parent 80e6a1c commit e1e5413

12 files changed

Lines changed: 528 additions & 35 deletions

File tree

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/WorkersPresentCondition.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.temporal.spring.boot.autoconfigure;
22

33
import io.temporal.spring.boot.autoconfigure.properties.WorkerProperties;
4+
import io.temporal.spring.boot.autoconfigure.properties.WorkersAutoDiscoveryProperties;
45
import java.util.List;
56
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
67
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
@@ -15,11 +16,10 @@ class WorkersPresentCondition extends SpringBootCondition {
1516
private static final Bindable<List<WorkerProperties>> WORKER_PROPERTIES_LIST =
1617
Bindable.listOf(WorkerProperties.class);
1718

18-
private static final Bindable<List<String>> AUTO_DISCOVERY_PACKAGES_LIST =
19-
Bindable.listOf(String.class);
19+
private static final Bindable<WorkersAutoDiscoveryProperties> AUTO_DISCOVERY_BINDABLE =
20+
Bindable.of(WorkersAutoDiscoveryProperties.class);
2021
private static final String WORKERS_KEY = "spring.temporal.workers";
21-
private static final String AUTO_DISCOVERY_KEY =
22-
"spring.temporal.workers-auto-discovery.packages";
22+
private static final String AUTO_DISCOVERY_KEY = "spring.temporal.workers-auto-discovery";
2323

2424
public WorkersPresentCondition() {}
2525

@@ -34,8 +34,8 @@ public ConditionOutcome getMatchOutcome(
3434
}
3535

3636
BindResult<?> autoDiscoveryProperty =
37-
Binder.get(context.getEnvironment()).bind(AUTO_DISCOVERY_KEY, AUTO_DISCOVERY_PACKAGES_LIST);
38-
messageBuilder = ConditionMessage.forCondition("Auto Discovery Packages Set");
37+
Binder.get(context.getEnvironment()).bind(AUTO_DISCOVERY_KEY, AUTO_DISCOVERY_BINDABLE);
38+
messageBuilder = ConditionMessage.forCondition("Workers Auto Discovery Set");
3939
if (autoDiscoveryProperty.isBound()) {
4040
return ConditionOutcome.match(messageBuilder.found("property").items(AUTO_DISCOVERY_KEY));
4141
}

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/WorkersAutoDiscoveryProperties.java

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,113 @@
11
package io.temporal.spring.boot.autoconfigure.properties;
22

3+
import java.util.ArrayList;
34
import java.util.List;
45
import javax.annotation.Nullable;
56
import org.springframework.boot.context.properties.ConstructorBinding;
67

78
public class WorkersAutoDiscoveryProperties {
8-
private final @Nullable List<String> packages;
9+
/**
10+
* When {@code true}, enables auto-registration of all {@code @ActivityImpl},
11+
* {@code @NexusServiceImpl}, and {@code @WorkflowImpl}-annotated Spring beans. This is the
12+
* recommended option for simple use cases where all implementations are Spring-managed beans. It
13+
* implies {@link #registerActivities} and {@link #registerNexusOperations} default to {@code
14+
* true}, and also enables auto-registration of {@code @WorkflowImpl}-annotated classes that are
15+
* Spring-managed beans, without needing {@link #workflowPackages}.
16+
*
17+
* <p>Classpath-scanning via {@link #workflowPackages} (for non-bean workflow classes) still
18+
* requires explicit package configuration regardless of this flag.
19+
*/
20+
private final @Nullable Boolean enable;
21+
22+
private final @Nullable List<String> workflowPackages;
23+
private final @Nullable Boolean registerActivities;
24+
private final @Nullable Boolean registerNexusOperations;
25+
26+
/**
27+
* @deprecated Use {@link #workflowPackages} instead. If set and non-empty, this property causes
28+
* {@link #registerActivities} and {@link #registerNexusOperations} to default to {@code
29+
* true}, and its entries to be considered as if they were provided through {@link
30+
* #workflowPackages}. Setting both {@link #packages} and any of the other new properties is
31+
* unsupported and will result in an exception.
32+
*/
33+
@Deprecated private final @Nullable List<String> packages;
934

1035
@ConstructorBinding
11-
public WorkersAutoDiscoveryProperties(@Nullable List<String> packages) {
36+
public WorkersAutoDiscoveryProperties(
37+
@Nullable Boolean enable,
38+
@Nullable List<String> workflowPackages,
39+
@Nullable Boolean registerActivities,
40+
@Nullable Boolean registerNexusOperations,
41+
@Nullable List<String> packages) {
42+
if (packages != null
43+
&& !packages.isEmpty()
44+
&& (enable != null
45+
|| workflowPackages != null
46+
|| registerActivities != null
47+
|| registerNexusOperations != null)) {
48+
throw new IllegalStateException(
49+
"spring.temporal.workers-auto-discovery.packages is deprecated and cannot be combined "
50+
+ "with enable, workflow-packages, register-activities, or register-nexus-operations. "
51+
+ "Migrate to the new properties and remove packages.");
52+
}
53+
this.enable = enable;
54+
this.workflowPackages = workflowPackages;
55+
this.registerActivities = registerActivities;
56+
this.registerNexusOperations = registerNexusOperations;
1257
this.packages = packages;
1358
}
1459

60+
/**
61+
* Returns whether {@code @WorkflowImpl}-annotated Spring beans should be automatically registered
62+
* with matching workers (without classpath scanning). Defaults to {@code true} when {@link
63+
* #enable} is {@code true}, and to {@code false} otherwise. Note: non-bean workflow classes still
64+
* require {@link #workflowPackages} for classpath scanning.
65+
*/
66+
public boolean isRegisterWorkflowBeans() {
67+
return Boolean.TRUE.equals(enable);
68+
}
69+
70+
/**
71+
* Returns whether {@code @ActivityImpl}-annotated beans should be automatically registered with
72+
* matching workers. Defaults to {@code true} when {@link #enable} is {@code true} or when the
73+
* deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
74+
*/
75+
public boolean isRegisterActivities() {
76+
if (registerActivities != null) return registerActivities;
77+
return Boolean.TRUE.equals(enable) || (packages != null && !packages.isEmpty());
78+
}
79+
80+
/**
81+
* Returns whether {@code @NexusServiceImpl}-annotated beans should be automatically registered
82+
* with matching workers. Defaults to {@code true} when {@link #enable} is {@code true} or when
83+
* the deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
84+
*/
85+
public boolean isRegisterNexusOperations() {
86+
if (registerNexusOperations != null) return registerNexusOperations;
87+
return Boolean.TRUE.equals(enable) || (packages != null && !packages.isEmpty());
88+
}
89+
90+
/**
91+
* Returns the list of packages to scan for {@code @WorkflowImpl} classes. When the deprecated
92+
* {@link #packages} property is set, its entries are used; otherwise returns the entries of
93+
* {@link #workflowPackages}. The two properties cannot be set simultaneously.
94+
*/
95+
public List<String> getEffectiveWorkflowPackages() {
96+
List<String> result = new ArrayList<>();
97+
if (packages != null) result.addAll(packages);
98+
if (workflowPackages != null) result.addAll(workflowPackages);
99+
return result;
100+
}
101+
102+
@Nullable
103+
public List<String> getWorkflowPackages() {
104+
return workflowPackages;
105+
}
106+
107+
/**
108+
* @deprecated Use {@link #getWorkflowPackages()} instead.
109+
*/
110+
@Deprecated
15111
@Nullable
16112
public List<String> getPackages() {
17113
return packages;

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.temporal.spring.boot.WorkflowImpl;
2222
import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties;
2323
import io.temporal.spring.boot.autoconfigure.properties.WorkerProperties;
24+
import io.temporal.spring.boot.autoconfigure.properties.WorkersAutoDiscoveryProperties;
2425
import io.temporal.worker.*;
2526
import io.temporal.workflow.DynamicWorkflow;
2627
import java.lang.reflect.Constructor;
@@ -179,22 +180,40 @@ private Collection<Worker> createWorkers(WorkerFactory workerFactory) {
179180
createWorkerFromAnExplicitConfig(workerFactory, workerProperties, workers));
180181
}
181182

182-
if (namespaceProperties.getWorkersAutoDiscovery() != null
183-
&& namespaceProperties.getWorkersAutoDiscovery().getPackages() != null) {
184-
Collection<Class<?>> autoDiscoveredWorkflowImplementationClasses =
185-
autoDiscoverWorkflowImplementations();
186-
Map<String, Object> autoDiscoveredActivityBeans = autoDiscoverActivityBeans();
187-
Map<String, Object> autoDiscoveredNexusServiceBeans = autoDiscoverNexusServiceBeans();
183+
WorkersAutoDiscoveryProperties autoDiscovery = namespaceProperties.getWorkersAutoDiscovery();
184+
if (autoDiscovery != null) {
185+
// @ActivityImpl beans are Spring beans, discoverable without classpath scanning.
186+
if (autoDiscovery.isRegisterActivities()) {
187+
Map<String, Object> autoDiscoveredActivityBeans = autoDiscoverActivityBeans();
188+
configureActivityBeansByTaskQueue(workerFactory, workers, autoDiscoveredActivityBeans);
189+
configureActivityBeansByWorkerName(workers, autoDiscoveredActivityBeans);
190+
}
191+
192+
// @NexusServiceImpl beans are Spring beans, discoverable without classpath scanning.
193+
if (autoDiscovery.isRegisterNexusOperations()) {
194+
Map<String, Object> autoDiscoveredNexusServiceBeans = autoDiscoverNexusServiceBeans();
195+
configureNexusServiceBeansByTaskQueue(
196+
workerFactory, workers, autoDiscoveredNexusServiceBeans);
197+
configureNexusServiceBeansByWorkerName(workers, autoDiscoveredNexusServiceBeans);
198+
}
188199

189-
configureWorkflowImplementationsByTaskQueue(
190-
workerFactory, workers, autoDiscoveredWorkflowImplementationClasses);
191-
configureActivityBeansByTaskQueue(workerFactory, workers, autoDiscoveredActivityBeans);
192-
configureNexusServiceBeansByTaskQueue(
193-
workerFactory, workers, autoDiscoveredNexusServiceBeans);
194-
configureWorkflowImplementationsByWorkerName(
195-
workers, autoDiscoveredWorkflowImplementationClasses);
196-
configureActivityBeansByWorkerName(workers, autoDiscoveredActivityBeans);
197-
configureNexusServiceBeansByWorkerName(workers, autoDiscoveredNexusServiceBeans);
200+
// Workflow discovery: Spring-bean-based (enable: true) and/or classpath-scanning (packages).
201+
// These two sources are unioned; duplicates are harmless because Set is used internally.
202+
Set<Class<?>> autoDiscoveredWorkflowImplementationClasses = new HashSet<>();
203+
if (autoDiscovery.isRegisterWorkflowBeans()) {
204+
autoDiscoveredWorkflowImplementationClasses.addAll(autoDiscoverWorkflowBeans());
205+
}
206+
List<String> workflowPackages = autoDiscovery.getEffectiveWorkflowPackages();
207+
if (!workflowPackages.isEmpty()) {
208+
autoDiscoveredWorkflowImplementationClasses.addAll(
209+
autoDiscoverWorkflowImplementations(workflowPackages));
210+
}
211+
if (!autoDiscoveredWorkflowImplementationClasses.isEmpty()) {
212+
configureWorkflowImplementationsByTaskQueue(
213+
workerFactory, workers, autoDiscoveredWorkflowImplementationClasses);
214+
configureWorkflowImplementationsByWorkerName(
215+
workers, autoDiscoveredWorkflowImplementationClasses);
216+
}
198217
}
199218

200219
return workers.getWorkers();
@@ -350,12 +369,12 @@ private void configureNexusServiceBeansByWorkerName(
350369
});
351370
}
352371

353-
private Collection<Class<?>> autoDiscoverWorkflowImplementations() {
372+
private Collection<Class<?>> autoDiscoverWorkflowImplementations(List<String> packages) {
354373
ClassPathScanningCandidateComponentProvider scanner =
355374
new ClassPathScanningCandidateComponentProvider(false, environment);
356375
scanner.addIncludeFilter(new AnnotationTypeFilter(WorkflowImpl.class));
357376
Set<Class<?>> implementations = new HashSet<>();
358-
for (String pckg : namespaceProperties.getWorkersAutoDiscovery().getPackages()) {
377+
for (String pckg : packages) {
359378
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(pckg);
360379
for (BeanDefinition beanDefinition : candidateComponents) {
361380
try {
@@ -369,6 +388,12 @@ private Collection<Class<?>> autoDiscoverWorkflowImplementations() {
369388
return implementations;
370389
}
371390

391+
private Collection<Class<?>> autoDiscoverWorkflowBeans() {
392+
return beanFactory.getBeansWithAnnotation(WorkflowImpl.class).values().stream()
393+
.map(AopUtils::getTargetClass)
394+
.collect(java.util.stream.Collectors.toSet());
395+
}
396+
372397
private Map<String, Object> autoDiscoverActivityBeans() {
373398
return beanFactory.getBeansWithAnnotation(ActivityImpl.class);
374399
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.temporal.spring.boot.autoconfigure;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate;
6+
import java.util.Map;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.TestInstance;
10+
import org.junit.jupiter.api.Timeout;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.boot.test.context.SpringBootTest;
13+
import org.springframework.context.ConfigurableApplicationContext;
14+
import org.springframework.context.annotation.ComponentScan;
15+
import org.springframework.context.annotation.FilterType;
16+
import org.springframework.test.context.ActiveProfiles;
17+
18+
/**
19+
* Regression test for https://github.com/temporalio/sdk-java/issues/2780:
20+
* {@code @ActivityImpl}-annotated beans should be auto-registered with workers even when no
21+
* workflow packages are configured under {@code spring.temporal.workers-auto-discovery.packages}.
22+
*/
23+
@SpringBootTest(classes = AutoDiscoveryActivitiesOnlyTest.Configuration.class)
24+
@ActiveProfiles(profiles = "auto-discovery-activities-only")
25+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
26+
public class AutoDiscoveryActivitiesOnlyTest {
27+
28+
@Autowired ConfigurableApplicationContext applicationContext;
29+
30+
@Autowired private WorkersTemplate workersTemplate;
31+
32+
@BeforeEach
33+
void setUp() {
34+
applicationContext.start();
35+
}
36+
37+
@Test
38+
@Timeout(value = 10)
39+
public void testActivityBeansRegisteredWithoutWorkflowPackages() {
40+
assertNotNull(workersTemplate);
41+
Map<String, WorkersTemplate.RegisteredInfo> registeredInfoMap =
42+
workersTemplate.getRegisteredInfo();
43+
44+
// One worker should have been created for the task queue specified in @ActivityImpl
45+
assertEquals(1, registeredInfoMap.size());
46+
registeredInfoMap.forEach(
47+
(taskQueue, info) -> {
48+
assertEquals("UnitTest", taskQueue);
49+
50+
// No workflow packages configured, so no workflows should be registered
51+
assertTrue(
52+
info.getRegisteredWorkflowInfo().isEmpty(),
53+
"No workflows expected when packages: [] is configured");
54+
55+
// @ActivityImpl bean should be registered despite no packages being configured
56+
assertFalse(
57+
info.getRegisteredActivityInfo().isEmpty(),
58+
"@ActivityImpl beans should be auto-registered without workflow packages");
59+
assertEquals(1, info.getRegisteredActivityInfo().size());
60+
assertEquals(
61+
"io.temporal.spring.boot.autoconfigure.bytaskqueue.TestActivityImpl",
62+
info.getRegisteredActivityInfo().get(0).getClassName());
63+
64+
// @NexusServiceImpl bean should also be registered
65+
assertFalse(
66+
info.getRegisteredNexusServiceInfos().isEmpty(),
67+
"@NexusServiceImpl beans should be auto-registered without workflow packages");
68+
assertEquals(1, info.getRegisteredNexusServiceInfos().size());
69+
});
70+
}
71+
72+
@ComponentScan(
73+
excludeFilters =
74+
@ComponentScan.Filter(
75+
pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*",
76+
type = FilterType.REGEX))
77+
public static class Configuration {}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.temporal.spring.boot.autoconfigure;
2+
3+
import io.temporal.api.nexus.v1.Endpoint;
4+
import io.temporal.client.WorkflowClient;
5+
import io.temporal.client.WorkflowOptions;
6+
import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow;
7+
import io.temporal.testing.TestWorkflowEnvironment;
8+
import org.junit.jupiter.api.*;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.context.ConfigurableApplicationContext;
12+
import org.springframework.context.annotation.ComponentScan;
13+
import org.springframework.context.annotation.FilterType;
14+
import org.springframework.test.context.ActiveProfiles;
15+
16+
/**
17+
* Verifies that the deprecated {@code workers-auto-discovery.packages} property still correctly
18+
* registers workflows, activities, and nexus services (backward compatibility).
19+
*/
20+
@SpringBootTest(classes = AutoDiscoveryByTaskQueueLegacyTest.Configuration.class)
21+
@ActiveProfiles(profiles = "auto-discovery-by-task-queue-legacy")
22+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
23+
public class AutoDiscoveryByTaskQueueLegacyTest {
24+
@Autowired ConfigurableApplicationContext applicationContext;
25+
26+
@Autowired TestWorkflowEnvironment testWorkflowEnvironment;
27+
28+
@Autowired WorkflowClient workflowClient;
29+
Endpoint endpoint;
30+
31+
@BeforeEach
32+
void setUp() {
33+
applicationContext.start();
34+
endpoint =
35+
testWorkflowEnvironment.createNexusEndpoint("AutoDiscoveryByTaskQueueEndpoint", "UnitTest");
36+
}
37+
38+
@AfterEach
39+
void tearDown() {
40+
testWorkflowEnvironment.deleteNexusEndpoint(endpoint);
41+
}
42+
43+
@Test
44+
@Timeout(value = 10)
45+
public void testAutoDiscovery() {
46+
TestWorkflow testWorkflow =
47+
workflowClient.newWorkflowStub(
48+
TestWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue("UnitTest").build());
49+
testWorkflow.execute("nexus");
50+
}
51+
52+
@ComponentScan(
53+
excludeFilters =
54+
@ComponentScan.Filter(
55+
pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*",
56+
type = FilterType.REGEX))
57+
public static class Configuration {}
58+
}

0 commit comments

Comments
 (0)