Skip to content

Commit 02182b9

Browse files
Support plugins in spring-boot-autoconfigure (#2766)
* [WIP] Support plugins in spring-boot-autoconfigure * attempt to properly register things to account for plugin propagaion * Maintain backwards compat * Extract out fitlerPlugins * Review pass over this * remove deprecated constructor * Change filterPlugins to accept varargs exclude types Instead of nesting filterPlugins calls, accept multiple exclude types in a single call for cleaner usage at call sites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * formatting --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fb2defd commit 02182b9

14 files changed

+844
-25
lines changed

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,51 @@ static String temporalCustomizerBeanName(String beanPrefix, Class<?> optionsBuil
168168
bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1);
169169
return beanPrefix + bindingCustomizerName;
170170
}
171+
172+
/**
173+
* Filter out plugins that implement a higher-level plugin interface, as those are handled at that
174+
* higher level via propagation.
175+
*
176+
* <p>The plugin hierarchy is: WorkflowServiceStubsPlugin -> WorkflowClientPlugin -> WorkerPlugin.
177+
* A plugin implementing a higher-level interface will be registered at that level and propagate
178+
* down automatically, so it should not also be registered at lower levels.
179+
*
180+
* @param plugins the list of plugins to filter (may be null)
181+
* @param excludeTypes the plugin interfaces to exclude
182+
* @return the filtered list, or null if input was null or all plugins were filtered out
183+
*/
184+
static <T> @Nullable List<T> filterPlugins(@Nullable List<T> plugins, Class<?>... excludeTypes) {
185+
if (plugins == null || plugins.isEmpty()) {
186+
return null;
187+
}
188+
List<T> filtered = new ArrayList<>();
189+
for (T plugin : plugins) {
190+
boolean excluded = false;
191+
for (Class<?> excludeType : excludeTypes) {
192+
if (excludeType.isInstance(plugin)) {
193+
excluded = true;
194+
break;
195+
}
196+
}
197+
if (!excluded) {
198+
filtered.add(plugin);
199+
}
200+
}
201+
return filtered.isEmpty() ? null : filtered;
202+
}
203+
204+
/**
205+
* Sort plugins by @Order and @Priority annotations for consistent ordering.
206+
*
207+
* @param plugins the list of plugins to sort (may be null)
208+
* @return the sorted list, or null if input was null
209+
*/
210+
static <T> @Nullable List<T> sortPlugins(@Nullable List<T> plugins) {
211+
if (plugins == null || plugins.isEmpty()) {
212+
return plugins;
213+
}
214+
List<T> sorted = new ArrayList<>(plugins);
215+
AnnotationAwareOrderComparator.sort(sorted);
216+
return sorted;
217+
}
171218
}

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import io.opentracing.Tracer;
66
import io.temporal.client.WorkflowClient;
77
import io.temporal.client.WorkflowClientOptions;
8+
import io.temporal.client.WorkflowClientPlugin;
89
import io.temporal.client.schedules.ScheduleClient;
910
import io.temporal.client.schedules.ScheduleClientOptions;
11+
import io.temporal.client.schedules.ScheduleClientPlugin;
1012
import io.temporal.common.converter.DataConverter;
1113
import io.temporal.serviceclient.WorkflowServiceStubs;
1214
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
15+
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
1316
import io.temporal.spring.boot.TemporalOptionsCustomizer;
1417
import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties;
1518
import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties;
@@ -23,7 +26,9 @@
2326
import io.temporal.worker.WorkerFactory;
2427
import io.temporal.worker.WorkerFactoryOptions.Builder;
2528
import io.temporal.worker.WorkerOptions;
29+
import io.temporal.worker.WorkerPlugin;
2630
import io.temporal.worker.WorkflowImplementationOptions;
31+
import java.util.ArrayList;
2732
import java.util.Collections;
2833
import java.util.List;
2934
import java.util.stream.Collectors;
@@ -50,6 +55,10 @@ public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryA
5055
private @Nullable Tracer tracer;
5156
private @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment;
5257
private @Nullable Scope metricsScope;
58+
private @Nullable List<WorkflowServiceStubsPlugin> serviceStubsPlugins;
59+
private @Nullable List<WorkflowClientPlugin> workflowClientPlugins;
60+
private @Nullable List<ScheduleClientPlugin> scheduleClientPlugins;
61+
private @Nullable List<WorkerPlugin> workerPlugins;
5362

5463
public NonRootBeanPostProcessor(@Nonnull TemporalProperties temporalProperties) {
5564
this.temporalProperties = temporalProperties;
@@ -78,6 +87,20 @@ public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull Stri
7887
findBean(
7988
"temporalTestWorkflowEnvironmentAdapter", TestWorkflowEnvironmentAdapter.class);
8089
}
90+
// Collect all plugin types
91+
serviceStubsPlugins = findAllBeans(WorkflowServiceStubsPlugin.class);
92+
// Filter plugins so each is only registered at its highest applicable level
93+
workflowClientPlugins =
94+
AutoConfigurationUtils.filterPlugins(
95+
findAllBeans(WorkflowClientPlugin.class), WorkflowServiceStubsPlugin.class);
96+
scheduleClientPlugins =
97+
AutoConfigurationUtils.filterPlugins(
98+
findAllBeans(ScheduleClientPlugin.class), WorkflowServiceStubsPlugin.class);
99+
workerPlugins =
100+
AutoConfigurationUtils.filterPlugins(
101+
findAllBeans(WorkerPlugin.class),
102+
WorkflowServiceStubsPlugin.class,
103+
WorkflowClientPlugin.class);
81104
namespaceProperties.forEach(this::injectBeanByNonRootNamespace);
82105
}
83106
}
@@ -124,7 +147,8 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
124147
connectionProperties,
125148
metricsScope,
126149
testWorkflowEnvironment,
127-
workflowServiceStubsCustomizers);
150+
workflowServiceStubsCustomizers,
151+
serviceStubsPlugins);
128152
WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs();
129153

130154
NonRootNamespaceTemplate namespaceTemplate =
@@ -142,7 +166,10 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
142166
workerCustomizers,
143167
workflowClientCustomizers,
144168
scheduleClientCustomizers,
145-
workflowImplementationCustomizers);
169+
workflowImplementationCustomizers,
170+
workflowClientPlugins,
171+
scheduleClientPlugins,
172+
workerPlugins);
146173

147174
ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate();
148175
WorkflowClient workflowClient = clientTemplate.getWorkflowClient();
@@ -200,6 +227,16 @@ private <T> T findBeanByNamespace(String beanPrefix, Class<T> clazz) {
200227
return null;
201228
}
202229

230+
private <T> @Nullable List<T> findAllBeans(Class<T> clazz) {
231+
try {
232+
List<T> beans = new ArrayList<>(beanFactory.getBeansOfType(clazz).values());
233+
return AutoConfigurationUtils.sortPlugins(beans);
234+
} catch (NoSuchBeanDefinitionException ignore) {
235+
// No beans of this type defined - this is expected for optional plugins
236+
}
237+
return null;
238+
}
239+
203240
private <T> List<TemporalOptionsCustomizer<T>> findBeanByNameSpaceForTemporalCustomizer(
204241
String beanPrefix, Class<T> genericOptionsBuilderClass) {
205242
String beanName =

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
import io.opentracing.Tracer;
44
import io.temporal.client.WorkflowClient;
55
import io.temporal.client.WorkflowClientOptions;
6+
import io.temporal.client.WorkflowClientPlugin;
67
import io.temporal.client.schedules.ScheduleClient;
78
import io.temporal.client.schedules.ScheduleClientOptions;
9+
import io.temporal.client.schedules.ScheduleClientPlugin;
810
import io.temporal.common.converter.DataConverter;
911
import io.temporal.common.interceptors.ScheduleClientInterceptor;
1012
import io.temporal.common.interceptors.WorkerInterceptor;
1113
import io.temporal.common.interceptors.WorkflowClientInterceptor;
1214
import io.temporal.serviceclient.WorkflowServiceStubs;
15+
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
1316
import io.temporal.spring.boot.TemporalOptionsCustomizer;
1417
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
1518
import io.temporal.spring.boot.autoconfigure.template.ClientTemplate;
@@ -20,6 +23,7 @@
2023
import io.temporal.worker.WorkerFactory;
2124
import io.temporal.worker.WorkerFactoryOptions;
2225
import io.temporal.worker.WorkerOptions;
26+
import io.temporal.worker.WorkerPlugin;
2327
import io.temporal.worker.WorkflowImplementationOptions;
2428
import java.util.Collection;
2529
import java.util.List;
@@ -87,7 +91,10 @@ public NamespaceTemplate rootNamespaceTemplate(
8791
scheduleCustomizerMap,
8892
@Autowired(required = false) @Nullable
8993
Map<String, TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>>
90-
workflowImplementationCustomizerMap) {
94+
workflowImplementationCustomizerMap,
95+
@Autowired(required = false) @Nullable List<WorkflowClientPlugin> workflowClientPlugins,
96+
@Autowired(required = false) @Nullable List<ScheduleClientPlugin> scheduleClientPlugins,
97+
@Autowired(required = false) @Nullable List<WorkerPlugin> workerPlugins) {
9198
DataConverter chosenDataConverter =
9299
AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties);
93100
List<WorkflowClientInterceptor> chosenClientInterceptors =
@@ -121,6 +128,29 @@ public NamespaceTemplate rootNamespaceTemplate(
121128
WorkflowImplementationOptions.Builder.class,
122129
properties);
123130

131+
// Sort plugins by @Order/@Priority for consistent ordering
132+
List<WorkflowClientPlugin> sortedClientPlugins =
133+
AutoConfigurationUtils.sortPlugins(workflowClientPlugins);
134+
List<ScheduleClientPlugin> sortedSchedulePlugins =
135+
AutoConfigurationUtils.sortPlugins(scheduleClientPlugins);
136+
List<WorkerPlugin> sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins);
137+
138+
// Filter plugins so each is only registered at its highest applicable level.
139+
// WorkflowServiceStubsPlugin is handled at ServiceStubsAutoConfiguration level and propagates
140+
// down.
141+
// WorkflowClientPlugin (not WorkflowServiceStubsPlugin) is handled here and propagates to
142+
// workers.
143+
// ScheduleClientPlugin (not WorkflowServiceStubsPlugin) is handled here.
144+
// WorkerPlugin (not WorkflowServiceStubsPlugin, not WorkflowClientPlugin) is handled here.
145+
List<WorkflowClientPlugin> filteredClientPlugins =
146+
AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class);
147+
List<ScheduleClientPlugin> filteredSchedulePlugins =
148+
AutoConfigurationUtils.filterPlugins(
149+
sortedSchedulePlugins, WorkflowServiceStubsPlugin.class);
150+
List<WorkerPlugin> filteredWorkerPlugins =
151+
AutoConfigurationUtils.filterPlugins(
152+
sortedWorkerPlugins, WorkflowServiceStubsPlugin.class, WorkflowClientPlugin.class);
153+
124154
return new NamespaceTemplate(
125155
properties,
126156
workflowServiceStubs,
@@ -134,7 +164,10 @@ public NamespaceTemplate rootNamespaceTemplate(
134164
workerCustomizer,
135165
clientCustomizer,
136166
scheduleCustomizer,
137-
workflowImplementationCustomizer);
167+
workflowImplementationCustomizer,
168+
filteredClientPlugins,
169+
filteredSchedulePlugins,
170+
filteredWorkerPlugins);
138171
}
139172

140173
/** Client */

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.temporal.serviceclient.WorkflowServiceStubs;
55
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
66
import io.temporal.serviceclient.WorkflowServiceStubsOptions.Builder;
7+
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
78
import io.temporal.spring.boot.TemporalOptionsCustomizer;
89
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
910
import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate;
@@ -42,15 +43,19 @@ public ServiceStubsTemplate serviceStubsTemplate(
4243
TestWorkflowEnvironmentAdapter testWorkflowEnvironment,
4344
@Autowired(required = false) @Nullable
4445
Map<String, TemporalOptionsCustomizer<WorkflowServiceStubsOptions.Builder>>
45-
workflowServiceStubsCustomizerMap) {
46+
workflowServiceStubsCustomizerMap,
47+
@Autowired(required = false) @Nullable List<WorkflowServiceStubsPlugin> plugins) {
4648
List<TemporalOptionsCustomizer<Builder>> workflowServiceStubsCustomizer =
4749
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
4850
beanFactory, workflowServiceStubsCustomizerMap, Builder.class, properties);
51+
// Sort plugins by @Order/@Priority for consistent ordering
52+
List<WorkflowServiceStubsPlugin> sortedPlugins = AutoConfigurationUtils.sortPlugins(plugins);
4953
return new ServiceStubsTemplate(
5054
properties.getConnection(),
5155
metricsScope,
5256
testWorkflowEnvironment,
53-
workflowServiceStubsCustomizer);
57+
workflowServiceStubsCustomizer,
58+
sortedPlugins);
5459
}
5560

5661
@Bean(name = "temporalWorkflowServiceStubs")

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import com.uber.m3.tally.Scope;
44
import io.opentracing.Tracer;
55
import io.temporal.client.WorkflowClientOptions;
6+
import io.temporal.client.WorkflowClientPlugin;
67
import io.temporal.client.schedules.ScheduleClientOptions;
8+
import io.temporal.client.schedules.ScheduleClientPlugin;
79
import io.temporal.common.converter.DataConverter;
810
import io.temporal.common.interceptors.ScheduleClientInterceptor;
911
import io.temporal.common.interceptors.WorkerInterceptor;
1012
import io.temporal.common.interceptors.WorkflowClientInterceptor;
13+
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
1114
import io.temporal.spring.boot.TemporalOptionsCustomizer;
1215
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
1316
import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter;
@@ -16,6 +19,7 @@
1619
import io.temporal.testing.TestEnvironmentOptions;
1720
import io.temporal.testing.TestWorkflowEnvironment;
1821
import io.temporal.worker.WorkerFactoryOptions;
22+
import io.temporal.worker.WorkerPlugin;
1923
import java.util.List;
2024
import java.util.Map;
2125
import javax.annotation.Nullable;
@@ -79,7 +83,10 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
7983
Map<String, TemporalOptionsCustomizer<WorkflowClientOptions.Builder>> clientCustomizerMap,
8084
@Autowired(required = false) @Nullable
8185
Map<String, TemporalOptionsCustomizer<ScheduleClientOptions.Builder>>
82-
scheduleCustomizerMap) {
86+
scheduleCustomizerMap,
87+
@Autowired(required = false) @Nullable List<WorkflowClientPlugin> workflowClientPlugins,
88+
@Autowired(required = false) @Nullable List<ScheduleClientPlugin> scheduleClientPlugins,
89+
@Autowired(required = false) @Nullable List<WorkerPlugin> workerPlugins) {
8390
DataConverter chosenDataConverter =
8491
AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties);
8592
List<WorkflowClientInterceptor> chosenClientInterceptors =
@@ -104,6 +111,33 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
104111
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
105112
beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties);
106113

114+
// Sort plugins by @Order/@Priority for consistent ordering
115+
List<WorkflowClientPlugin> sortedClientPlugins =
116+
AutoConfigurationUtils.sortPlugins(workflowClientPlugins);
117+
List<ScheduleClientPlugin> sortedSchedulePlugins =
118+
AutoConfigurationUtils.sortPlugins(scheduleClientPlugins);
119+
List<WorkerPlugin> sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins);
120+
121+
// Filter plugins so each is only registered at its highest applicable level.
122+
// Note: TestWorkflowEnvironment creates its own internal test server and does not accept
123+
// WorkflowServiceStubsPlugin directly. Any beans implementing WorkflowServiceStubsPlugin
124+
// will be ignored in test mode - only their lower-level plugin functionality (if any) is used.
125+
if (sortedClientPlugins != null
126+
&& sortedClientPlugins.stream().anyMatch(WorkflowServiceStubsPlugin.class::isInstance)) {
127+
log.warn(
128+
"WorkflowServiceStubsPlugin beans are present but will be ignored in test mode. "
129+
+ "TestWorkflowEnvironment creates its own test server and does not support "
130+
+ "WorkflowServiceStubsPlugin. Only WorkflowClientPlugin functionality will be used.");
131+
}
132+
List<WorkflowClientPlugin> filteredClientPlugins =
133+
AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class);
134+
List<ScheduleClientPlugin> filteredSchedulePlugins =
135+
AutoConfigurationUtils.filterPlugins(
136+
sortedSchedulePlugins, WorkflowServiceStubsPlugin.class);
137+
List<WorkerPlugin> filteredWorkerPlugins =
138+
AutoConfigurationUtils.filterPlugins(
139+
sortedWorkerPlugins, WorkflowServiceStubsPlugin.class, WorkflowClientPlugin.class);
140+
107141
TestEnvironmentOptions.Builder options =
108142
TestEnvironmentOptions.newBuilder()
109143
.setWorkflowClientOptions(
@@ -114,7 +148,9 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
114148
chosenScheduleClientInterceptors,
115149
otTracer,
116150
clientCustomizer,
117-
scheduleCustomizer)
151+
scheduleCustomizer,
152+
filteredClientPlugins,
153+
filteredSchedulePlugins)
118154
.createWorkflowClientOptions());
119155

120156
if (metricsScope != null) {
@@ -123,7 +159,11 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
123159

124160
options.setWorkerFactoryOptions(
125161
new WorkerFactoryOptionsTemplate(
126-
properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer)
162+
properties,
163+
chosenWorkerInterceptors,
164+
otTracer,
165+
workerFactoryCustomizer,
166+
filteredWorkerPlugins)
127167
.createWorkerFactoryOptions());
128168

129169
if (testEnvOptionsCustomizers != null) {

0 commit comments

Comments
 (0)