From e33eb583a6f794fe01efef7073a4430e20047ab3 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Sat, 9 Apr 2022 20:01:25 +0800 Subject: [PATCH] use spring application event to broadcast the config change event --- .../config/PropertySourcesProcessor.java | 34 +++-- .../AutoUpdateConfigChangeListener.java | 50 +++++-- .../spring/spi/ApolloConfigChangeEvent.java | 34 +++++ .../DefaultApolloConfigRegistrarHelper.java | 3 + ...tConfigPropertySourcesProcessorHelper.java | 3 + .../config/PropertySourcesProcessorTest.java | 125 ++++++++++++++++++ 6 files changed, 222 insertions(+), 27 deletions(-) create mode 100644 apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/ApolloConfigChangeEvent.java create mode 100644 apollo-client/src/test/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessorTest.java diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java index c4eef96310b..f8abc16993d 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java @@ -16,33 +16,33 @@ */ package com.ctrip.framework.apollo.spring.config; +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.ConfigService; import com.ctrip.framework.apollo.build.ApolloInjector; -import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener; +import com.ctrip.framework.apollo.spring.spi.ApolloConfigChangeEvent; import com.ctrip.framework.apollo.spring.util.SpringInjector; import com.ctrip.framework.apollo.util.ConfigUtil; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; - -import com.ctrip.framework.apollo.Config; -import com.ctrip.framework.apollo.ConfigService; - import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Set; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.EnvironmentAware; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; - -import java.util.Collection; -import java.util.Iterator; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; @@ -56,7 +56,8 @@ * * @author Jason Song(song_s@ctrip.com) */ -public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered { +public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, + ApplicationEventPublisherAware, PriorityOrdered { private static final Multimap NAMESPACE_NAMES = LinkedHashMultimap.create(); private static final Set AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES = Sets.newConcurrentHashSet(); @@ -64,6 +65,7 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir .getInstance(ConfigPropertySourceFactory.class); private ConfigUtil configUtil; private ConfigurableEnvironment environment; + private ApplicationEventPublisher applicationEventPublisher; public static boolean addNamespaces(Collection namespaces, int order) { return NAMESPACE_NAMES.putAll(order, namespaces); @@ -134,17 +136,16 @@ private void ensureBootstrapPropertyPrecedence(ConfigurableEnvironment environme } private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) { - if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() || - !AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) { + if (!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) { return; } - AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener( - environment, beanFactory); + ConfigChangeListener configChangeEventPublisher = changeEvent -> + applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent)); List configPropertySources = configPropertySourceFactory.getAllConfigPropertySources(); for (ConfigPropertySource configPropertySource : configPropertySources) { - configPropertySource.addChangeListener(autoUpdateConfigChangeListener); + configPropertySource.addChangeListener(configChangeEventPublisher); } } @@ -165,4 +166,9 @@ static void reset() { NAMESPACE_NAMES.clear(); AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.clear(); } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/AutoUpdateConfigChangeListener.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/AutoUpdateConfigChangeListener.java index ffeeefb3c36..0fe95f8f200 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/AutoUpdateConfigChangeListener.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/AutoUpdateConfigChangeListener.java @@ -17,8 +17,11 @@ package com.ctrip.framework.apollo.spring.property; import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.build.ApolloInjector; import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.spi.ApolloConfigChangeEvent; import com.ctrip.framework.apollo.spring.util.SpringInjector; +import com.ctrip.framework.apollo.util.ConfigUtil; import com.google.gson.Gson; import java.lang.reflect.Field; import java.lang.reflect.Type; @@ -26,34 +29,38 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.core.env.Environment; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.util.CollectionUtils; /** * Create by zhangzheng on 2018/3/6 */ -public class AutoUpdateConfigChangeListener implements ConfigChangeListener{ - private static final Logger logger = LoggerFactory.getLogger(AutoUpdateConfigChangeListener.class); +public class AutoUpdateConfigChangeListener implements ConfigChangeListener, + ApplicationListener, ApplicationContextAware { + + private static final Logger logger = LoggerFactory.getLogger( + AutoUpdateConfigChangeListener.class); private final boolean typeConverterHasConvertIfNecessaryWithFieldParameter; - private final Environment environment; - private final ConfigurableBeanFactory beanFactory; - private final TypeConverter typeConverter; + private ConfigurableBeanFactory beanFactory; + private TypeConverter typeConverter; private final PlaceholderHelper placeholderHelper; private final SpringValueRegistry springValueRegistry; private final Gson gson; + private final ConfigUtil configUtil; - public AutoUpdateConfigChangeListener(Environment environment, ConfigurableListableBeanFactory beanFactory){ + public AutoUpdateConfigChangeListener() { this.typeConverterHasConvertIfNecessaryWithFieldParameter = testTypeConverterHasConvertIfNecessaryWithFieldParameter(); - this.beanFactory = beanFactory; - this.typeConverter = this.beanFactory.getTypeConverter(); - this.environment = environment; this.placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class); this.springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class); this.gson = new Gson(); + this.configUtil = ApolloInjector.getInstance(ConfigUtil.class); } @Override @@ -90,7 +97,9 @@ private void updateSpringValue(SpringValue springValue) { /** * Logic transplanted from DefaultListableBeanFactory - * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency(org.springframework.beans.factory.config.DependencyDescriptor, java.lang.String, java.util.Set, org.springframework.beans.TypeConverter) + * + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency(org.springframework.beans.factory.config.DependencyDescriptor, + * java.lang.String, java.util.Set, org.springframework.beans.TypeConverter) */ private Object resolvePropertyValue(SpringValue springValue) { // value will never be null, as @Value and @ApolloJsonValue will not allow that @@ -98,7 +107,7 @@ private Object resolvePropertyValue(SpringValue springValue) { .resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder()); if (springValue.isJson()) { - value = parseJsonValue((String)value, springValue.getGenericType()); + value = parseJsonValue((String) value, springValue.getGenericType()); } else { if (springValue.isField()) { // org.springframework.beans.TypeConverter#convertIfNecessary(java.lang.Object, java.lang.Class, java.lang.reflect.Field) is available from Spring 3.2.0+ @@ -135,4 +144,19 @@ private boolean testTypeConverterHasConvertIfNecessaryWithFieldParameter() { return true; } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + //it is safe enough to cast as all known application context is derived from ConfigurableApplicationContext + this.beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); + this.typeConverter = this.beanFactory.getTypeConverter(); + } + + @Override + public void onApplicationEvent(ApolloConfigChangeEvent event) { + if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { + return; + } + this.onChange(event.getConfigChangeEvent()); + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/ApolloConfigChangeEvent.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/ApolloConfigChangeEvent.java new file mode 100644 index 00000000000..f2c9e88025c --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/ApolloConfigChangeEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.spring.spi; + +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import org.springframework.context.ApplicationEvent; + +/** + * A Spring Application Event that is fired when Apollo config changes. + */ +public class ApolloConfigChangeEvent extends ApplicationEvent { + + public ApolloConfigChangeEvent(ConfigChangeEvent source) { + super(source); + } + + public ConfigChangeEvent getConfigChangeEvent() { + return (ConfigChangeEvent) getSource(); + } +} diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultApolloConfigRegistrarHelper.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultApolloConfigRegistrarHelper.java index 9214f444af7..ec8af35073c 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultApolloConfigRegistrarHelper.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultApolloConfigRegistrarHelper.java @@ -21,6 +21,7 @@ import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor; import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor; +import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener; import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor; import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; import com.google.common.collect.Lists; @@ -55,6 +56,8 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(), PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, + AutoUpdateConfigChangeListener.class.getName(), AutoUpdateConfigChangeListener.class); BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(), PropertySourcesProcessor.class); BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(), diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultConfigPropertySourcesProcessorHelper.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultConfigPropertySourcesProcessorHelper.java index 42e63e7c0f6..a04f5ae7678 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultConfigPropertySourcesProcessorHelper.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/spi/DefaultConfigPropertySourcesProcessorHelper.java @@ -19,6 +19,7 @@ import com.ctrip.framework.apollo.core.spi.Ordered; import com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor; import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor; +import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener; import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor; import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; import java.util.HashMap; @@ -37,6 +38,8 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(), PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, + AutoUpdateConfigChangeListener.class.getName(), AutoUpdateConfigChangeListener.class); BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(), ApolloAnnotationProcessor.class); BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessorTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessorTest.java new file mode 100644 index 00000000000..538beee4fda --- /dev/null +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessorTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.spring.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.AbstractSpringIntegrationTest; +import com.ctrip.framework.apollo.spring.spi.ApolloConfigChangeEvent; +import com.google.common.collect.Lists; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; + +public class PropertySourcesProcessorTest extends AbstractSpringIntegrationTest { + + private ConfigurableEnvironment environment; + private ConfigurableListableBeanFactory beanFactory; + private PropertySourcesProcessor processor; + private MutablePropertySources propertySources; + private ApplicationEventPublisher applicationEventPublisher; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + propertySources = mock(MutablePropertySources.class); + environment = mock(ConfigurableEnvironment.class); + when(environment.getPropertySources()).thenReturn(propertySources); + beanFactory = mock(ConfigurableListableBeanFactory.class); + applicationEventPublisher = mock(ApplicationEventPublisher.class); + processor = new PropertySourcesProcessor(); + processor.setEnvironment(environment); + processor.setApplicationEventPublisher(applicationEventPublisher); + } + + @Override + @After + public void tearDown() throws Exception { + super.tearDown(); + PropertySourcesProcessor.reset(); + } + + @Test + public void testInitializePropertySources() { + String namespaceName = "someNamespace"; + String anotherNamespaceName = "anotherNamespace"; + Config config = mock(Config.class); + Config anotherConfig = mock(Config.class); + mockConfig(namespaceName, config); + mockConfig(anotherNamespaceName, anotherConfig); + PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaceName, anotherNamespaceName), + 0); + + processor.postProcessBeanFactory(beanFactory); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass( + CompositePropertySource.class); + verify(propertySources).addFirst(argumentCaptor.capture()); + + CompositePropertySource compositePropertySource = argumentCaptor.getValue(); + assertEquals(2, compositePropertySource.getPropertySources().size()); + + ConfigPropertySource propertySource = (ConfigPropertySource) Lists.newArrayList( + compositePropertySource.getPropertySources()).get(0); + ConfigPropertySource anotherPropertySource = (ConfigPropertySource) Lists.newArrayList( + compositePropertySource.getPropertySources()).get(1); + + assertEquals(namespaceName, propertySource.getName()); + assertSame(config, propertySource.getSource()); + assertEquals(anotherNamespaceName, anotherPropertySource.getName()); + assertSame(anotherConfig, anotherPropertySource.getSource()); + } + + @Test + public void testApplicationEvent() { + String namespaceName = "someNamespace"; + Config config = mock(Config.class); + mockConfig(namespaceName, config); + PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaceName), 0); + ConfigChangeEvent someConfigChangeEvent = mock(ConfigChangeEvent.class); + + processor.postProcessBeanFactory(beanFactory); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass( + ConfigChangeListener.class); + verify(config).addChangeListener(argumentCaptor.capture()); + + ConfigChangeListener listener = argumentCaptor.getValue(); + listener.onChange(someConfigChangeEvent); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass( + ApolloConfigChangeEvent.class); + verify(applicationEventPublisher).publishEvent(eventCaptor.capture()); + + ApolloConfigChangeEvent event = eventCaptor.getValue(); + assertSame(someConfigChangeEvent, event.getConfigChangeEvent()); + } +}