Skip to content

Commit bf0b196

Browse files
Allow multiple customizers in Springboot (#2649)
Allow multiple customizers in Springboot
1 parent eaad70d commit bf0b196

17 files changed

+331
-191
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
* Beans of this class can be added to Spring context to get a fine control over *Options objects
1313
* that are created by Temporal Spring Boot Autoconfigure module.
1414
*
15-
* <p>Only one bean of each generic type can be added to Spring context.
15+
* <p>Multiple beans of each generic type can be added to Spring context. They will be ordered,
16+
* taking into account {@link org.springframework.core.Ordered Ordered} and {@link
17+
* org.springframework.core.annotation.Order @Order} values of the target
1618
*
1719
* @param <T> Temporal Options Builder to customize. Respected types: {@link
1820
* WorkflowServiceStubsOptions.Builder}, {@link WorkflowClientOptions.Builder}, {@link

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

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
import io.temporal.spring.boot.TemporalOptionsCustomizer;
99
import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties;
1010
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
11-
import java.util.ArrayList;
12-
import java.util.List;
13-
import java.util.Map;
11+
import java.util.*;
1412
import java.util.Map.Entry;
15-
import java.util.Objects;
1613
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
1715
import javax.annotation.Nullable;
16+
import org.springframework.beans.factory.ListableBeanFactory;
1817
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
18+
import org.springframework.core.OrderComparator;
19+
import org.springframework.core.Ordered;
20+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
21+
import org.springframework.core.annotation.Order;
1922

2023
class AutoConfigurationUtils {
2124

@@ -94,32 +97,68 @@ static List<WorkerInterceptor> chooseWorkerInterceptors(
9497
return workerInterceptor;
9598
}
9699

97-
static <T> TemporalOptionsCustomizer<T> chooseTemporalCustomizerBean(
100+
/**
101+
* Create a comparator that can extract @Order and @Priority from beans in the given bean factory.
102+
* This is needed because the default OrderComparator doesn't know about the bean factory and
103+
* therefore can't look up annotations on beans.
104+
*/
105+
private static Comparator<Object> beanFactoryAwareOrderComparator(
106+
ListableBeanFactory beanFactory) {
107+
return OrderComparator.INSTANCE.withSourceProvider(
108+
o -> {
109+
if (!(o instanceof Map.Entry)) {
110+
throw new IllegalStateException("Unexpected object type: " + o);
111+
}
112+
Map.Entry<String, TemporalOptionsCustomizer<?>> entry =
113+
(Map.Entry<String, TemporalOptionsCustomizer<?>>) o;
114+
// Check if the bean itself has a Priority annotation
115+
Integer priority = AnnotationAwareOrderComparator.INSTANCE.getPriority(entry.getValue());
116+
if (priority != null) {
117+
return (Ordered) () -> priority;
118+
}
119+
120+
// Check if the bean factory method or the bean has an Order annotations
121+
String beanName = entry.getKey();
122+
if (beanName != null) {
123+
Order order = beanFactory.findAnnotationOnBean(beanName, Order.class);
124+
if (order != null) {
125+
return (Ordered) order::value;
126+
}
127+
}
128+
129+
// Nothing present
130+
return null;
131+
});
132+
}
133+
134+
static <T> List<TemporalOptionsCustomizer<T>> chooseTemporalCustomizerBeans(
135+
ListableBeanFactory beanFactory,
98136
Map<String, TemporalOptionsCustomizer<T>> customizerMap,
99137
Class<T> genericOptionsBuilderClass,
100138
TemporalProperties properties) {
101139
if (Objects.isNull(customizerMap) || customizerMap.isEmpty()) {
102140
return null;
103141
}
104142
List<NonRootNamespaceProperties> nonRootNamespaceProperties = properties.getNamespaces();
105-
if (Objects.isNull(nonRootNamespaceProperties) || nonRootNamespaceProperties.isEmpty()) {
106-
return customizerMap.values().stream().findFirst().orElse(null);
143+
Stream<Entry<String, TemporalOptionsCustomizer<T>>> customizerStream =
144+
customizerMap.entrySet().stream();
145+
if (!(Objects.isNull(nonRootNamespaceProperties) || nonRootNamespaceProperties.isEmpty())) {
146+
// Non-root namespace bean names, such as "nsWorkerFactoryCustomizer", "nsWorkerCustomizer"
147+
List<String> nonRootBeanNames =
148+
nonRootNamespaceProperties.stream()
149+
.map(
150+
ns ->
151+
temporalCustomizerBeanName(
152+
MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace()),
153+
genericOptionsBuilderClass))
154+
.collect(Collectors.toList());
155+
customizerStream =
156+
customizerStream.filter(entry -> !nonRootBeanNames.contains(entry.getKey()));
107157
}
108-
// Non-root namespace bean names, such as "nsWorkerFactoryCustomizer", "nsWorkerCustomizer"
109-
List<String> nonRootBeanNames =
110-
nonRootNamespaceProperties.stream()
111-
.map(
112-
ns ->
113-
temporalCustomizerBeanName(
114-
MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace()),
115-
genericOptionsBuilderClass))
116-
.collect(Collectors.toList());
117-
118-
return customizerMap.entrySet().stream()
119-
.filter(entry -> !nonRootBeanNames.contains(entry.getKey()))
120-
.findFirst()
158+
return customizerStream
159+
.sorted(beanFactoryAwareOrderComparator(beanFactory))
121160
.map(Entry::getValue)
122-
.orElse(null);
161+
.collect(Collectors.toList());
123162
}
124163

125164
static String temporalCustomizerBeanName(String beanPrefix, Class<?> optionsBuilderClass) {

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

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
import io.temporal.worker.WorkerFactoryOptions.Builder;
2525
import io.temporal.worker.WorkerOptions;
2626
import io.temporal.worker.WorkflowImplementationOptions;
27+
import java.util.Collections;
2728
import java.util.List;
28-
import java.util.Optional;
29+
import java.util.stream.Collectors;
2930
import javax.annotation.Nonnull;
3031
import javax.annotation.Nullable;
3132
import org.slf4j.Logger;
@@ -83,19 +84,30 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
8384
DataConverter dataConverterByNamespace = findBeanByNamespace(beanPrefix, DataConverter.class);
8485

8586
// found regarding namespace customizer bean, it can be optional
86-
TemporalOptionsCustomizer<Builder> workFactoryCustomizer =
87+
List<TemporalOptionsCustomizer<Builder>> workFactoryCustomizers =
8788
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, Builder.class);
88-
TemporalOptionsCustomizer<WorkflowServiceStubsOptions.Builder> workflowServiceStubsCustomizer =
89-
findBeanByNameSpaceForTemporalCustomizer(
90-
beanPrefix, WorkflowServiceStubsOptions.Builder.class);
91-
TemporalOptionsCustomizer<WorkerOptions.Builder> WorkerCustomizer =
89+
List<TemporalOptionsCustomizer<WorkflowServiceStubsOptions.Builder>>
90+
workflowServiceStubsCustomizers =
91+
findBeanByNameSpaceForTemporalCustomizer(
92+
beanPrefix, WorkflowServiceStubsOptions.Builder.class);
93+
List<TemporalOptionsCustomizer<WorkerOptions.Builder>> workerCustomizers =
9294
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkerOptions.Builder.class);
93-
TemporalOptionsCustomizer<WorkflowClientOptions.Builder> workflowClientCustomizer =
95+
List<TemporalOptionsCustomizer<WorkflowClientOptions.Builder>> workflowClientCustomizers =
9496
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkflowClientOptions.Builder.class);
95-
TemporalOptionsCustomizer<ScheduleClientOptions.Builder> scheduleClientCustomizer =
97+
if (workflowClientCustomizers != null) {
98+
workflowClientCustomizers =
99+
workflowClientCustomizers.stream()
100+
.map(
101+
c ->
102+
(TemporalOptionsCustomizer<WorkflowClientOptions.Builder>)
103+
(WorkflowClientOptions.Builder o) ->
104+
c.customize(o).setNamespace(ns.getNamespace()))
105+
.collect(Collectors.toList());
106+
}
107+
List<TemporalOptionsCustomizer<ScheduleClientOptions.Builder>> scheduleClientCustomizers =
96108
findBeanByNameSpaceForTemporalCustomizer(beanPrefix, ScheduleClientOptions.Builder.class);
97-
TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>
98-
workflowImplementationCustomizer =
109+
List<TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>>
110+
workflowImplementationCustomizers =
99111
findBeanByNameSpaceForTemporalCustomizer(
100112
beanPrefix, WorkflowImplementationOptions.Builder.class);
101113

@@ -107,7 +119,7 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
107119
connectionProperties,
108120
metricsScope,
109121
testWorkflowEnvironment,
110-
workflowServiceStubsCustomizer);
122+
workflowServiceStubsCustomizers);
111123
WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs();
112124

113125
NonRootNamespaceTemplate namespaceTemplate =
@@ -121,16 +133,11 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) {
121133
null,
122134
tracer,
123135
testWorkflowEnvironment,
124-
workFactoryCustomizer,
125-
WorkerCustomizer,
126-
builder ->
127-
// Must make sure the namespace is set at the end of the builder chain
128-
Optional.ofNullable(workflowClientCustomizer)
129-
.map(c -> c.customize(builder))
130-
.orElse(builder)
131-
.setNamespace(ns.getNamespace()),
132-
scheduleClientCustomizer,
133-
workflowImplementationCustomizer);
136+
workFactoryCustomizers,
137+
workerCustomizers,
138+
workflowClientCustomizers,
139+
scheduleClientCustomizers,
140+
workflowImplementationCustomizers);
134141

135142
ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate();
136143
WorkflowClient workflowClient = clientTemplate.getWorkflowClient();
@@ -188,18 +195,19 @@ private <T> T findBeanByNamespace(String beanPrefix, Class<T> clazz) {
188195
return null;
189196
}
190197

191-
private <T> TemporalOptionsCustomizer<T> findBeanByNameSpaceForTemporalCustomizer(
198+
private <T> List<TemporalOptionsCustomizer<T>> findBeanByNameSpaceForTemporalCustomizer(
192199
String beanPrefix, Class<T> genericOptionsBuilderClass) {
193200
String beanName =
194201
AutoConfigurationUtils.temporalCustomizerBeanName(beanPrefix, genericOptionsBuilderClass);
195202
try {
196-
TemporalOptionsCustomizer genericOptionsCustomizer =
203+
// TODO(https://github.com/temporalio/sdk-java/issues/2638): Support multiple customizers in
204+
// the non root namespace
205+
TemporalOptionsCustomizer<T> genericOptionsCustomizer =
197206
beanFactory.getBean(beanName, TemporalOptionsCustomizer.class);
198-
return (TemporalOptionsCustomizer<T>) genericOptionsCustomizer;
207+
return Collections.singletonList(genericOptionsCustomizer);
199208
} catch (BeansException e) {
200209
log.warn("No TemporalOptionsCustomizer found for {}. ", beanName);
201210
if (genericOptionsBuilderClass.isAssignableFrom(Builder.class)) {
202-
// print tips once
203211
log.debug(
204212
"No TemporalOptionsCustomizer found for {}. \n You can add Customizer bean to do by namespace customization. \n "
205213
+ "Note: bean name should start with namespace name and end with Customizer, and the middle part should be the customizer "

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,25 @@ public NamespaceTemplate rootNamespaceTemplate(
9898
scheduleClientInterceptors, properties);
9999
List<WorkerInterceptor> chosenWorkerInterceptors =
100100
AutoConfigurationUtils.chooseWorkerInterceptors(workerInterceptors, properties);
101-
TemporalOptionsCustomizer<WorkerFactoryOptions.Builder> workerFactoryCustomizer =
102-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
103-
workerFactoryCustomizerMap, WorkerFactoryOptions.Builder.class, properties);
104-
TemporalOptionsCustomizer<WorkerOptions.Builder> workerCustomizer =
105-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
106-
workerCustomizerMap, WorkerOptions.Builder.class, properties);
107-
TemporalOptionsCustomizer<WorkflowClientOptions.Builder> clientCustomizer =
108-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
109-
clientCustomizerMap, WorkflowClientOptions.Builder.class, properties);
110-
TemporalOptionsCustomizer<ScheduleClientOptions.Builder> scheduleCustomizer =
111-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
112-
scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties);
113-
TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>
101+
List<TemporalOptionsCustomizer<WorkerFactoryOptions.Builder>> workerFactoryCustomizer =
102+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
103+
beanFactory,
104+
workerFactoryCustomizerMap,
105+
WorkerFactoryOptions.Builder.class,
106+
properties);
107+
List<TemporalOptionsCustomizer<WorkerOptions.Builder>> workerCustomizer =
108+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
109+
beanFactory, workerCustomizerMap, WorkerOptions.Builder.class, properties);
110+
List<TemporalOptionsCustomizer<WorkflowClientOptions.Builder>> clientCustomizer =
111+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
112+
beanFactory, clientCustomizerMap, WorkflowClientOptions.Builder.class, properties);
113+
List<TemporalOptionsCustomizer<ScheduleClientOptions.Builder>> scheduleCustomizer =
114+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
115+
beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties);
116+
List<TemporalOptionsCustomizer<WorkflowImplementationOptions.Builder>>
114117
workflowImplementationCustomizer =
115-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
118+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
119+
beanFactory,
116120
workflowImplementationCustomizerMap,
117121
WorkflowImplementationOptions.Builder.class,
118122
properties);

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties;
99
import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate;
1010
import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter;
11+
import java.util.List;
1112
import java.util.Map;
1213
import javax.annotation.Nullable;
1314
import org.springframework.beans.factory.annotation.Autowired;
1415
import org.springframework.beans.factory.annotation.Qualifier;
16+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
1517
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
1618
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
1719
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -26,6 +28,12 @@
2628
"${spring.temporal.test-server.enabled:false} || '${spring.temporal.connection.target:}'.length() > 0")
2729
public class ServiceStubsAutoConfiguration {
2830

31+
ConfigurableListableBeanFactory beanFactory;
32+
33+
public ServiceStubsAutoConfiguration(ConfigurableListableBeanFactory beanFactory) {
34+
this.beanFactory = beanFactory;
35+
}
36+
2937
@Bean(name = "temporalServiceStubsTemplate")
3038
public ServiceStubsTemplate serviceStubsTemplate(
3139
TemporalProperties properties,
@@ -35,9 +43,9 @@ public ServiceStubsTemplate serviceStubsTemplate(
3543
@Autowired(required = false) @Nullable
3644
Map<String, TemporalOptionsCustomizer<WorkflowServiceStubsOptions.Builder>>
3745
workflowServiceStubsCustomizerMap) {
38-
TemporalOptionsCustomizer<Builder> workflowServiceStubsCustomizer =
39-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
40-
workflowServiceStubsCustomizerMap, Builder.class, properties);
46+
List<TemporalOptionsCustomizer<Builder>> workflowServiceStubsCustomizer =
47+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
48+
beanFactory, workflowServiceStubsCustomizerMap, Builder.class, properties);
4149
return new ServiceStubsTemplate(
4250
properties.getConnection(),
4351
metricsScope,

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.slf4j.LoggerFactory;
2424
import org.springframework.beans.factory.annotation.Autowired;
2525
import org.springframework.beans.factory.annotation.Qualifier;
26+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2627
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2728
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2829
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -43,6 +44,12 @@ public class TestServerAutoConfiguration {
4344

4445
private static final Logger log = LoggerFactory.getLogger(TestServerAutoConfiguration.class);
4546

47+
private final ConfigurableListableBeanFactory beanFactory;
48+
49+
public TestServerAutoConfiguration(ConfigurableListableBeanFactory beanFactory) {
50+
this.beanFactory = beanFactory;
51+
}
52+
4653
@Bean(name = "temporalTestWorkflowEnvironmentAdapter")
4754
public TestWorkflowEnvironmentAdapter testTestWorkflowEnvironmentAdapter(
4855
@Qualifier("temporalTestWorkflowEnvironment")
@@ -64,7 +71,7 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
6471
List<ScheduleClientInterceptor> scheduleClientInterceptors,
6572
@Autowired(required = false) @Nullable List<WorkerInterceptor> workerInterceptors,
6673
@Autowired(required = false) @Nullable
67-
TemporalOptionsCustomizer<TestEnvironmentOptions.Builder> testEnvOptionsCustomizer,
74+
List<TemporalOptionsCustomizer<TestEnvironmentOptions.Builder>> testEnvOptionsCustomizers,
6875
@Autowired(required = false) @Nullable
6976
Map<String, TemporalOptionsCustomizer<WorkerFactoryOptions.Builder>>
7077
workerFactoryCustomizerMap,
@@ -84,15 +91,18 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
8491
List<WorkerInterceptor> chosenWorkerInterceptors =
8592
AutoConfigurationUtils.chooseWorkerInterceptors(workerInterceptors, properties);
8693

87-
TemporalOptionsCustomizer<WorkerFactoryOptions.Builder> workerFactoryCustomizer =
88-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
89-
workerFactoryCustomizerMap, WorkerFactoryOptions.Builder.class, properties);
90-
TemporalOptionsCustomizer<WorkflowClientOptions.Builder> clientCustomizer =
91-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
92-
clientCustomizerMap, WorkflowClientOptions.Builder.class, properties);
93-
TemporalOptionsCustomizer<ScheduleClientOptions.Builder> scheduleCustomizer =
94-
AutoConfigurationUtils.chooseTemporalCustomizerBean(
95-
scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties);
94+
List<TemporalOptionsCustomizer<WorkerFactoryOptions.Builder>> workerFactoryCustomizer =
95+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
96+
beanFactory,
97+
workerFactoryCustomizerMap,
98+
WorkerFactoryOptions.Builder.class,
99+
properties);
100+
List<TemporalOptionsCustomizer<WorkflowClientOptions.Builder>> clientCustomizer =
101+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
102+
beanFactory, clientCustomizerMap, WorkflowClientOptions.Builder.class, properties);
103+
List<TemporalOptionsCustomizer<ScheduleClientOptions.Builder>> scheduleCustomizer =
104+
AutoConfigurationUtils.chooseTemporalCustomizerBeans(
105+
beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties);
96106

97107
TestEnvironmentOptions.Builder options =
98108
TestEnvironmentOptions.newBuilder()
@@ -116,8 +126,11 @@ public TestWorkflowEnvironment testWorkflowEnvironment(
116126
properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer)
117127
.createWorkerFactoryOptions());
118128

119-
if (testEnvOptionsCustomizer != null) {
120-
options = testEnvOptionsCustomizer.customize(options);
129+
if (testEnvOptionsCustomizers != null) {
130+
for (TemporalOptionsCustomizer<TestEnvironmentOptions.Builder> testEnvOptionsCustomizer :
131+
testEnvOptionsCustomizers) {
132+
options = testEnvOptionsCustomizer.customize(options);
133+
}
121134
}
122135

123136
return TestWorkflowEnvironment.newInstance(options.build());

0 commit comments

Comments
 (0)