diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceAutoConfiguration.java index b8d332cbe..7990e0e10 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceAutoConfiguration.java @@ -1,134 +1,130 @@ -package io.temporal.spring.boot.autoconfigure; - -import com.google.common.base.MoreObjects; -import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; -import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; -import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; -import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; -import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.event.ApplicationContextEvent; -import org.springframework.context.event.ContextClosedEvent; -import org.springframework.context.event.ContextRefreshedEvent; - -@Configuration -@EnableConfigurationProperties(TemporalProperties.class) -@AutoConfigureAfter({RootNamespaceAutoConfiguration.class, ServiceStubsAutoConfiguration.class}) -@ConditionalOnBean(ServiceStubsAutoConfiguration.class) -@Conditional(NamespacesPresentCondition.class) -@ConditionalOnExpression( - "${spring.temporal.test-server.enabled:false} || '${spring.temporal.connection.target:}'.length() > 0") -public class NonRootNamespaceAutoConfiguration { - - protected static final Logger log = - LoggerFactory.getLogger(NonRootNamespaceAutoConfiguration.class); - - @Bean - public static NonRootBeanPostProcessor nonRootBeanPostProcessor( - @Lazy TemporalProperties properties) { - return new NonRootBeanPostProcessor(properties); - } - - @Bean - public static NonRootNamespaceEventListener nonRootNamespaceEventListener( - @Lazy TemporalProperties temporalProperties, - @Nullable @Lazy List workersTemplates) { - return new NonRootNamespaceEventListener(temporalProperties, workersTemplates); - } - - public static class NonRootNamespaceEventListener - implements ApplicationListener, ApplicationContextAware { - - private final TemporalProperties temporalProperties; - private final List workersTemplates; - private ApplicationContext applicationContext; - - public NonRootNamespaceEventListener( - TemporalProperties temporalProperties, List workersTemplates) { - this.temporalProperties = temporalProperties; - this.workersTemplates = workersTemplates; - } - - @Override - public void onApplicationEvent(ApplicationContextEvent event) { - if (event.getApplicationContext() == this.applicationContext) { - if (event instanceof ContextRefreshedEvent) { - onStart(); - } - } else if (event instanceof ContextClosedEvent) { - onStop(); - } - } - - private void onStart() { - this.executeByNamespace( - (nonRootNamespaceProperties, workersTemplate) -> { - String namespace = nonRootNamespaceProperties.getNamespace(); - Boolean startWorkers = - Optional.of(nonRootNamespaceProperties) - .map(NonRootNamespaceProperties::getStartWorkers) - .orElse(temporalProperties.getStartWorkers()); - startWorkers = MoreObjects.firstNonNull(startWorkers, Boolean.TRUE); - if (!startWorkers) { - log.info("skip start workers for non-root namespace [{}]", namespace); - return; - } - - workersTemplate - .getWorkers() - .forEach( - worker -> - log.debug( - "register worker :[{}] in worker queue [{}]", - worker.getTaskQueue(), - namespace)); - workersTemplate.getWorkerFactory().start(); - log.info("started workers for non-root namespace [{}]", namespace); - }); - } - - private void onStop() { - this.executeByNamespace( - (nonRootNamespaceProperties, workersTemplate) -> { - log.info("shutdown workers for non-root namespace"); - workersTemplate.getWorkerFactory().shutdown(); - }); - } - - private void executeByNamespace( - BiConsumer consumer) { - if (temporalProperties.getNamespaces() == null) { - return; - } - for (WorkersTemplate workersTemplate : workersTemplates) { - NamespaceProperties namespaceProperties = workersTemplate.getNamespaceProperties(); - if (namespaceProperties instanceof NonRootNamespaceProperties) { - NonRootNamespaceProperties nonRootNamespaceProperties = - (NonRootNamespaceProperties) namespaceProperties; - consumer.accept(nonRootNamespaceProperties, workersTemplate); - } - } - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - } -} +package io.temporal.spring.boot.autoconfigure; + +import com.google.common.base.MoreObjects; +import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; +import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; +import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; +import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.ApplicationContextEvent; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; + +@Configuration +@EnableConfigurationProperties(TemporalProperties.class) +@AutoConfigureAfter({RootNamespaceAutoConfiguration.class, ServiceStubsAutoConfiguration.class}) +@ConditionalOnBean(ServiceStubsAutoConfiguration.class) +@Conditional(NamespacesPresentCondition.class) +@ConditionalOnExpression( + "${spring.temporal.test-server.enabled:false} || '${spring.temporal.connection.target:}'.length() > 0") +@Import(NonRootNamespaceRegistrar.class) +public class NonRootNamespaceAutoConfiguration { + + protected static final Logger log = + LoggerFactory.getLogger(NonRootNamespaceAutoConfiguration.class); + + @Bean + public static NonRootNamespaceEventListener nonRootNamespaceEventListener( + @Lazy TemporalProperties temporalProperties, + @Nullable @Lazy List workersTemplates) { + return new NonRootNamespaceEventListener(temporalProperties, workersTemplates); + } + + public static class NonRootNamespaceEventListener + implements ApplicationListener, ApplicationContextAware { + + private final TemporalProperties temporalProperties; + private final List workersTemplates; + private ApplicationContext applicationContext; + + public NonRootNamespaceEventListener( + TemporalProperties temporalProperties, List workersTemplates) { + this.temporalProperties = temporalProperties; + this.workersTemplates = workersTemplates; + } + + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + if (event instanceof ContextRefreshedEvent) { + onStart(); + } + } else if (event instanceof ContextClosedEvent) { + onStop(); + } + } + + private void onStart() { + this.executeByNamespace( + (nonRootNamespaceProperties, workersTemplate) -> { + String namespace = nonRootNamespaceProperties.getNamespace(); + Boolean startWorkers = + Optional.of(nonRootNamespaceProperties) + .map(NonRootNamespaceProperties::getStartWorkers) + .orElse(temporalProperties.getStartWorkers()); + startWorkers = MoreObjects.firstNonNull(startWorkers, Boolean.TRUE); + if (!startWorkers) { + log.info("skip start workers for non-root namespace [{}]", namespace); + return; + } + + workersTemplate + .getWorkers() + .forEach( + worker -> + log.debug( + "register worker :[{}] in worker queue [{}]", + worker.getTaskQueue(), + namespace)); + workersTemplate.getWorkerFactory().start(); + log.info("started workers for non-root namespace [{}]", namespace); + }); + } + + private void onStop() { + this.executeByNamespace( + (nonRootNamespaceProperties, workersTemplate) -> { + log.info("shutdown workers for non-root namespace"); + workersTemplate.getWorkerFactory().shutdown(); + }); + } + + private void executeByNamespace( + BiConsumer consumer) { + if (temporalProperties.getNamespaces() == null) { + return; + } + for (WorkersTemplate workersTemplate : workersTemplates) { + NamespaceProperties namespaceProperties = workersTemplate.getNamespaceProperties(); + if (namespaceProperties instanceof NonRootNamespaceProperties) { + NonRootNamespaceProperties nonRootNamespaceProperties = + (NonRootNamespaceProperties) namespaceProperties; + consumer.accept(nonRootNamespaceProperties, workersTemplate); + } + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + } +} 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/NonRootNamespaceFactory.java similarity index 63% rename from temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java rename to temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceFactory.java index 71f952137..cbba9bebf 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/NonRootNamespaceFactory.java @@ -1,214 +1,218 @@ -package io.temporal.spring.boot.autoconfigure; - -import com.google.common.base.MoreObjects; -import com.uber.m3.tally.Scope; -import io.opentracing.Tracer; -import io.temporal.client.WorkflowClient; -import io.temporal.client.WorkflowClientOptions; -import io.temporal.client.schedules.ScheduleClient; -import io.temporal.client.schedules.ScheduleClientOptions; -import io.temporal.common.converter.DataConverter; -import io.temporal.serviceclient.WorkflowServiceStubs; -import io.temporal.serviceclient.WorkflowServiceStubsOptions; -import io.temporal.spring.boot.TemporalOptionsCustomizer; -import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; -import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; -import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; -import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; -import io.temporal.spring.boot.autoconfigure.template.NamespaceTemplate; -import io.temporal.spring.boot.autoconfigure.template.NonRootNamespaceTemplate; -import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate; -import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter; -import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; -import io.temporal.worker.WorkerFactory; -import io.temporal.worker.WorkerFactoryOptions.Builder; -import io.temporal.worker.WorkerOptions; -import io.temporal.worker.WorkflowImplementationOptions; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanNotOfRequiredTypeException; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; - -public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { - - private static final Logger log = LoggerFactory.getLogger(NonRootBeanPostProcessor.class); - - private ConfigurableListableBeanFactory beanFactory; - - private final @Nonnull TemporalProperties temporalProperties; - private final @Nullable List namespaceProperties; - private @Nullable Tracer tracer; - private @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment; - private @Nullable Scope metricsScope; - - public NonRootBeanPostProcessor(@Nonnull TemporalProperties temporalProperties) { - this.temporalProperties = temporalProperties; - this.namespaceProperties = temporalProperties.getNamespaces(); - } - - @Override - public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull String beanName) - throws BeansException { - if (bean instanceof NamespaceTemplate && beanName.equals("temporalRootNamespaceTemplate")) { - if (namespaceProperties != null) { - // If there are non-root namespaces, we need to inject beans for each of them. Look - // up the bean manually instead of using @Autowired to avoid circular dependencies or - // causing the dependency to - // get initialized to early and skip post-processing. - // - // Note: We don't use @Lazy here because these dependencies are optional and @Lazy doesn't - // interact well with - // optional dependencies. - metricsScope = findBean("temporalMetricsScope", Scope.class); - tracer = findBean(Tracer.class); - testWorkflowEnvironment = - findBean("temporalTestWorkflowEnvironment", TestWorkflowEnvironmentAdapter.class); - namespaceProperties.forEach(this::injectBeanByNonRootNamespace); - } - } - return bean; - } - - private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { - String beanPrefix = MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace()); - DataConverter dataConverterByNamespace = findBeanByNamespace(beanPrefix, DataConverter.class); - - // found regarding namespace customizer bean, it can be optional - TemporalOptionsCustomizer workFactoryCustomizer = - findBeanByNameSpaceForTemporalCustomizer(beanPrefix, Builder.class); - TemporalOptionsCustomizer workflowServiceStubsCustomizer = - findBeanByNameSpaceForTemporalCustomizer( - beanPrefix, WorkflowServiceStubsOptions.Builder.class); - TemporalOptionsCustomizer WorkerCustomizer = - findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkerOptions.Builder.class); - TemporalOptionsCustomizer workflowClientCustomizer = - findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkflowClientOptions.Builder.class); - TemporalOptionsCustomizer scheduleClientCustomizer = - findBeanByNameSpaceForTemporalCustomizer(beanPrefix, ScheduleClientOptions.Builder.class); - TemporalOptionsCustomizer - workflowImplementationCustomizer = - findBeanByNameSpaceForTemporalCustomizer( - beanPrefix, WorkflowImplementationOptions.Builder.class); - - // it not set namespace connection properties, use root connection properties - ConnectionProperties connectionProperties = - MoreObjects.firstNonNull(ns.getConnection(), temporalProperties.getConnection()); - ServiceStubsTemplate serviceStubsTemplate = - new ServiceStubsTemplate( - connectionProperties, - metricsScope, - testWorkflowEnvironment, - workflowServiceStubsCustomizer); - WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs(); - - NonRootNamespaceTemplate namespaceTemplate = - new NonRootNamespaceTemplate( - beanFactory, - ns, - workflowServiceStubs, - dataConverterByNamespace, - null, // Currently interceptors are not supported in non-root namespace - null, - null, - tracer, - testWorkflowEnvironment, - workFactoryCustomizer, - WorkerCustomizer, - builder -> - // Must make sure the namespace is set at the end of the builder chain - Optional.ofNullable(workflowClientCustomizer) - .map(c -> c.customize(builder)) - .orElse(builder) - .setNamespace(ns.getNamespace()), - scheduleClientCustomizer, - workflowImplementationCustomizer); - - ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate(); - WorkflowClient workflowClient = clientTemplate.getWorkflowClient(); - ScheduleClient scheduleClient = clientTemplate.getScheduleClient(); - WorkersTemplate workersTemplate = namespaceTemplate.getWorkersTemplate(); - WorkerFactory workerFactory = workersTemplate.getWorkerFactory(); - - // register beans by namespace - beanFactory.registerSingleton( - beanPrefix + ServiceStubsTemplate.class.getSimpleName(), serviceStubsTemplate); - beanFactory.registerSingleton( - beanPrefix + WorkflowServiceStubs.class.getSimpleName(), workflowServiceStubs); - beanFactory.registerSingleton( - beanPrefix + NamespaceTemplate.class.getSimpleName(), namespaceTemplate); - beanFactory.registerSingleton( - beanPrefix + ClientTemplate.class.getSimpleName(), namespaceTemplate.getClientTemplate()); - beanFactory.registerSingleton( - beanPrefix + WorkersTemplate.class.getSimpleName(), workersTemplate); - beanFactory.registerSingleton( - beanPrefix + WorkflowClient.class.getSimpleName(), workflowClient); - beanFactory.registerSingleton( - beanPrefix + ScheduleClient.class.getSimpleName(), scheduleClient); - beanFactory.registerSingleton(beanPrefix + WorkerFactory.class.getSimpleName(), workerFactory); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - - private T findBeanByNamespace(String beanPrefix, Class clazz) { - try { - return beanFactory.getBean(beanPrefix + clazz.getSimpleName(), clazz); - } catch (NoSuchBeanDefinitionException ignore) { - // Made non-namespace bean optional - } - return null; - } - - private @Nullable T findBean(Class clazz) { - try { - return beanFactory.getBean(clazz); - } catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException ignore) { - // Ignore if the bean is not found or not of the required type - } - return null; - } - - private @Nullable T findBean(String beanName, Class clazz) { - try { - return beanFactory.getBean(beanName, clazz); - } catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException ignore) { - // Ignore if the bean is not found or not of the required type - } - return null; - } - - private TemporalOptionsCustomizer findBeanByNameSpaceForTemporalCustomizer( - String beanPrefix, Class genericOptionsBuilderClass) { - String beanName = - AutoConfigurationUtils.temporalCustomizerBeanName(beanPrefix, genericOptionsBuilderClass); - try { - TemporalOptionsCustomizer genericOptionsCustomizer = - beanFactory.getBean(beanName, TemporalOptionsCustomizer.class); - return (TemporalOptionsCustomizer) genericOptionsCustomizer; - } catch (BeansException e) { - log.warn("No TemporalOptionsCustomizer found for {}. ", beanName); - if (genericOptionsBuilderClass.isAssignableFrom(Builder.class)) { - // print tips once - log.debug( - "No TemporalOptionsCustomizer found for {}. \n You can add Customizer bean to do by namespace customization. \n " - + "Note: bean name should start with namespace name and end with Customizer, and the middle part should be the customizer " - + "target class name. \n " - + "Example: @Bean(\"nsWorkerFactoryCustomizer\") is a customizer bean for WorkerFactory via " - + "TemporalOptionsCustomizer", - genericOptionsBuilderClass.getSimpleName()); - } - return null; - } - } -} +package io.temporal.spring.boot.autoconfigure; + +import com.google.common.base.MoreObjects; +import com.uber.m3.tally.Scope; +import io.opentracing.Tracer; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.schedules.ScheduleClient; +import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.common.converter.DataConverter; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.spring.boot.TemporalOptionsCustomizer; +import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; +import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; +import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; +import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; +import io.temporal.spring.boot.autoconfigure.template.NamespaceTemplate; +import io.temporal.spring.boot.autoconfigure.template.NonRootNamespaceTemplate; +import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate; +import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter; +import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; +import io.temporal.worker.WorkerFactory; +import io.temporal.worker.WorkerFactoryOptions.Builder; +import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkflowImplementationOptions; +import java.util.Optional; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +@SuppressWarnings("unused") +public class NonRootNamespaceFactory implements BeanFactoryAware { + + private static final Logger log = LoggerFactory.getLogger(NonRootNamespaceFactory.class); + + private final NonRootNamespaceProperties namespaceProperties; + private final TemporalProperties temporalProperties; + private BeanFactory beanFactory; + + private NonRootNamespaceTemplate namespaceTemplate; + private boolean initialized = false; + + public NonRootNamespaceFactory( + NonRootNamespaceProperties namespaceProperties, TemporalProperties temporalProperties) { + this.namespaceProperties = namespaceProperties; + this.temporalProperties = temporalProperties; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + private synchronized void ensureInitialized() { + if (!initialized) { + initializeNamespaceTemplate(); + initialized = true; + } + } + + private void initializeNamespaceTemplate() { + String beanPrefix = + MoreObjects.firstNonNull( + namespaceProperties.getAlias(), namespaceProperties.getNamespace()); + + Scope metricsScope = findBean("temporalMetricsScope", Scope.class); + Tracer tracer = findBean(Tracer.class); + TestWorkflowEnvironmentAdapter testWorkflowEnvironment = + findBean("temporalTestWorkflowEnvironment", TestWorkflowEnvironmentAdapter.class); + + DataConverter dataConverterByNamespace = findBeanByNamespace(beanPrefix, DataConverter.class); + + // Find customizers + TemporalOptionsCustomizer workFactoryCustomizer = + findBeanByNameSpaceForTemporalCustomizer(beanPrefix, Builder.class); + TemporalOptionsCustomizer workflowServiceStubsCustomizer = + findBeanByNameSpaceForTemporalCustomizer( + beanPrefix, WorkflowServiceStubsOptions.Builder.class); + TemporalOptionsCustomizer WorkerCustomizer = + findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkerOptions.Builder.class); + TemporalOptionsCustomizer workflowClientCustomizer = + findBeanByNameSpaceForTemporalCustomizer(beanPrefix, WorkflowClientOptions.Builder.class); + TemporalOptionsCustomizer scheduleClientCustomizer = + findBeanByNameSpaceForTemporalCustomizer(beanPrefix, ScheduleClientOptions.Builder.class); + TemporalOptionsCustomizer + workflowImplementationCustomizer = + findBeanByNameSpaceForTemporalCustomizer( + beanPrefix, WorkflowImplementationOptions.Builder.class); + + // it not set namespace connection properties, use root connection properties + ConnectionProperties connectionProperties = + MoreObjects.firstNonNull( + namespaceProperties.getConnection(), temporalProperties.getConnection()); + + ServiceStubsTemplate serviceStubsTemplate = + new ServiceStubsTemplate( + connectionProperties, + metricsScope, + testWorkflowEnvironment, + workflowServiceStubsCustomizer); + + WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs(); + + namespaceTemplate = + new NonRootNamespaceTemplate( + beanFactory, + namespaceProperties, + workflowServiceStubs, + dataConverterByNamespace, + null, // Currently interceptors are not supported in non-root namespace + null, + null, + tracer, + testWorkflowEnvironment, + workFactoryCustomizer, + WorkerCustomizer, + builder -> + // Must make sure the namespace is set at the end of the builder chain + Optional.ofNullable(workflowClientCustomizer) + .map(c -> c.customize(builder)) + .orElse(builder) + .setNamespace(namespaceProperties.getNamespace()), + scheduleClientCustomizer, + workflowImplementationCustomizer); + } + + public WorkflowClient createWorkflowClient() { + ensureInitialized(); + return namespaceTemplate.getClientTemplate().getWorkflowClient(); + } + + public ScheduleClient createScheduleClient() { + ensureInitialized(); + return namespaceTemplate.getClientTemplate().getScheduleClient(); + } + + public WorkerFactory createWorkerFactory() { + ensureInitialized(); + return namespaceTemplate.getWorkersTemplate().getWorkerFactory(); + } + + public WorkflowServiceStubs createWorkflowServiceStubs() { + ensureInitialized(); + return namespaceTemplate.getClientTemplate().getWorkflowClient().getWorkflowServiceStubs(); + } + + public ClientTemplate createClientTemplate() { + ensureInitialized(); + return namespaceTemplate.getClientTemplate(); + } + + public WorkersTemplate createWorkersTemplate() { + ensureInitialized(); + return namespaceTemplate.getWorkersTemplate(); + } + + public NamespaceTemplate createNamespaceTemplate() { + ensureInitialized(); + return namespaceTemplate; + } + + private T findBeanByNamespace(String beanPrefix, Class clazz) { + try { + return beanFactory.getBean(beanPrefix + clazz.getSimpleName(), clazz); + } catch (NoSuchBeanDefinitionException ignore) { + // Made non-namespace bean optional + } + return null; + } + + private @Nullable T findBean(Class clazz) { + try { + return beanFactory.getBean(clazz); + } catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException ignore) { + // Ignore if the bean is not found or not of the required type + } + return null; + } + + private @Nullable T findBean(String beanName, Class clazz) { + try { + return beanFactory.getBean(beanName, clazz); + } catch (NoSuchBeanDefinitionException | BeanNotOfRequiredTypeException ignore) { + // Ignore if the bean is not found or not of the required type + } + return null; + } + + private TemporalOptionsCustomizer findBeanByNameSpaceForTemporalCustomizer( + String beanPrefix, Class genericOptionsBuilderClass) { + String beanName = + AutoConfigurationUtils.temporalCustomizerBeanName(beanPrefix, genericOptionsBuilderClass); + try { + TemporalOptionsCustomizer genericOptionsCustomizer = + beanFactory.getBean(beanName, TemporalOptionsCustomizer.class); + return (TemporalOptionsCustomizer) genericOptionsCustomizer; + } catch (BeansException e) { + log.warn("No TemporalOptionsCustomizer found for {}. ", beanName); + if (genericOptionsBuilderClass.isAssignableFrom(Builder.class)) { + // print tips once + log.debug( + "No TemporalOptionsCustomizer found for {}. \n You can add Customizer bean to do by namespace customization. \n " + + "Note: bean name should start with namespace name and end with Customizer, and the middle part should be the customizer " + + "target class name. \n " + + "Example: @Bean(\"nsWorkerFactoryCustomizer\") is a customizer bean for WorkerFactory via " + + "TemporalOptionsCustomizer", + genericOptionsBuilderClass.getSimpleName()); + } + return null; + } + } +} diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrar.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrar.java new file mode 100644 index 000000000..58ff16b07 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrar.java @@ -0,0 +1,164 @@ +package io.temporal.spring.boot.autoconfigure; + +import com.google.common.base.MoreObjects; +import io.temporal.client.WorkflowClient; +import io.temporal.client.schedules.ScheduleClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; +import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; +import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; +import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; +import io.temporal.spring.boot.autoconfigure.template.NamespaceTemplate; +import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; +import io.temporal.worker.WorkerFactory; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +public class NonRootNamespaceRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { + + private static final Logger log = LoggerFactory.getLogger(NonRootNamespaceRegistrar.class); + + private Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void registerBeanDefinitions( + AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + TemporalProperties temporalProperties = bindTemporalProperties(); + + List namespaces = temporalProperties.getNamespaces(); + if (namespaces != null && !namespaces.isEmpty()) { + log.info("Registering bean definitions for {} non-root namespaces", namespaces.size()); + + namespaces.forEach( + ns -> registerBeanDefinitionsForNamespace(registry, ns, temporalProperties)); + } else { + log.debug("No non-root namespaces found in configuration - skipping bean registration"); + } + } + + private TemporalProperties bindTemporalProperties() { + try { + return Binder.get(environment) + .bind("spring.temporal", TemporalProperties.class) + .orElse(createDefaultTemporalProperties()); + } catch (Exception e) { + log.warn("Failed to bind TemporalProperties from environment: {}", e.getMessage()); + return createDefaultTemporalProperties(); + } + } + + private TemporalProperties createDefaultTemporalProperties() { + ConnectionProperties connection = new ConnectionProperties("localhost:7233", null, null, null); + return new TemporalProperties(null, null, null, null, null, connection, null, null, null); + } + + private void registerBeanDefinitionsForNamespace( + BeanDefinitionRegistry registry, + NonRootNamespaceProperties ns, + TemporalProperties temporalProperties) { + + String beanPrefix = MoreObjects.firstNonNull(ns.getAlias(), ns.getNamespace()); + registerNamespaceFactoryBeanDefinition(registry, beanPrefix, ns, temporalProperties); + + registerWorkflowClientBeanDefinition(registry, beanPrefix); + registerScheduleClientBeanDefinition(registry, beanPrefix); + registerWorkerFactoryBeanDefinition(registry, beanPrefix); + registerServiceStubsBeanDefinition(registry, beanPrefix); + registerClientTemplateBeanDefinition(registry, beanPrefix); + registerWorkersTemplateBeanDefinition(registry, beanPrefix); + registerNamespaceTemplateBeanDefinition(registry, beanPrefix); + } + + private void registerNamespaceFactoryBeanDefinition( + BeanDefinitionRegistry registry, + String beanPrefix, + NonRootNamespaceProperties ns, + TemporalProperties temporalProperties) { + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(NonRootNamespaceFactory.class) + .addConstructorArgValue(ns) + .addConstructorArgValue(temporalProperties) + .setScope("singleton"); + + registry.registerBeanDefinition(beanPrefix + "NamespaceFactory", builder.getBeanDefinition()); + } + + private void registerWorkflowClientBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(WorkflowClient.class) + .setFactoryMethodOnBean("createWorkflowClient", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "WorkflowClient", builder.getBeanDefinition()); + } + + private void registerScheduleClientBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(ScheduleClient.class) + .setFactoryMethodOnBean("createScheduleClient", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "ScheduleClient", builder.getBeanDefinition()); + } + + private void registerWorkerFactoryBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(WorkerFactory.class) + .setFactoryMethodOnBean("createWorkerFactory", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "WorkerFactory", builder.getBeanDefinition()); + } + + private void registerServiceStubsBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(WorkflowServiceStubs.class) + .setFactoryMethodOnBean("createWorkflowServiceStubs", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition( + beanPrefix + "WorkflowServiceStubs", builder.getBeanDefinition()); + } + + private void registerClientTemplateBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(ClientTemplate.class) + .setFactoryMethodOnBean("createClientTemplate", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "ClientTemplate", builder.getBeanDefinition()); + } + + private void registerWorkersTemplateBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(WorkersTemplate.class) + .setFactoryMethodOnBean("createWorkersTemplate", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "WorkersTemplate", builder.getBeanDefinition()); + } + + private void registerNamespaceTemplateBeanDefinition( + BeanDefinitionRegistry registry, String beanPrefix) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(NamespaceTemplate.class) + .setFactoryMethodOnBean("createNamespaceTemplate", beanPrefix + "NamespaceFactory"); + + registry.registerBeanDefinition(beanPrefix + "NamespaceTemplate", builder.getBeanDefinition()); + } +} 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 371ed3c62..6340ad043 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 @@ -17,6 +17,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; @Configuration @EnableConfigurationProperties(TemporalProperties.class) @@ -46,6 +47,7 @@ public ServiceStubsTemplate serviceStubsTemplate( } @Bean(name = "temporalWorkflowServiceStubs") + @Primary public WorkflowServiceStubs workflowServiceStubsTemplate( @Qualifier("temporalServiceStubsTemplate") ServiceStubsTemplate serviceStubsTemplate) { return serviceStubsTemplate.getWorkflowServiceStubs(); diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/MultiNamespaceTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/MultiNamespaceTest.java index eb80003cf..e1682a049 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/MultiNamespaceTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/MultiNamespaceTest.java @@ -1,92 +1,94 @@ -package io.temporal.spring.boot.autoconfigure; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.temporal.spring.boot.TemporalOptionsCustomizer; -import io.temporal.worker.WorkflowImplementationOptions; -import io.temporal.worker.WorkflowImplementationOptions.Builder; -import java.util.Map; -import java.util.Set; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.Timeout; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest(classes = MultiNamespaceTest.Configuration.class) -@ActiveProfiles(profiles = "multi-namespaces") -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class MultiNamespaceTest { - - @Autowired ConfigurableApplicationContext applicationContext; - - @BeforeEach - void setUp() { - applicationContext.start(); - } - - @Test - @Timeout(value = 10) - public void shouldContainsNonRootRelatedBean() { - Assertions.assertTrue(applicationContext.containsBean("nonRootBeanPostProcessor")); - Assertions.assertTrue(applicationContext.containsBean("ns1NamespaceTemplate")); - Assertions.assertTrue(applicationContext.containsBean("namespace2NamespaceTemplate")); - Assertions.assertTrue(applicationContext.containsBean("ns1ClientTemplate")); - Assertions.assertTrue(applicationContext.containsBean("namespace2ClientTemplate")); - Assertions.assertTrue(applicationContext.containsBean("ns1WorkflowClient")); - Assertions.assertTrue(applicationContext.containsBean("namespace2WorkflowClient")); - Assertions.assertTrue(applicationContext.containsBean("ns1ScheduleClient")); - Assertions.assertTrue(applicationContext.containsBean("namespace2ScheduleClient")); - Assertions.assertTrue(applicationContext.containsBean("ns1WorkerFactory")); - Assertions.assertTrue(applicationContext.containsBean("namespace2WorkerFactory")); - - String builderCanonicalName = Builder.class.getCanonicalName(); - String bindingCustomizerName = builderCanonicalName.replace("Options.Builder", "Customizer"); - bindingCustomizerName = - bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1); - - Map beansOfType = - applicationContext.getBeansOfType(TemporalOptionsCustomizer.class); - Set beanNames = beansOfType.keySet(); - Assertions.assertEquals(3, beansOfType.size()); - Assertions.assertTrue(beanNames.contains("ns1" + bindingCustomizerName)); - Assertions.assertTrue(beanNames.contains("namespace2" + bindingCustomizerName)); - } - - @EnableAutoConfiguration - public static class Configuration { - - @Bean - public TemporalOptionsCustomizer - workflowImplementationCustomizer() { - return getReturningMock(); - } - - @Bean - public TemporalOptionsCustomizer - ns1WorkflowImplementationCustomizer() { - return getReturningMock(); - } - - @Bean - public TemporalOptionsCustomizer - namespace2WorkflowImplementationCustomizer() { - return getReturningMock(); - } - - @SuppressWarnings("unchecked") - private TemporalOptionsCustomizer getReturningMock() { - return when(mock(TemporalOptionsCustomizer.class).customize(any())) - .thenAnswer(invocation -> invocation.getArgument(0)) - .getMock(); - } - } -} +package io.temporal.spring.boot.autoconfigure; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.temporal.spring.boot.TemporalOptionsCustomizer; +import io.temporal.worker.WorkflowImplementationOptions; +import io.temporal.worker.WorkflowImplementationOptions.Builder; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = MultiNamespaceTest.Configuration.class) +@ActiveProfiles(profiles = "multi-namespaces") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class MultiNamespaceTest { + + @Autowired ConfigurableApplicationContext applicationContext; + + @BeforeEach + void setUp() { + applicationContext.start(); + } + + @Test + @Timeout(value = 10) + public void shouldContainsNonRootRelatedBean() { + Assertions.assertTrue(applicationContext.containsBean("ns1NamespaceFactory")); + Assertions.assertTrue(applicationContext.containsBean("namespace2NamespaceFactory")); + + Assertions.assertTrue(applicationContext.containsBean("ns1NamespaceTemplate")); + Assertions.assertTrue(applicationContext.containsBean("namespace2NamespaceTemplate")); + Assertions.assertTrue(applicationContext.containsBean("ns1ClientTemplate")); + Assertions.assertTrue(applicationContext.containsBean("namespace2ClientTemplate")); + Assertions.assertTrue(applicationContext.containsBean("ns1WorkflowClient")); + Assertions.assertTrue(applicationContext.containsBean("namespace2WorkflowClient")); + Assertions.assertTrue(applicationContext.containsBean("ns1ScheduleClient")); + Assertions.assertTrue(applicationContext.containsBean("namespace2ScheduleClient")); + Assertions.assertTrue(applicationContext.containsBean("ns1WorkerFactory")); + Assertions.assertTrue(applicationContext.containsBean("namespace2WorkerFactory")); + + String builderCanonicalName = Builder.class.getCanonicalName(); + String bindingCustomizerName = builderCanonicalName.replace("Options.Builder", "Customizer"); + bindingCustomizerName = + bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1); + + Map beansOfType = + applicationContext.getBeansOfType(TemporalOptionsCustomizer.class); + Set beanNames = beansOfType.keySet(); + Assertions.assertEquals(3, beansOfType.size()); + Assertions.assertTrue(beanNames.contains("ns1" + bindingCustomizerName)); + Assertions.assertTrue(beanNames.contains("namespace2" + bindingCustomizerName)); + } + + @EnableAutoConfiguration + public static class Configuration { + + @Bean + public TemporalOptionsCustomizer + workflowImplementationCustomizer() { + return getReturningMock(); + } + + @Bean + public TemporalOptionsCustomizer + ns1WorkflowImplementationCustomizer() { + return getReturningMock(); + } + + @Bean + public TemporalOptionsCustomizer + namespace2WorkflowImplementationCustomizer() { + return getReturningMock(); + } + + @SuppressWarnings("unchecked") + private TemporalOptionsCustomizer getReturningMock() { + return when(mock(TemporalOptionsCustomizer.class).customize(any())) + .thenAnswer(invocation -> invocation.getArgument(0)) + .getMock(); + } + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceBeanAvailabilityTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceBeanAvailabilityTest.java new file mode 100644 index 000000000..8110fabb8 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceBeanAvailabilityTest.java @@ -0,0 +1,178 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.temporal.client.WorkflowClient; +import javax.annotation.Resource; +import org.junit.jupiter.api.*; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = NonRootNamespaceBeanAvailabilityTest.TestConfiguration.class) +@ActiveProfiles(profiles = "multi-namespaces") +public class NonRootNamespaceBeanAvailabilityTest { + + @Autowired private ConfigurableApplicationContext applicationContext; + @Autowired private ResourceInjectionComponent resourceInjectionComponent; + @Autowired private AutowiredInjectionComponent autowiredInjectionComponent; + @Autowired private ConstructorInjectionComponent constructorInjectionComponent; + @Autowired private BeanAvailabilityValidator beanAvailabilityValidator; + + @Test + public void shouldRegisterNonRootNamespaceBeansInContext() { + assertThat(applicationContext.containsBean("ns1WorkflowClient")).isTrue(); + assertThat(applicationContext.containsBean("namespace2WorkflowClient")).isTrue(); + } + + @Test + public void shouldHaveBeansAvailableForLookup() { + WorkflowClient ns1Client = + applicationContext.getBean("ns1WorkflowClient", WorkflowClient.class); + WorkflowClient ns2Client = + applicationContext.getBean("namespace2WorkflowClient", WorkflowClient.class); + + assertNotNull(ns1Client); + assertNotNull(ns2Client); + assertThat(ns1Client.getOptions().getNamespace()).isEqualTo("namespace1"); + assertThat(ns2Client.getOptions().getNamespace()).isEqualTo("namespace2"); + } + + @Test + public void shouldInjectNonRootBeansWithResourceAnnotation() { + assertNotNull(resourceInjectionComponent); + assertNotNull(resourceInjectionComponent.getNs1WorkflowClient()); + assertThat(resourceInjectionComponent.getNs1WorkflowClient().getOptions().getNamespace()) + .isEqualTo("namespace1"); + } + + @Test + public void shouldInjectNonRootBeansWithAutowiredAndQualifier() { + assertNotNull(autowiredInjectionComponent); + assertNotNull(autowiredInjectionComponent.getNs1WorkflowClient()); + assertThat(autowiredInjectionComponent.getNs1WorkflowClient().getOptions().getNamespace()) + .isEqualTo("namespace1"); + } + + @Test + public void shouldInjectNonRootBeansWithConstructorInjection() { + assertNotNull(constructorInjectionComponent); + assertNotNull(constructorInjectionComponent.getNs1WorkflowClient()); + assertThat(constructorInjectionComponent.getNs1WorkflowClient().getOptions().getNamespace()) + .isEqualTo("namespace1"); + } + + @Test + public void shouldHandleTimingIssueGracefully() { + assertThat(beanAvailabilityValidator.isTimingIssueDetected()) + .isFalse() + .as("No timing issues should be detected - Issue #2579 should be resolved"); + } + + @Configuration + @EnableAutoConfiguration + static class TestConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public BeanAvailabilityValidator timingTestBeanPostProcessor() { + return new BeanAvailabilityValidator(); + } + + @Bean + @Profile("multi-namespaces") + public ResourceInjectionComponent resourceInjectionComponent() { + return new ResourceInjectionComponent(); + } + + @Bean + @Profile("multi-namespaces") + public AutowiredInjectionComponent autowiredInjectionComponent() { + return new AutowiredInjectionComponent(); + } + + @Bean + @Profile("multi-namespaces") + public ConstructorInjectionComponent constructorInjectionComponent( + @Qualifier("ns1WorkflowClient") WorkflowClient ns1WorkflowClient) { + return new ConstructorInjectionComponent(ns1WorkflowClient); + } + } + + static class ResourceInjectionComponent { + @Resource(name = "ns1WorkflowClient") + private WorkflowClient ns1WorkflowClient; + + public WorkflowClient getNs1WorkflowClient() { + return ns1WorkflowClient; + } + } + + static class AutowiredInjectionComponent { + @Autowired + @Qualifier("ns1WorkflowClient") + private WorkflowClient ns1WorkflowClient; + + public WorkflowClient getNs1WorkflowClient() { + return ns1WorkflowClient; + } + } + + static class ConstructorInjectionComponent { + private final WorkflowClient ns1WorkflowClient; + + public ConstructorInjectionComponent(WorkflowClient ns1WorkflowClient) { + this.ns1WorkflowClient = ns1WorkflowClient; + } + + public WorkflowClient getNs1WorkflowClient() { + return ns1WorkflowClient; + } + } + + static class BeanAvailabilityValidator implements BeanPostProcessor, BeanFactoryAware { + private BeanFactory beanFactory; + private boolean timingIssueDetected = false; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + // Helps verify non-root namespace beans are accessible during bean post-processing + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (beanName.equals("resourceInjectionComponent") + || beanName.equals("autowiredInjectionComponent") + || beanName.equals("constructorInjectionComponent")) { + try { + WorkflowClient ns1Client = beanFactory.getBean("ns1WorkflowClient", WorkflowClient.class); + if (!"namespace1".equals(ns1Client.getOptions().getNamespace())) { + timingIssueDetected = true; + } + } catch (Exception e) { + timingIssueDetected = true; + } + } + return bean; + } + + public boolean isTimingIssueDetected() { + return timingIssueDetected; + } + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceFactoryTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceFactoryTest.java new file mode 100644 index 000000000..40a4c3366 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceFactoryTest.java @@ -0,0 +1,82 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.temporal.client.WorkflowClient; +import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; +import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; +import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +public class NonRootNamespaceFactoryTest { + + private NonRootNamespaceFactory factory; + private BeanFactory mockBeanFactory; + + @BeforeEach + void setUp() { + NonRootNamespaceProperties namespaceProperties = createTestNamespaceProperties(); + TemporalProperties temporalProperties = createTestTemporalProperties(); + mockBeanFactory = mock(BeanFactory.class); + + factory = new NonRootNamespaceFactory(namespaceProperties, temporalProperties); + factory.setBeanFactory(mockBeanFactory); + } + + @Test + public void shouldCreateWorkflowClientSuccessfully() { + setupBasicMockBehavior(); + + WorkflowClient client = factory.createWorkflowClient(); + + assertNotNull(client); + assertThat(client.getOptions().getNamespace()).isEqualTo("test-namespace"); + } + + @Test + public void shouldCacheWorkflowClientAfterFirstCreation() { + setupBasicMockBehavior(); + + WorkflowClient client1 = factory.createWorkflowClient(); + WorkflowClient client2 = factory.createWorkflowClient(); + + assertSame(client1, client2); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldHandleMissingOptionalDependenciesGracefully() { + when(mockBeanFactory.getBean(anyString(), any(Class.class))) + .thenThrow(new NoSuchBeanDefinitionException("bean")); + when(mockBeanFactory.getBean(any(Class.class))) + .thenThrow(new NoSuchBeanDefinitionException("bean")); + + WorkflowClient client = factory.createWorkflowClient(); + assertNotNull(client); + } + + private NonRootNamespaceProperties createTestNamespaceProperties() { + return new NonRootNamespaceProperties( + "testNs", "test-namespace", null, null, null, null, null, null); + } + + private TemporalProperties createTestTemporalProperties() { + ConnectionProperties connection = new ConnectionProperties("localhost:7233", null, null, null); + return new TemporalProperties(null, null, null, null, null, connection, null, null, null); + } + + @SuppressWarnings("unchecked") + private void setupBasicMockBehavior() { + when(mockBeanFactory.getBean(anyString(), any(Class.class))).thenReturn(null); + when(mockBeanFactory.getBean(any(Class.class))).thenReturn(null); + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrarTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrarTest.java new file mode 100644 index 000000000..a34b1ab0e --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NonRootNamespaceRegistrarTest.java @@ -0,0 +1,53 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.temporal.client.WorkflowClient; +import javax.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = NonRootNamespaceRegistrarTest.TestConfiguration.class) +@ActiveProfiles(profiles = "multi-namespaces") +public class NonRootNamespaceRegistrarTest { + + @Resource(name = "ns1WorkflowClient") + private WorkflowClient ns1WorkflowClient; + + @Autowired private ApplicationContext applicationContext; + + @Test + public void shouldInjectWorkflowClientUsingResource() { + assertNotNull(ns1WorkflowClient, "WorkflowClient should be injected via @Resource"); + assertThat(ns1WorkflowClient.getOptions().getNamespace()).isEqualTo("namespace1"); + } + + @Test + public void shouldRegisterBeansForMultipleNamespaces() { + assertTrue(applicationContext.containsBean("ns1WorkflowClient")); + assertTrue(applicationContext.containsBean("namespace2WorkflowClient")); + } + + @Test + public void shouldCreateDistinctBeanInstancesForDifferentNamespaces() { + WorkflowClient ns1Client = + applicationContext.getBean("ns1WorkflowClient", WorkflowClient.class); + WorkflowClient ns2Client = + applicationContext.getBean("namespace2WorkflowClient", WorkflowClient.class); + + assertThat(ns1Client).isNotSameAs(ns2Client); + assertThat(ns1Client.getOptions().getNamespace()).isEqualTo("namespace1"); + assertThat(ns2Client.getOptions().getNamespace()).isEqualTo("namespace2"); + } + + @Configuration + @EnableAutoConfiguration + static class TestConfiguration {} +}