From 889a14c8828009a764aa87f5216841cf5b4f388d Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Mon, 26 Jan 2026 20:39:38 -0500 Subject: [PATCH 1/5] [WIP] Support plugins in spring-boot-autoconfigure --- .../NonRootBeanPostProcessor.java | 15 ++++++++++++- .../RootNamespaceAutoConfiguration.java | 7 ++++-- .../TestServerAutoConfiguration.java | 6 +++-- .../template/NamespaceTemplate.java | 9 ++++++-- .../template/NonRootNamespaceTemplate.java | 7 ++++-- .../WorkerFactoryOptionsTemplate.java | 22 ++++++++++++++++++- .../template/WorkersTemplate.java | 11 ++++++++-- 7 files changed, 65 insertions(+), 12 deletions(-) diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java index a0079036c..cd023171b 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java @@ -23,6 +23,7 @@ import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions.Builder; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.Collections; import java.util.List; @@ -50,6 +51,7 @@ public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryA private @Nullable Tracer tracer; private @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment; private @Nullable Scope metricsScope; + private @Nullable List plugins; public NonRootBeanPostProcessor(@Nonnull TemporalProperties temporalProperties) { this.temporalProperties = temporalProperties; @@ -78,6 +80,7 @@ public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull Stri findBean( "temporalTestWorkflowEnvironmentAdapter", TestWorkflowEnvironmentAdapter.class); } + plugins = findAllBeans(WorkerPlugin.class); namespaceProperties.forEach(this::injectBeanByNonRootNamespace); } } @@ -142,7 +145,8 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { workerCustomizers, workflowClientCustomizers, scheduleClientCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + plugins); ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate(); WorkflowClient workflowClient = clientTemplate.getWorkflowClient(); @@ -200,6 +204,15 @@ private T findBeanByNamespace(String beanPrefix, Class clazz) { return null; } + private @Nullable List findAllBeans(Class clazz) { + try { + return new java.util.ArrayList<>(beanFactory.getBeansOfType(clazz).values()); + } catch (BeansException ignore) { + // Ignore if no beans are found + } + return null; + } + private List> findBeanByNameSpaceForTemporalCustomizer( String beanPrefix, Class genericOptionsBuilderClass) { String beanName = diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java index 718fd6445..9dfcac385 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java @@ -20,6 +20,7 @@ import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.Collection; import java.util.List; @@ -87,7 +88,8 @@ public NamespaceTemplate rootNamespaceTemplate( scheduleCustomizerMap, @Autowired(required = false) @Nullable Map> - workflowImplementationCustomizerMap) { + workflowImplementationCustomizerMap, + @Autowired(required = false) @Nullable List plugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -134,7 +136,8 @@ public NamespaceTemplate rootNamespaceTemplate( workerCustomizer, clientCustomizer, scheduleCustomizer, - workflowImplementationCustomizer); + workflowImplementationCustomizer, + plugins); } /** Client */ diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java index c372b797f..27f849d3d 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java @@ -16,6 +16,7 @@ import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerPlugin; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -79,7 +80,8 @@ public TestWorkflowEnvironment testWorkflowEnvironment( Map> clientCustomizerMap, @Autowired(required = false) @Nullable Map> - scheduleCustomizerMap) { + scheduleCustomizerMap, + @Autowired(required = false) @Nullable List plugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -123,7 +125,7 @@ public TestWorkflowEnvironment testWorkflowEnvironment( options.setWorkerFactoryOptions( new WorkerFactoryOptionsTemplate( - properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer) + properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer, plugins) .createWorkerFactoryOptions()); if (testEnvOptionsCustomizers != null) { diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java index eef891870..91709be8d 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java @@ -12,6 +12,7 @@ import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.List; import javax.annotation.Nonnull; @@ -36,6 +37,7 @@ public class NamespaceTemplate { scheduleCustomizers; private final @Nullable List> workflowImplementationCustomizers; + private final @Nullable List plugins; private ClientTemplate clientTemplate; private WorkersTemplate workersTemplate; @@ -56,7 +58,8 @@ public NamespaceTemplate( @Nullable List> scheduleCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List plugins) { this.namespaceProperties = namespaceProperties; this.workflowServiceStubs = workflowServiceStubs; this.dataConverter = dataConverter; @@ -71,6 +74,7 @@ public NamespaceTemplate( this.clientCustomizers = clientCustomizers; this.scheduleCustomizers = scheduleCustomizers; this.workflowImplementationCustomizers = workflowImplementationCustomizers; + this.plugins = plugins; } public ClientTemplate getClientTemplate() { @@ -101,7 +105,8 @@ public WorkersTemplate getWorkersTemplate() { testWorkflowEnvironment, workerFactoryCustomizers, workerCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + plugins); } return this.workersTemplate; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java index f2292e385..6d7c0f2c1 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java @@ -12,6 +12,7 @@ import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.List; import javax.annotation.Nonnull; @@ -40,7 +41,8 @@ public NonRootNamespaceTemplate( @Nullable List> scheduleCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List plugins) { super( namespaceProperties, workflowServiceStubs, @@ -54,7 +56,8 @@ public NonRootNamespaceTemplate( workerCustomizers, clientCustomizers, scheduleCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + plugins); this.beanFactory = beanFactory; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java index b7b44d44f..baa037402 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java @@ -7,6 +7,7 @@ import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerPlugin; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -18,16 +19,31 @@ public class WorkerFactoryOptionsTemplate { private final @Nullable List workerInterceptors; private final @Nullable Tracer tracer; private final @Nullable List> customizer; + private final @Nullable List plugins; public WorkerFactoryOptionsTemplate( @Nonnull NamespaceProperties namespaceProperties, @Nullable List workerInterceptors, @Nullable Tracer tracer, - @Nullable List> customizer) { + @Nullable List> customizer, + @Nullable List plugins) { this.namespaceProperties = namespaceProperties; this.workerInterceptors = workerInterceptors; this.tracer = tracer; this.customizer = customizer; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public WorkerFactoryOptionsTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable List> customizer) { + this(namespaceProperties, workerInterceptors, tracer, customizer, null); } public WorkerFactoryOptions createWorkerFactoryOptions() { @@ -56,6 +72,10 @@ public WorkerFactoryOptions createWorkerFactoryOptions() { } options.setWorkerInterceptors(interceptors.toArray(new WorkerInterceptor[0])); + if (plugins != null && !plugins.isEmpty()) { + options.setPlugins(plugins.toArray(new WorkerPlugin[0])); + } + if (customizer != null) { for (TemporalOptionsCustomizer customizer : customizer) { options = customizer.customize(options); 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 870ede282..23d28185c 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 @@ -62,6 +62,7 @@ public class WorkersTemplate implements BeanFactoryAware, EnvironmentAware { private final @Nullable List> workerCustomizers; private final @Nullable List> workflowImplementationCustomizers; + private final @Nullable List plugins; private ConfigurableListableBeanFactory beanFactory; private Environment environment; @@ -81,7 +82,8 @@ public WorkersTemplate( @Nullable List> workerCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List plugins) { this.namespaceProperties = namespaceProperties; this.workerInterceptors = workerInterceptors; this.tracer = tracer; @@ -91,6 +93,7 @@ public WorkersTemplate( this.workerFactoryCustomizers = workerFactoryCustomizers; this.workerCustomizers = workerCustomizers; this.workflowImplementationCustomizers = workflowImplementationCustomizers; + this.plugins = plugins; } public NamespaceProperties getNamespaceProperties() { @@ -126,7 +129,11 @@ WorkerFactory createWorkerFactory(WorkflowClient workflowClient) { } else { WorkerFactoryOptions workerFactoryOptions = new WorkerFactoryOptionsTemplate( - namespaceProperties, workerInterceptors, tracer, workerFactoryCustomizers) + namespaceProperties, + workerInterceptors, + tracer, + workerFactoryCustomizers, + plugins) .createWorkerFactoryOptions(); return WorkerFactory.newInstance(workflowClient, workerFactoryOptions); } From 7c00bfa13ccb1632e98d8ae4fb0fabeac1525248 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Tue, 27 Jan 2026 14:35:43 -0500 Subject: [PATCH 2/5] attempt to properly register things to account for plugin propagaion --- .../NonRootBeanPostProcessor.java | 50 +++++++++++++++++-- .../RootNamespaceAutoConfiguration.java | 46 ++++++++++++++++- .../ServiceStubsAutoConfiguration.java | 7 ++- .../TestServerAutoConfiguration.java | 48 ++++++++++++++++-- .../template/ClientTemplate.java | 11 ++-- .../template/NamespaceTemplate.java | 20 ++++++-- .../template/NonRootNamespaceTemplate.java | 10 +++- .../template/ServiceStubOptionsTemplate.java | 23 ++++++++- .../template/ServiceStubsTemplate.java | 11 +++- .../WorkflowClientOptionsTemplate.java | 44 +++++++++++++++- 10 files changed, 243 insertions(+), 27 deletions(-) diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java index cd023171b..8a5597182 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java @@ -5,11 +5,14 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; @@ -25,6 +28,7 @@ import io.temporal.worker.WorkerOptions; import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -51,7 +55,10 @@ public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryA private @Nullable Tracer tracer; private @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment; private @Nullable Scope metricsScope; - private @Nullable List plugins; + private @Nullable List serviceStubsPlugins; + private @Nullable List workflowClientPlugins; + private @Nullable List scheduleClientPlugins; + private @Nullable List workerPlugins; public NonRootBeanPostProcessor(@Nonnull TemporalProperties temporalProperties) { this.temporalProperties = temporalProperties; @@ -80,7 +87,19 @@ public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull Stri findBean( "temporalTestWorkflowEnvironmentAdapter", TestWorkflowEnvironmentAdapter.class); } - plugins = findAllBeans(WorkerPlugin.class); + // Collect all plugin types + serviceStubsPlugins = findAllBeans(WorkflowServiceStubsPlugin.class); + // Filter plugins so each is only registered at its highest applicable level + workflowClientPlugins = + filterPlugins( + findAllBeans(WorkflowClientPlugin.class), WorkflowServiceStubsPlugin.class); + scheduleClientPlugins = + filterPlugins( + findAllBeans(ScheduleClientPlugin.class), WorkflowServiceStubsPlugin.class); + workerPlugins = + filterPlugins( + filterPlugins(findAllBeans(WorkerPlugin.class), WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); namespaceProperties.forEach(this::injectBeanByNonRootNamespace); } } @@ -127,7 +146,8 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { connectionProperties, metricsScope, testWorkflowEnvironment, - workflowServiceStubsCustomizers); + workflowServiceStubsCustomizers, + serviceStubsPlugins); WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs(); NonRootNamespaceTemplate namespaceTemplate = @@ -146,7 +166,9 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { workflowClientCustomizers, scheduleClientCustomizers, workflowImplementationCustomizers, - plugins); + workflowClientPlugins, + scheduleClientPlugins, + workerPlugins); ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate(); WorkflowClient workflowClient = clientTemplate.getWorkflowClient(); @@ -206,13 +228,31 @@ private T findBeanByNamespace(String beanPrefix, Class clazz) { private @Nullable List findAllBeans(Class clazz) { try { - return new java.util.ArrayList<>(beanFactory.getBeansOfType(clazz).values()); + return new ArrayList<>(beanFactory.getBeansOfType(clazz).values()); } catch (BeansException ignore) { // Ignore if no beans are found } return null; } + /** + * Filter out plugins that implement a higher-level plugin interface, as those are handled at that + * higher level via propagation. + */ + private static @Nullable List filterPlugins( + @Nullable List plugins, Class excludeType) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List filtered = new ArrayList<>(); + for (T plugin : plugins) { + if (!excludeType.isInstance(plugin)) { + filtered.add(plugin); + } + } + return filtered.isEmpty() ? null : filtered; + } + private List> findBeanByNameSpaceForTemporalCustomizer( String beanPrefix, Class genericOptionsBuilderClass) { String beanName = diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java index 9dfcac385..9969f1f1c 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java @@ -3,13 +3,16 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; @@ -22,6 +25,7 @@ import io.temporal.worker.WorkerOptions; import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -89,7 +93,9 @@ public NamespaceTemplate rootNamespaceTemplate( @Autowired(required = false) @Nullable Map> workflowImplementationCustomizerMap, - @Autowired(required = false) @Nullable List plugins) { + @Autowired(required = false) @Nullable List workflowClientPlugins, + @Autowired(required = false) @Nullable List scheduleClientPlugins, + @Autowired(required = false) @Nullable List workerPlugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -123,6 +129,22 @@ public NamespaceTemplate rootNamespaceTemplate( WorkflowImplementationOptions.Builder.class, properties); + // Filter plugins so each is only registered at its highest applicable level. + // WorkflowServiceStubsPlugin is handled at ServiceStubsAutoConfiguration level and propagates + // down. + // WorkflowClientPlugin (not WorkflowServiceStubsPlugin) is handled here and propagates to + // workers. + // ScheduleClientPlugin (not WorkflowServiceStubsPlugin) is handled here. + // WorkerPlugin (not WorkflowServiceStubsPlugin, not WorkflowClientPlugin) is handled here. + List filteredClientPlugins = + filterPlugins(workflowClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredSchedulePlugins = + filterPlugins(scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredWorkerPlugins = + filterPlugins( + filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); + return new NamespaceTemplate( properties, workflowServiceStubs, @@ -137,7 +159,27 @@ public NamespaceTemplate rootNamespaceTemplate( clientCustomizer, scheduleCustomizer, workflowImplementationCustomizer, - plugins); + filteredClientPlugins, + filteredSchedulePlugins, + filteredWorkerPlugins); + } + + /** + * Filter out plugins that implement a higher-level plugin interface, as those are handled at that + * higher level via propagation. + */ + private static @Nullable List filterPlugins( + @Nullable List plugins, Class excludeType) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List filtered = new ArrayList<>(); + for (T plugin : plugins) { + if (!excludeType.isInstance(plugin)) { + filtered.add(plugin); + } + } + return filtered.isEmpty() ? null : filtered; } /** Client */ diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java index 761883df0..aae781b44 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java @@ -4,6 +4,7 @@ import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions.Builder; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate; @@ -42,7 +43,8 @@ public ServiceStubsTemplate serviceStubsTemplate( TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Autowired(required = false) @Nullable Map> - workflowServiceStubsCustomizerMap) { + workflowServiceStubsCustomizerMap, + @Autowired(required = false) @Nullable List plugins) { List> workflowServiceStubsCustomizer = AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, workflowServiceStubsCustomizerMap, Builder.class, properties); @@ -50,7 +52,8 @@ public ServiceStubsTemplate serviceStubsTemplate( properties.getConnection(), metricsScope, testWorkflowEnvironment, - workflowServiceStubsCustomizer); + workflowServiceStubsCustomizer, + plugins); } @Bean(name = "temporalWorkflowServiceStubs") diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java index 27f849d3d..d90cbc35f 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java @@ -3,11 +3,14 @@ import com.uber.m3.tally.Scope; import io.opentracing.Tracer; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter; @@ -17,6 +20,7 @@ import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerPlugin; +import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -81,7 +85,9 @@ public TestWorkflowEnvironment testWorkflowEnvironment( @Autowired(required = false) @Nullable Map> scheduleCustomizerMap, - @Autowired(required = false) @Nullable List plugins) { + @Autowired(required = false) @Nullable List workflowClientPlugins, + @Autowired(required = false) @Nullable List scheduleClientPlugins, + @Autowired(required = false) @Nullable List workerPlugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -106,6 +112,18 @@ public TestWorkflowEnvironment testWorkflowEnvironment( AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties); + // Filter plugins so each is only registered at its highest applicable level. + // Note: TestWorkflowEnvironment doesn't support WorkflowServiceStubsPlugin directly since it + // creates its own test server. We filter those out and handle the rest. + List filteredClientPlugins = + filterPlugins(workflowClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredSchedulePlugins = + filterPlugins(scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredWorkerPlugins = + filterPlugins( + filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); + TestEnvironmentOptions.Builder options = TestEnvironmentOptions.newBuilder() .setWorkflowClientOptions( @@ -116,7 +134,9 @@ public TestWorkflowEnvironment testWorkflowEnvironment( chosenScheduleClientInterceptors, otTracer, clientCustomizer, - scheduleCustomizer) + scheduleCustomizer, + filteredClientPlugins, + filteredSchedulePlugins) .createWorkflowClientOptions()); if (metricsScope != null) { @@ -125,7 +145,11 @@ public TestWorkflowEnvironment testWorkflowEnvironment( options.setWorkerFactoryOptions( new WorkerFactoryOptionsTemplate( - properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer, plugins) + properties, + chosenWorkerInterceptors, + otTracer, + workerFactoryCustomizer, + filteredWorkerPlugins) .createWorkerFactoryOptions()); if (testEnvOptionsCustomizers != null) { @@ -137,4 +161,22 @@ public TestWorkflowEnvironment testWorkflowEnvironment( return TestWorkflowEnvironment.newInstance(options.build()); } + + /** + * Filter out plugins that implement a higher-level plugin interface, as those are handled at that + * higher level via propagation. + */ + private static @Nullable List filterPlugins( + @Nullable List plugins, Class excludeType) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List filtered = new ArrayList<>(); + for (T plugin : plugins) { + if (!excludeType.isInstance(plugin)) { + filtered.add(plugin); + } + } + return filtered.isEmpty() ? null : filtered; + } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java index f117fbc33..600d2e4c9 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java @@ -4,8 +4,10 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -33,8 +35,9 @@ public ClientTemplate( @Nullable WorkflowServiceStubs workflowServiceStubs, @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Nullable List> clientCustomizers, - @Nullable - List> scheduleCustomizers) { + @Nullable List> scheduleCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins) { this.optionsTemplate = new WorkflowClientOptionsTemplate( namespace, @@ -43,7 +46,9 @@ public ClientTemplate( scheduleClientInterceptors, tracer, clientCustomizers, - scheduleCustomizers); + scheduleCustomizers, + workflowClientPlugins, + scheduleClientPlugins); this.workflowServiceStubs = workflowServiceStubs; this.testWorkflowEnvironment = testWorkflowEnvironment; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java index 91709be8d..f8966d6ac 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; @@ -37,7 +39,9 @@ public class NamespaceTemplate { scheduleCustomizers; private final @Nullable List> workflowImplementationCustomizers; - private final @Nullable List plugins; + private final @Nullable List workflowClientPlugins; + private final @Nullable List scheduleClientPlugins; + private final @Nullable List workerPlugins; private ClientTemplate clientTemplate; private WorkersTemplate workersTemplate; @@ -59,7 +63,9 @@ public NamespaceTemplate( @Nullable List> workflowImplementationCustomizers, - @Nullable List plugins) { + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins, + @Nullable List workerPlugins) { this.namespaceProperties = namespaceProperties; this.workflowServiceStubs = workflowServiceStubs; this.dataConverter = dataConverter; @@ -74,7 +80,9 @@ public NamespaceTemplate( this.clientCustomizers = clientCustomizers; this.scheduleCustomizers = scheduleCustomizers; this.workflowImplementationCustomizers = workflowImplementationCustomizers; - this.plugins = plugins; + this.workflowClientPlugins = workflowClientPlugins; + this.scheduleClientPlugins = scheduleClientPlugins; + this.workerPlugins = workerPlugins; } public ClientTemplate getClientTemplate() { @@ -89,7 +97,9 @@ public ClientTemplate getClientTemplate() { workflowServiceStubs, testWorkflowEnvironment, clientCustomizers, - scheduleCustomizers); + scheduleCustomizers, + workflowClientPlugins, + scheduleClientPlugins); } return clientTemplate; } @@ -106,7 +116,7 @@ public WorkersTemplate getWorkersTemplate() { workerFactoryCustomizers, workerCustomizers, workflowImplementationCustomizers, - plugins); + workerPlugins); } return this.workersTemplate; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java index 6d7c0f2c1..f0b1da371 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; @@ -42,7 +44,9 @@ public NonRootNamespaceTemplate( @Nullable List> workflowImplementationCustomizers, - @Nullable List plugins) { + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins, + @Nullable List workerPlugins) { super( namespaceProperties, workflowServiceStubs, @@ -57,7 +61,9 @@ public NonRootNamespaceTemplate( clientCustomizers, scheduleCustomizers, workflowImplementationCustomizers, - plugins); + workflowClientPlugins, + scheduleClientPlugins, + workerPlugins); this.beanFactory = beanFactory; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java index 684faac63..dbcd8ebb2 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java @@ -5,6 +5,7 @@ import io.temporal.internal.common.ShadingHelpers; import io.temporal.serviceclient.SimpleSslContextBuilder; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import java.io.ByteArrayInputStream; @@ -24,16 +25,32 @@ public class ServiceStubOptionsTemplate { private final @Nullable Scope metricsScope; private final @Nullable List> workflowServiceStubsCustomizer; + private final @Nullable List plugins; public ServiceStubOptionsTemplate( @Nonnull ConnectionProperties connectionProperties, @Nullable Scope metricsScope, @Nullable List> - workflowServiceStubsCustomizer) { + workflowServiceStubsCustomizer, + @Nullable List plugins) { this.connectionProperties = connectionProperties; this.metricsScope = metricsScope; this.workflowServiceStubsCustomizer = workflowServiceStubsCustomizer; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public ServiceStubOptionsTemplate( + @Nonnull ConnectionProperties connectionProperties, + @Nullable Scope metricsScope, + @Nullable + List> + workflowServiceStubsCustomizer) { + this(connectionProperties, metricsScope, workflowServiceStubsCustomizer, null); } public WorkflowServiceStubsOptions createServiceStubOptions() { @@ -59,6 +76,10 @@ public WorkflowServiceStubsOptions createServiceStubOptions() { stubsOptionsBuilder.setMetricsScope(metricsScope); } + if (plugins != null && !plugins.isEmpty()) { + stubsOptionsBuilder.setPlugins(plugins.toArray(new WorkflowServiceStubsPlugin[0])); + } + if (workflowServiceStubsCustomizer != null) { for (TemporalOptionsCustomizer workflowServiceStubsCustomizer : workflowServiceStubsCustomizer) { diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java index 7d6597143..c46b29688 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java @@ -3,6 +3,7 @@ import com.uber.m3.tally.Scope; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import java.util.List; @@ -18,6 +19,7 @@ public class ServiceStubsTemplate { private final @Nullable List> workflowServiceStubsCustomizers; + private final @Nullable List plugins; private WorkflowServiceStubs workflowServiceStubs; @@ -27,11 +29,13 @@ public ServiceStubsTemplate( @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Nullable List> - workflowServiceStubsCustomizers) { + workflowServiceStubsCustomizers, + @Nullable List plugins) { this.connectionProperties = connectionProperties; this.metricsScope = metricsScope; this.testWorkflowEnvironment = testWorkflowEnvironment; this.workflowServiceStubsCustomizers = workflowServiceStubsCustomizers; + this.plugins = plugins; } public WorkflowServiceStubs getWorkflowServiceStubs() { @@ -54,7 +58,10 @@ private WorkflowServiceStubs createServiceStubs() { workflowServiceStubs = WorkflowServiceStubs.newServiceStubs( new ServiceStubOptionsTemplate( - connectionProperties, metricsScope, workflowServiceStubsCustomizers) + connectionProperties, + metricsScope, + workflowServiceStubsCustomizers, + plugins) .createServiceStubOptions()); } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java index 02ef555d9..ca4e56457 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -25,6 +27,8 @@ public class WorkflowClientOptionsTemplate { clientCustomizers; private final @Nullable List> scheduleCustomizers; + private final @Nullable List workflowClientPlugins; + private final @Nullable List scheduleClientPlugins; public WorkflowClientOptionsTemplate( @Nonnull String namespace, @@ -33,8 +37,9 @@ public WorkflowClientOptionsTemplate( @Nullable List scheduleClientInterceptors, @Nullable Tracer tracer, @Nullable List> clientCustomizers, - @Nullable - List> scheduleCustomizers) { + @Nullable List> scheduleCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.workflowClientInterceptors = workflowClientInterceptors; @@ -42,6 +47,33 @@ public WorkflowClientOptionsTemplate( this.tracer = tracer; this.clientCustomizers = clientCustomizers; this.scheduleCustomizers = scheduleCustomizers; + this.workflowClientPlugins = workflowClientPlugins; + this.scheduleClientPlugins = scheduleClientPlugins; + } + + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public WorkflowClientOptionsTemplate( + @Nonnull String namespace, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable Tracer tracer, + @Nullable List> clientCustomizers, + @Nullable + List> scheduleCustomizers) { + this( + namespace, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + tracer, + clientCustomizers, + scheduleCustomizers, + null, + null); } public WorkflowClientOptions createWorkflowClientOptions() { @@ -62,6 +94,10 @@ public WorkflowClientOptions createWorkflowClientOptions() { options.setInterceptors(interceptors.toArray(new WorkflowClientInterceptor[0])); + if (workflowClientPlugins != null && !workflowClientPlugins.isEmpty()) { + options.setPlugins(workflowClientPlugins.toArray(new WorkflowClientPlugin[0])); + } + if (clientCustomizers != null) { for (TemporalOptionsCustomizer customizer : clientCustomizers) { @@ -79,6 +115,10 @@ public ScheduleClientOptions createScheduleClientOptions() { options.setInterceptors(scheduleClientInterceptors); } + if (scheduleClientPlugins != null && !scheduleClientPlugins.isEmpty()) { + options.setPlugins(scheduleClientPlugins.toArray(new ScheduleClientPlugin[0])); + } + if (scheduleCustomizers != null) { for (TemporalOptionsCustomizer customizer : scheduleCustomizers) { From a373289311da0c4eeb89146332a10070c564aa61 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Tue, 27 Jan 2026 15:22:26 -0500 Subject: [PATCH 3/5] Maintain backwards compat --- .../template/ClientTemplate.java | 29 +++++++++++++ .../template/NamespaceTemplate.java | 40 ++++++++++++++++++ .../template/NonRootNamespaceTemplate.java | 42 +++++++++++++++++++ .../template/ServiceStubsTemplate.java | 19 +++++++++ .../template/WorkersTemplate.java | 28 +++++++++++++ 5 files changed, 158 insertions(+) diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java index 600d2e4c9..c9f68cb62 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java @@ -53,6 +53,35 @@ public ClientTemplate( this.testWorkflowEnvironment = testWorkflowEnvironment; } + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public ClientTemplate( + @Nonnull String namespace, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable Tracer tracer, + @Nullable WorkflowServiceStubs workflowServiceStubs, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable List> clientCustomizers, + @Nullable + List> scheduleCustomizers) { + this( + namespace, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + tracer, + workflowServiceStubs, + testWorkflowEnvironment, + clientCustomizers, + scheduleCustomizers, + null, + null); + } + public WorkflowClient getWorkflowClient() { if (workflowClient == null) { this.workflowClient = createWorkflowClient(); diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java index f8966d6ac..d80c69955 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java @@ -85,6 +85,46 @@ public NamespaceTemplate( this.workerPlugins = workerPlugins; } + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public NamespaceTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nonnull WorkflowServiceStubs workflowServiceStubs, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable List> clientCustomizers, + @Nullable List> scheduleCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + namespaceProperties, + workflowServiceStubs, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + clientCustomizers, + scheduleCustomizers, + workflowImplementationCustomizers, + null, + null, + null); + } + public ClientTemplate getClientTemplate() { if (clientTemplate == null) { this.clientTemplate = diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java index f0b1da371..7f62bf97b 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java @@ -67,6 +67,48 @@ public NonRootNamespaceTemplate( this.beanFactory = beanFactory; } + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public NonRootNamespaceTemplate( + @Nonnull BeanFactory beanFactory, + @Nonnull NonRootNamespaceProperties namespaceProperties, + @Nonnull WorkflowServiceStubs workflowServiceStubs, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable List> clientCustomizers, + @Nullable List> scheduleCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + beanFactory, + namespaceProperties, + workflowServiceStubs, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + clientCustomizers, + scheduleCustomizers, + workflowImplementationCustomizers, + null, + null, + null); + } + @Override public WorkersTemplate getWorkersTemplate() { WorkersTemplate workersTemplate = super.getWorkersTemplate(); diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java index c46b29688..1db7c4b34 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java @@ -38,6 +38,25 @@ public ServiceStubsTemplate( this.plugins = plugins; } + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public ServiceStubsTemplate( + @Nonnull ConnectionProperties connectionProperties, + @Nullable Scope metricsScope, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> + workflowServiceStubsCustomizers) { + this( + connectionProperties, + metricsScope, + testWorkflowEnvironment, + workflowServiceStubsCustomizers, + null); + } + public WorkflowServiceStubs getWorkflowServiceStubs() { if (workflowServiceStubs == null) { this.workflowServiceStubs = createServiceStubs(); 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 23d28185c..0192fd4bf 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 @@ -96,6 +96,34 @@ public WorkersTemplate( this.plugins = plugins; } + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public WorkersTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nullable ClientTemplate clientTemplate, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + namespaceProperties, + clientTemplate, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + workflowImplementationCustomizers, + null); + } + public NamespaceProperties getNamespaceProperties() { return namespaceProperties; } From 9806864ee37f1bf181de688f1e29e76dabe7edc0 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Tue, 27 Jan 2026 15:26:59 -0500 Subject: [PATCH 4/5] Extract out fitlerPlugins --- .../autoconfigure/AutoConfigurationUtils.java | 17 +++++++++++ .../NonRootBeanPostProcessor.java | 27 ++++------------- .../RootNamespaceAutoConfiguration.java | 29 ++++--------------- .../TestServerAutoConfiguration.java | 29 ++++--------------- 4 files changed, 34 insertions(+), 68 deletions(-) diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java index 03bcbe8f1..eb10aea43 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java @@ -168,4 +168,21 @@ static String temporalCustomizerBeanName(String beanPrefix, Class optionsBuil bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1); return beanPrefix + bindingCustomizerName; } + + /** + * Filter out plugins that implement a higher-level plugin interface, as those are handled at that + * higher level via propagation. + */ + static @Nullable List filterPlugins(@Nullable List plugins, Class excludeType) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List filtered = new ArrayList<>(); + for (T plugin : plugins) { + if (!excludeType.isInstance(plugin)) { + filtered.add(plugin); + } + } + return filtered.isEmpty() ? null : filtered; + } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java index 8a5597182..09a457619 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java @@ -91,14 +91,15 @@ public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull Stri serviceStubsPlugins = findAllBeans(WorkflowServiceStubsPlugin.class); // Filter plugins so each is only registered at its highest applicable level workflowClientPlugins = - filterPlugins( + AutoConfigurationUtils.filterPlugins( findAllBeans(WorkflowClientPlugin.class), WorkflowServiceStubsPlugin.class); scheduleClientPlugins = - filterPlugins( + AutoConfigurationUtils.filterPlugins( findAllBeans(ScheduleClientPlugin.class), WorkflowServiceStubsPlugin.class); workerPlugins = - filterPlugins( - filterPlugins(findAllBeans(WorkerPlugin.class), WorkflowServiceStubsPlugin.class), + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins( + findAllBeans(WorkerPlugin.class), WorkflowServiceStubsPlugin.class), WorkflowClientPlugin.class); namespaceProperties.forEach(this::injectBeanByNonRootNamespace); } @@ -235,24 +236,6 @@ private T findBeanByNamespace(String beanPrefix, Class clazz) { return null; } - /** - * Filter out plugins that implement a higher-level plugin interface, as those are handled at that - * higher level via propagation. - */ - private static @Nullable List filterPlugins( - @Nullable List plugins, Class excludeType) { - if (plugins == null || plugins.isEmpty()) { - return plugins; - } - List filtered = new ArrayList<>(); - for (T plugin : plugins) { - if (!excludeType.isInstance(plugin)) { - filtered.add(plugin); - } - } - return filtered.isEmpty() ? null : filtered; - } - private List> findBeanByNameSpaceForTemporalCustomizer( String beanPrefix, Class genericOptionsBuilderClass) { String beanName = diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java index 9969f1f1c..4299119d1 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java @@ -25,7 +25,6 @@ import io.temporal.worker.WorkerOptions; import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -137,12 +136,14 @@ public NamespaceTemplate rootNamespaceTemplate( // ScheduleClientPlugin (not WorkflowServiceStubsPlugin) is handled here. // WorkerPlugin (not WorkflowServiceStubsPlugin, not WorkflowClientPlugin) is handled here. List filteredClientPlugins = - filterPlugins(workflowClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins( + workflowClientPlugins, WorkflowServiceStubsPlugin.class); List filteredSchedulePlugins = - filterPlugins(scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins( + scheduleClientPlugins, WorkflowServiceStubsPlugin.class); List filteredWorkerPlugins = - filterPlugins( - filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), WorkflowClientPlugin.class); return new NamespaceTemplate( @@ -164,24 +165,6 @@ public NamespaceTemplate rootNamespaceTemplate( filteredWorkerPlugins); } - /** - * Filter out plugins that implement a higher-level plugin interface, as those are handled at that - * higher level via propagation. - */ - private static @Nullable List filterPlugins( - @Nullable List plugins, Class excludeType) { - if (plugins == null || plugins.isEmpty()) { - return plugins; - } - List filtered = new ArrayList<>(); - for (T plugin : plugins) { - if (!excludeType.isInstance(plugin)) { - filtered.add(plugin); - } - } - return filtered.isEmpty() ? null : filtered; - } - /** Client */ @Primary @Bean(name = "temporalClientTemplate") diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java index d90cbc35f..4ca69e082 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java @@ -20,7 +20,6 @@ import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerPlugin; -import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -116,12 +115,14 @@ public TestWorkflowEnvironment testWorkflowEnvironment( // Note: TestWorkflowEnvironment doesn't support WorkflowServiceStubsPlugin directly since it // creates its own test server. We filter those out and handle the rest. List filteredClientPlugins = - filterPlugins(workflowClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins( + workflowClientPlugins, WorkflowServiceStubsPlugin.class); List filteredSchedulePlugins = - filterPlugins(scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins( + scheduleClientPlugins, WorkflowServiceStubsPlugin.class); List filteredWorkerPlugins = - filterPlugins( - filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), WorkflowClientPlugin.class); TestEnvironmentOptions.Builder options = @@ -161,22 +162,4 @@ public TestWorkflowEnvironment testWorkflowEnvironment( return TestWorkflowEnvironment.newInstance(options.build()); } - - /** - * Filter out plugins that implement a higher-level plugin interface, as those are handled at that - * higher level via propagation. - */ - private static @Nullable List filterPlugins( - @Nullable List plugins, Class excludeType) { - if (plugins == null || plugins.isEmpty()) { - return plugins; - } - List filtered = new ArrayList<>(); - for (T plugin : plugins) { - if (!excludeType.isInstance(plugin)) { - filtered.add(plugin); - } - } - return filtered.isEmpty() ? null : filtered; - } } From c4be222beafdcd823eaa785a3438336f9e3f0f02 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Tue, 27 Jan 2026 16:11:41 -0500 Subject: [PATCH 5/5] Review pass over this --- .../autoconfigure/AutoConfigurationUtils.java | 30 +- .../NonRootBeanPostProcessor.java | 7 +- .../RootNamespaceAutoConfiguration.java | 15 +- .../ServiceStubsAutoConfiguration.java | 4 +- .../TestServerAutoConfiguration.java | 27 +- .../autoconfigure/PluginAutoWiringTest.java | 415 ++++++++++++++++++ 6 files changed, 482 insertions(+), 16 deletions(-) create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java index eb10aea43..37a7b5ce7 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java @@ -172,10 +172,21 @@ static String temporalCustomizerBeanName(String beanPrefix, Class optionsBuil /** * Filter out plugins that implement a higher-level plugin interface, as those are handled at that * higher level via propagation. + * + *

The plugin hierarchy is: WorkflowServiceStubsPlugin -> WorkflowClientPlugin -> WorkerPlugin. + * A plugin implementing a higher-level interface will be registered at that level and propagate + * down automatically, so it should not also be registered at lower levels. + * + * @param plugins the list of plugins to filter (may be null) + * @param excludeType the plugin interface to exclude + * @return the filtered list, or null if input was null or all plugins were filtered out */ static @Nullable List filterPlugins(@Nullable List plugins, Class excludeType) { - if (plugins == null || plugins.isEmpty()) { - return plugins; + if (plugins == null) { + return null; + } + if (plugins.isEmpty()) { + return null; } List filtered = new ArrayList<>(); for (T plugin : plugins) { @@ -185,4 +196,19 @@ static String temporalCustomizerBeanName(String beanPrefix, Class optionsBuil } return filtered.isEmpty() ? null : filtered; } + + /** + * Sort plugins by @Order and @Priority annotations for consistent ordering. + * + * @param plugins the list of plugins to sort (may be null) + * @return the sorted list, or null if input was null + */ + static @Nullable List sortPlugins(@Nullable List plugins) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List sorted = new ArrayList<>(plugins); + AnnotationAwareOrderComparator.sort(sorted); + return sorted; + } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java index 09a457619..674ec7b15 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java @@ -229,9 +229,10 @@ private T findBeanByNamespace(String beanPrefix, Class clazz) { private @Nullable List findAllBeans(Class clazz) { try { - return new ArrayList<>(beanFactory.getBeansOfType(clazz).values()); - } catch (BeansException ignore) { - // Ignore if no beans are found + List beans = new ArrayList<>(beanFactory.getBeansOfType(clazz).values()); + return AutoConfigurationUtils.sortPlugins(beans); + } catch (NoSuchBeanDefinitionException ignore) { + // No beans of this type defined - this is expected for optional plugins } return null; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java index 4299119d1..c5cd2a492 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java @@ -128,6 +128,13 @@ public NamespaceTemplate rootNamespaceTemplate( WorkflowImplementationOptions.Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedClientPlugins = + AutoConfigurationUtils.sortPlugins(workflowClientPlugins); + List sortedSchedulePlugins = + AutoConfigurationUtils.sortPlugins(scheduleClientPlugins); + List sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins); + // Filter plugins so each is only registered at its highest applicable level. // WorkflowServiceStubsPlugin is handled at ServiceStubsAutoConfiguration level and propagates // down. @@ -136,14 +143,14 @@ public NamespaceTemplate rootNamespaceTemplate( // ScheduleClientPlugin (not WorkflowServiceStubsPlugin) is handled here. // WorkerPlugin (not WorkflowServiceStubsPlugin, not WorkflowClientPlugin) is handled here. List filteredClientPlugins = - AutoConfigurationUtils.filterPlugins( - workflowClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class); List filteredSchedulePlugins = AutoConfigurationUtils.filterPlugins( - scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + sortedSchedulePlugins, WorkflowServiceStubsPlugin.class); List filteredWorkerPlugins = AutoConfigurationUtils.filterPlugins( - AutoConfigurationUtils.filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + AutoConfigurationUtils.filterPlugins( + sortedWorkerPlugins, WorkflowServiceStubsPlugin.class), WorkflowClientPlugin.class); return new NamespaceTemplate( diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java index aae781b44..022cd7f9c 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java @@ -48,12 +48,14 @@ public ServiceStubsTemplate serviceStubsTemplate( List> workflowServiceStubsCustomizer = AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, workflowServiceStubsCustomizerMap, Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedPlugins = AutoConfigurationUtils.sortPlugins(plugins); return new ServiceStubsTemplate( properties.getConnection(), metricsScope, testWorkflowEnvironment, workflowServiceStubsCustomizer, - plugins); + sortedPlugins); } @Bean(name = "temporalWorkflowServiceStubs") diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java index 4ca69e082..ab21591a7 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java @@ -111,18 +111,33 @@ public TestWorkflowEnvironment testWorkflowEnvironment( AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedClientPlugins = + AutoConfigurationUtils.sortPlugins(workflowClientPlugins); + List sortedSchedulePlugins = + AutoConfigurationUtils.sortPlugins(scheduleClientPlugins); + List sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins); + // Filter plugins so each is only registered at its highest applicable level. - // Note: TestWorkflowEnvironment doesn't support WorkflowServiceStubsPlugin directly since it - // creates its own test server. We filter those out and handle the rest. + // Note: TestWorkflowEnvironment creates its own internal test server and does not accept + // WorkflowServiceStubsPlugin directly. Any beans implementing WorkflowServiceStubsPlugin + // will be ignored in test mode - only their lower-level plugin functionality (if any) is used. + if (sortedClientPlugins != null + && sortedClientPlugins.stream().anyMatch(WorkflowServiceStubsPlugin.class::isInstance)) { + log.warn( + "WorkflowServiceStubsPlugin beans are present but will be ignored in test mode. " + + "TestWorkflowEnvironment creates its own test server and does not support " + + "WorkflowServiceStubsPlugin. Only WorkflowClientPlugin functionality will be used."); + } List filteredClientPlugins = - AutoConfigurationUtils.filterPlugins( - workflowClientPlugins, WorkflowServiceStubsPlugin.class); + AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class); List filteredSchedulePlugins = AutoConfigurationUtils.filterPlugins( - scheduleClientPlugins, WorkflowServiceStubsPlugin.class); + sortedSchedulePlugins, WorkflowServiceStubsPlugin.class); List filteredWorkerPlugins = AutoConfigurationUtils.filterPlugins( - AutoConfigurationUtils.filterPlugins(workerPlugins, WorkflowServiceStubsPlugin.class), + AutoConfigurationUtils.filterPlugins( + sortedWorkerPlugins, WorkflowServiceStubsPlugin.class), WorkflowClientPlugin.class); TestEnvironmentOptions.Builder options = diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java new file mode 100644 index 000000000..5b9dc6515 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java @@ -0,0 +1,415 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.junit.jupiter.api.Assertions.*; + +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; +import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; +import io.temporal.worker.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +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.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = PluginAutoWiringTest.Configuration.class) +@ActiveProfiles(profiles = "auto-discovery-by-task-queue") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class PluginAutoWiringTest { + @Autowired ConfigurableApplicationContext applicationContext; + + @Autowired WorkflowClient workflowClient; + + @Autowired WorkerFactory temporalWorkerFactory; + + @BeforeEach + void setUp() { + applicationContext.start(); + } + + @Test + @Timeout(value = 10) + public void testPluginsAreAutoWired() { + // Verify that plugin beans are present in the application context + assertNotNull(applicationContext.getBean("clientPlugin")); + assertNotNull(applicationContext.getBean("workerPlugin")); + assertNotNull(applicationContext.getBean("comboPlugin")); + } + + @Test + @Timeout(value = 10) + public void testClientPluginGetsCalled() { + // Verify client plugin got its configure method called by checking the recorded invocations + assertTrue( + Configuration.clientPluginInvocations.contains("configureWorkflowClient"), + "Client plugin configureWorkflowClient should have been called"); + } + + @Test + @Timeout(value = 10) + public void testWorkerPluginGetsCalled() { + // Verify worker plugin got its configure method called + assertTrue( + Configuration.workerPluginInvocations.contains("configureWorkerFactory"), + "Worker plugin configureWorkerFactory should have been called"); + } + + @Test + @Timeout(value = 10) + public void testPluginFiltering() { + // The combo plugin implements WorkflowServiceStubsPlugin, WorkflowClientPlugin, + // ScheduleClientPlugin, + // and WorkerPlugin. Because it implements WorkflowServiceStubsPlugin (the highest level), it + // gets + // filtered out from all lower levels (client, schedule, worker) to avoid double-registration. + // + // In normal (non-test) mode, the plugin would be registered at the stubs level and the SDK's + // plugin propagation system would call its lower-level methods. + // + // In test mode, WorkflowServiceStubsPlugin is not supported (TestWorkflowEnvironment creates + // its + // own server), so the combo plugin's configuration methods are NOT called at all. + // + // This verifies the filtering is working correctly: + // - Combo plugin should NOT have client/worker methods called directly (filtered out) + assertFalse( + Configuration.comboPluginInvocations.contains("configureWorkflowClient"), + "Combo plugin should be filtered out at client level (implements WorkflowServiceStubsPlugin)"); + assertFalse( + Configuration.comboPluginInvocations.contains("configureWorkerFactory"), + "Combo plugin should be filtered out at worker level (implements WorkflowServiceStubsPlugin)"); + + // The regular (non-combo) client and worker plugins should still be called + assertTrue( + Configuration.clientPluginInvocations.contains("configureWorkflowClient"), + "Regular client plugin should still be called"); + assertTrue( + Configuration.workerPluginInvocations.contains("configureWorkerFactory"), + "Regular worker plugin should still be called"); + } + + @Test + @Timeout(value = 10) + public void testPluginOrdering() { + // Verify that plugin ordering is applied (via @Order annotation) + // The ordered plugins should have their callbacks in the expected order + List record = Configuration.orderedPluginInvocations; + + int firstIndex = record.indexOf("first"); + int secondIndex = record.indexOf("second"); + int thirdIndex = record.indexOf("third"); + + assertTrue(firstIndex >= 0, "First ordered plugin should have been called"); + assertTrue(secondIndex >= 0, "Second ordered plugin should have been called"); + assertTrue(thirdIndex >= 0, "Third ordered plugin should have been called"); + + assertTrue(firstIndex < secondIndex, "First should be called before second"); + assertTrue(secondIndex < thirdIndex, "Second should be called before third"); + } + + @ComponentScan( + excludeFilters = + @ComponentScan.Filter( + pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*", + type = FilterType.REGEX)) + public static class Configuration { + // Track invocations for verification + static List clientPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List workerPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List comboPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List orderedPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + + // WorkflowClientPlugin only - middle level + @Bean + public TestClientPlugin clientPlugin() { + return new TestClientPlugin(clientPluginInvocations); + } + + // WorkerPlugin only - lowest level + @Bean + public TestWorkerPlugin workerPlugin() { + return new TestWorkerPlugin(workerPluginInvocations); + } + + // Combo plugin implementing all interfaces - tests filtering behavior + @Bean + public TestComboPlugin comboPlugin() { + return new TestComboPlugin(comboPluginInvocations); + } + + // Ordered plugins to test @Order support + @Bean + @Order(1) + public WorkerPlugin firstOrderedPlugin() { + return new OrderedWorkerPlugin("first", orderedPluginInvocations); + } + + @Bean + @Order(2) + public WorkerPlugin secondOrderedPlugin() { + return new OrderedWorkerPlugin("second", orderedPluginInvocations); + } + + @Bean + @Order(3) + public WorkerPlugin thirdOrderedPlugin() { + return new OrderedWorkerPlugin("third", orderedPluginInvocations); + } + } + + public static class TestClientPlugin implements WorkflowClientPlugin { + private final List invocations; + + public TestClientPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.client-plugin"; + } + + @Override + public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) { + invocations.add("configureWorkflowClient"); + } + } + + public static class TestWorkerPlugin implements WorkerPlugin { + private final List invocations; + + public TestWorkerPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.worker-plugin"; + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + invocations.add("configureWorkerFactory"); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } + + // Plugin that implements multiple interfaces - tests filtering behavior + public static class TestComboPlugin + implements WorkflowServiceStubsPlugin, + WorkflowClientPlugin, + ScheduleClientPlugin, + WorkerPlugin { + private final List invocations; + + public TestComboPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.combo-plugin"; + } + + @Override + public void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder) { + invocations.add("configureServiceStubs"); + } + + @Nonnull + @Override + public WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, + @Nonnull Supplier next) { + invocations.add("connectServiceClient"); + return next.get(); + } + + @Override + public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) { + invocations.add("configureWorkflowClient"); + } + + @Override + public void configureScheduleClient(@Nonnull ScheduleClientOptions.Builder builder) { + invocations.add("configureScheduleClient"); + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + invocations.add("configureWorkerFactory"); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } + + // Plugin for testing ordering + public static class OrderedWorkerPlugin implements WorkerPlugin { + private final String name; + private final List orderRecord; + + public OrderedWorkerPlugin(String name, List orderRecord) { + this.name = name; + this.orderRecord = orderRecord; + } + + @Nonnull + @Override + public String getName() { + return "test.ordered." + name; + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + orderRecord.add(name); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } +}