Skip to content

Commit 8716d22

Browse files
authored
Add explicit opt-in flags for activity/nexus bean auto-discovery in Spring Boot (#2830)
1 parent 2bbf52d commit 8716d22

File tree

12 files changed

+536
-35
lines changed

12 files changed

+536
-35
lines changed

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: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,121 @@
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;
7+
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
68

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

1037
@ConstructorBinding
11-
public WorkersAutoDiscoveryProperties(@Nullable List<String> packages) {
38+
public WorkersAutoDiscoveryProperties(
39+
@Nullable Boolean enabled,
40+
@Nullable List<String> workflowPackages,
41+
@Nullable Boolean registerActivityBeans,
42+
@Nullable Boolean registerNexusServiceBeans,
43+
@Nullable List<String> packages) {
44+
if (packages != null
45+
&& !packages.isEmpty()
46+
&& (enabled != null
47+
|| workflowPackages != null
48+
|| registerActivityBeans != null
49+
|| registerNexusServiceBeans != null)) {
50+
throw new IllegalStateException(
51+
"spring.temporal.workers-auto-discovery.packages is deprecated and cannot be combined "
52+
+ "with enabled, workflow-packages, register-activity-beans, or register-nexus-service-beans. "
53+
+ "Migrate to the new properties and remove packages.");
54+
}
55+
this.enabled = enabled;
56+
this.workflowPackages = workflowPackages;
57+
this.registerActivityBeans = registerActivityBeans;
58+
this.registerNexusServiceBeans = registerNexusServiceBeans;
1259
this.packages = packages;
1360
}
1461

62+
/**
63+
* Returns whether {@code @WorkflowImpl}-annotated classes that are also Spring-managed beans
64+
* should be automatically registered with matching workers (without classpath scanning). Defaults
65+
* to {@code true} when {@link #enabled} is {@code true}, {@code false} otherwise. Workflow
66+
* implementation classes that are not Spring-managed beans still require {@link
67+
* #workflowPackages} for classpath scanning.
68+
*/
69+
public boolean isRegisterWorkflowManagedBeans() {
70+
return Boolean.TRUE.equals(enabled);
71+
}
72+
73+
/**
74+
* Returns whether {@code @ActivityImpl}-annotated beans should be automatically registered with
75+
* matching workers. Defaults to {@code true} when {@link #enabled} is {@code true} or when the
76+
* deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
77+
*/
78+
public boolean isRegisterActivityBeans() {
79+
if (registerActivityBeans != null) return registerActivityBeans;
80+
return Boolean.TRUE.equals(enabled) || (packages != null && !packages.isEmpty());
81+
}
82+
83+
/**
84+
* Returns whether {@code @NexusServiceImpl}-annotated beans should be automatically registered
85+
* with matching workers. Defaults to {@code true} when {@link #enabled} is {@code true} or when
86+
* the deprecated {@link #packages} property is set and non-empty; {@code false} otherwise.
87+
*/
88+
public boolean isRegisterNexusServiceBeans() {
89+
if (registerNexusServiceBeans != null) return registerNexusServiceBeans;
90+
return Boolean.TRUE.equals(enabled) || (packages != null && !packages.isEmpty());
91+
}
92+
93+
/**
94+
* Returns the list of packages to scan for {@code @WorkflowImpl} classes. When the deprecated
95+
* {@link #packages} property is set, its entries are used; otherwise returns the entries of
96+
* {@link #workflowPackages}. The two properties cannot be set simultaneously.
97+
*/
98+
public List<String> getEffectiveWorkflowPackages() {
99+
List<String> result = new ArrayList<>();
100+
if (packages != null) result.addAll(packages);
101+
if (workflowPackages != null) result.addAll(workflowPackages);
102+
return result;
103+
}
104+
105+
@Nullable
106+
public List<String> getWorkflowPackages() {
107+
return workflowPackages;
108+
}
109+
110+
/**
111+
* @deprecated `packages` has unclear semantics with regard to registration of activity and nexus
112+
* service beans; use {@link #getWorkflowPackages()} instead.
113+
*/
114+
@Deprecated
115+
@DeprecatedConfigurationProperty(
116+
replacement = "spring.temporal.workers-auto-discovery.workflow-packages",
117+
reason =
118+
"'packages' has unclear semantics with regard to registration of activity and nexus service beans; use 'workflow-packages' instead")
15119
@Nullable
16120
public List<String> getPackages() {
17121
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.isRegisterActivityBeans()) {
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.isRegisterNexusServiceBeans()) {
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 (enabled: 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.isRegisterWorkflowManagedBeans()) {
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)