Browse Source

Reduce bootstrap time in the situation with large properties (#3800)

shawyeok 3 năm trước cách đây
mục cha
commit
6d50ca881a

+ 1 - 0
CHANGES.md

@@ -57,6 +57,7 @@ Apollo 1.9.0
 * [speed up the stale issue mark and close phase](https://github.com/ctripcorp/apollo/pull/3808)
 * [feature: add the delegating password encoder for apollo-portal simple auth](https://github.com/ctripcorp/apollo/pull/3804)
 * [support release apollo-client-config-data](https://github.com/ctripcorp/apollo/pull/3822)
+* [Reduce bootstrap time in the situation with large properties](https://github.com/ctripcorp/apollo/pull/3816)
 ------------------
 All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/6?closed=1)
 

+ 13 - 2
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java

@@ -18,12 +18,15 @@ package com.ctrip.framework.apollo.spring.boot;
 
 import com.ctrip.framework.apollo.Config;
 import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
 import com.ctrip.framework.apollo.core.ApolloClientSystemConsts;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.core.utils.DeferredLogger;
+import com.ctrip.framework.apollo.spring.config.CachedCompositePropertySource;
 import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory;
 import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
 import com.ctrip.framework.apollo.spring.util.SpringInjector;
+import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import java.util.List;
@@ -85,11 +88,14 @@ public class ApolloApplicationContextInitializer implements
       ApolloClientSystemConsts.APOLLO_ACCESS_KEY_SECRET,
       ApolloClientSystemConsts.APOLLO_META,
       ApolloClientSystemConsts.APOLLO_CONFIG_SERVICE,
-      ApolloClientSystemConsts.APOLLO_PROPERTY_ORDER_ENABLE};
+      ApolloClientSystemConsts.APOLLO_PROPERTY_ORDER_ENABLE,
+      ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE};
 
   private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector
       .getInstance(ConfigPropertySourceFactory.class);
 
+  private final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);
+
   private int order = DEFAULT_ORDER;
 
   @Override
@@ -123,7 +129,12 @@ public class ApolloApplicationContextInitializer implements
     logger.debug("Apollo bootstrap namespaces: {}", namespaces);
     List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
 
-    CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
+    CompositePropertySource composite;
+    if (configUtil.isPropertyNamesCacheEnabled()) {
+      composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
+    } else {
+      composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
+    }
     for (String namespace : namespaceList) {
       Config config = ConfigService.getConfig(namespace);
 

+ 66 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/CachedCompositePropertySource.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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 com.ctrip.framework.apollo.ConfigChangeListener;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import org.springframework.core.env.CompositePropertySource;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * @author Shawyeok (shawyeok@outlook.com)
+ */
+public class CachedCompositePropertySource extends CompositePropertySource implements
+    ConfigChangeListener {
+
+  private volatile String[] names;
+
+  public CachedCompositePropertySource(String name) {
+    super(name);
+  }
+
+  @Override
+  public String[] getPropertyNames() {
+    String[] propertyNames = this.names;
+    if (propertyNames == null) {
+      this.names = propertyNames = super.getPropertyNames();
+    }
+    return propertyNames;
+  }
+
+  @Override
+  public void addPropertySource(PropertySource<?> propertySource) {
+    super.addPropertySource(propertySource);
+    if (propertySource instanceof ConfigPropertySource) {
+      ((ConfigPropertySource) propertySource).addChangeListener(this);
+    }
+  }
+
+  @Override
+  public void addFirstPropertySource(PropertySource<?> propertySource) {
+    super.addFirstPropertySource(propertySource);
+    if (propertySource instanceof ConfigPropertySource) {
+      ((ConfigPropertySource) propertySource).addChangeListener(this);
+    }
+  }
+
+  @Override
+  public void onChange(ConfigChangeEvent changeEvent) {
+    // clear property names cache if any sources has changed
+    this.names = null;
+  }
+}

+ 5 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/ConfigPropertySource.java

@@ -35,6 +35,11 @@ public class ConfigPropertySource extends EnumerablePropertySource<Config> {
     super(name, source);
   }
 
+  @Override
+  public boolean containsProperty(String name) {
+    return this.source.getProperty(name, null) != null;
+  }
+
   @Override
   public String[] getPropertyNames() {
     Set<String> propertyNames = this.source.getPropertyNames();

+ 6 - 1
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java

@@ -80,7 +80,12 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
       //already initialized
       return;
     }
-    CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
+    CompositePropertySource composite;
+    if (configUtil.isPropertyNamesCacheEnabled()) {
+      composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
+    } else {
+      composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
+    }
 
     //sort by order asc
     ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());

+ 26 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java

@@ -56,6 +56,7 @@ public class ConfigUtil {
   private boolean autoUpdateInjectedSpringProperties = true;
   private final RateLimiter warnLogRateLimiter;
   private boolean propertiesOrdered = false;
+  private boolean propertyNamesCacheEnabled = false;
 
   public ConfigUtil() {
     warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute
@@ -68,6 +69,7 @@ public class ConfigUtil {
     initLongPollingInitialDelayInMills();
     initAutoUpdateInjectedSpringProperties();
     initPropertiesOrdered();
+    initPropertyNamesCacheEnabled();
   }
 
   /**
@@ -394,4 +396,28 @@ public class ConfigUtil {
   public boolean isPropertiesOrderEnabled() {
     return propertiesOrdered;
   }
+
+  public boolean isPropertyNamesCacheEnabled() {
+    return propertyNamesCacheEnabled;
+  }
+
+  private void initPropertyNamesCacheEnabled() {
+    String propertyName = ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE;
+    String propertyEnvName = ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE_ENVIRONMENT_VARIABLES;
+    String enablePropertyNamesCache = System.getProperty(propertyName);
+    if (Strings.isNullOrEmpty(enablePropertyNamesCache)) {
+      enablePropertyNamesCache = System.getenv(propertyEnvName);
+    }
+    if (Strings.isNullOrEmpty(enablePropertyNamesCache)) {
+      enablePropertyNamesCache = Foundation.app().getProperty(propertyName, "false");
+    }
+    if (!Strings.isNullOrEmpty(enablePropertyNamesCache)) {
+      try {
+        propertyNamesCacheEnabled = Boolean.parseBoolean(enablePropertyNamesCache);
+      } catch (Throwable ex) {
+        logger.warn("Config for {} is invalid: {}, set default value: false",
+            propertyName, enablePropertyNamesCache);
+      }
+    }
+  }
 }

+ 8 - 2
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java

@@ -19,6 +19,7 @@ package com.ctrip.framework.apollo.spring;
 import com.ctrip.framework.apollo.Config;
 import com.ctrip.framework.apollo.ConfigChangeListener;
 import com.ctrip.framework.apollo.ConfigFileChangeListener;
+import com.ctrip.framework.apollo.core.ApolloClientSystemConsts;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.internals.SimpleConfig;
 import com.ctrip.framework.apollo.internals.YamlConfigFile;
@@ -93,6 +94,7 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     System.clearProperty(SystemPropertyKeyConstants.FROM_SYSTEM_YAML_NAMESPACE);
     System.clearProperty(SystemPropertyKeyConstants.FROM_NAMESPACE_APPLICATION_KEY);
     System.clearProperty(SystemPropertyKeyConstants.FROM_NAMESPACE_APPLICATION_KEY_YAML);
+    System.clearProperty(ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE);
     super.tearDown();
   }
 
@@ -456,13 +458,17 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     Config applicationConfig = mock(Config.class);
     mockConfig(ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
 
+    System.setProperty(ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE, "true");
+
     getSimpleBean(TestApolloConfigChangeListenerResolveExpressionSimpleConfiguration.class);
 
     // no using
     verify(ignoreConfig, never()).addChangeListener(any(ConfigChangeListener.class));
 
-    // one invocation for spring value auto update and another for the @ApolloConfigChangeListener annotation
-    verify(applicationConfig, times(2)).addChangeListener(any(ConfigChangeListener.class));
+    // one invocation for spring value auto update
+    // one invocation for the @ApolloConfigChangeListener annotation
+    // one invocation for CachedCompositePropertySource clear cache listener
+    verify(applicationConfig, times(3)).addChangeListener(any(ConfigChangeListener.class));
   }
 
   /**

+ 35 - 1
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/boot/ApolloApplicationContextInitializerTest.java

@@ -18,14 +18,20 @@ package com.ctrip.framework.apollo.spring.boot;
 
 import static org.junit.Assert.*;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.build.MockInjector;
 import com.ctrip.framework.apollo.core.ApolloClientSystemConsts;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.spring.config.CachedCompositePropertySource;
+import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
+import com.ctrip.framework.apollo.util.ConfigUtil;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MutablePropertySources;
 
 public class ApolloApplicationContextInitializerTest {
 
@@ -42,6 +48,8 @@ public class ApolloApplicationContextInitializerTest {
     System.clearProperty(ConfigConsts.APOLLO_CLUSTER_KEY);
     System.clearProperty(ApolloClientSystemConsts.APOLLO_CACHE_DIR);
     System.clearProperty(ConfigConsts.APOLLO_META_KEY);
+
+    MockInjector.reset();
   }
 
   @Test
@@ -109,4 +117,30 @@ public class ApolloApplicationContextInitializerTest {
     assertNull(System.getProperty(ApolloClientSystemConsts.APOLLO_CACHE_DIR));
     assertNull(System.getProperty(ConfigConsts.APOLLO_META_KEY));
   }
+
+  @Test
+  public void testPropertyNamesCacheEnabled() {
+    ConfigurableEnvironment environment = mock(ConfigurableEnvironment.class);
+    MutablePropertySources propertySources = new MutablePropertySources();
+    when(environment.getPropertySources()).thenReturn(propertySources);
+    when(environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES,
+        ConfigConsts.NAMESPACE_APPLICATION)).thenReturn("");
+
+    apolloApplicationContextInitializer.initialize(environment);
+
+    assertTrue(propertySources.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME));
+    assertFalse(propertySources.iterator().next() instanceof CachedCompositePropertySource);
+
+    ConfigUtil configUtil = new ConfigUtil();
+    configUtil = spy(configUtil);
+    when(configUtil.isPropertyNamesCacheEnabled()).thenReturn(true);
+    MockInjector.setInstance(ConfigUtil.class, configUtil);
+    apolloApplicationContextInitializer = new ApolloApplicationContextInitializer();
+    propertySources.remove(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
+
+    apolloApplicationContextInitializer.initialize(environment);
+
+    assertTrue(propertySources.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME));
+    assertTrue(propertySources.iterator().next() instanceof CachedCompositePropertySource);
+  }
 }

+ 122 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/config/CachedCompositePropertySourceTest.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2021 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.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.ConfigChangeListener;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import org.assertj.core.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * @author Shawyeok (shawyeok@outlook.com)
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CachedCompositePropertySourceTest {
+
+  private CachedCompositePropertySource compositeSource;
+
+  @Mock
+  private ConfigPropertySource configPropertySource;
+
+  private List<ConfigChangeListener> listeners;
+
+  @Before
+  public void setUp() throws Exception {
+    compositeSource = new CachedCompositePropertySource("testCompositeSource");
+    listeners = new LinkedList<>();
+    Mockito.doAnswer(new Answer() {
+      @Override
+      public Object answer(InvocationOnMock invocation) throws Throwable {
+        ConfigChangeListener listener = invocation.getArgumentAt(0, ConfigChangeListener.class);
+        listeners.add(listener);
+        return Void.class;
+      }
+    }).when(configPropertySource).addChangeListener(any(ConfigChangeListener.class));
+    compositeSource.addPropertySource(configPropertySource);
+  }
+
+  @Test
+  public void testGetPropertyNames() {
+    String[] propertyNames = Arrays.array("propertyName");
+    String[] anotherPropertyNames = Arrays.array("propertyName", "anotherPropertyName");
+
+    when(configPropertySource.getPropertyNames()).thenReturn(propertyNames, anotherPropertyNames);
+
+    String[] returnedPropertyNames = compositeSource.getPropertyNames();
+    assertArrayEquals(propertyNames, returnedPropertyNames);
+    assertSame(returnedPropertyNames, compositeSource.getPropertyNames());
+
+    listeners.get(0).onChange(new ConfigChangeEvent(null, null));
+
+    returnedPropertyNames = compositeSource.getPropertyNames();
+    assertArrayEquals(anotherPropertyNames, returnedPropertyNames);
+    assertSame(returnedPropertyNames, compositeSource.getPropertyNames());
+  }
+
+  @Test
+  public void testAddPropertySource() {
+    verify(configPropertySource, times(1))
+        .addChangeListener(any(CachedCompositePropertySource.class));
+    assertEquals(1, listeners.size());
+    assertTrue(compositeSource.getPropertySources().contains(configPropertySource));
+  }
+
+  @Test
+  public void testAddFirstPropertySource() {
+    ConfigPropertySource anotherSource = mock(ConfigPropertySource.class);
+    final List<ConfigChangeListener> anotherListenerList = new LinkedList<>();
+    Mockito.doAnswer(new Answer() {
+      @Override
+      public Object answer(InvocationOnMock invocation) throws Throwable {
+        ConfigChangeListener listener = invocation.getArgumentAt(0, ConfigChangeListener.class);
+        anotherListenerList.add(listener);
+        return Void.class;
+      }
+    }).when(anotherSource).addChangeListener(any(ConfigChangeListener.class));
+    compositeSource.addFirstPropertySource(anotherSource);
+
+    Collection<PropertySource<?>> propertySources = compositeSource.getPropertySources();
+    Iterator<PropertySource<?>> it = propertySources.iterator();
+
+    assertEquals(2, propertySources.size());
+    assertEquals(1, anotherListenerList.size());
+    assertSame(anotherSource, it.next());
+    assertSame(configPropertySource, it.next());
+  }
+}

+ 11 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/util/ConfigUtilTest.java

@@ -46,6 +46,7 @@ public class ConfigUtilTest {
     System.clearProperty("apollo.autoUpdateInjectedSpringProperties");
     System.clearProperty(ApolloClientSystemConsts.APOLLO_CACHE_DIR);
     System.clearProperty(PropertiesFactory.APOLLO_PROPERTY_ORDER_ENABLE);
+    System.clearProperty(ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE);
   }
 
   @Test
@@ -253,4 +254,14 @@ public class ConfigUtilTest {
     assertEquals(propertiesOrdered,
         configUtil.isPropertiesOrderEnabled());
   }
+
+  @Test
+  public void test() {
+    ConfigUtil configUtil = new ConfigUtil();
+    assertFalse(configUtil.isPropertyNamesCacheEnabled());
+
+    System.setProperty(ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE, "true");
+    configUtil = new ConfigUtil();
+    assertTrue(configUtil.isPropertyNamesCacheEnabled());
+  }
 }

+ 10 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/ApolloClientSystemConsts.java

@@ -126,4 +126,14 @@ public class ApolloClientSystemConsts {
    * enable property order environment variables
    */
   public static final String APOLLO_PROPERTY_ORDER_ENABLE_ENVIRONMENT_VARIABLES = "APOLLO_PROPERTY_ORDER_ENABLE";
+
+  /**
+   * enable property names cache
+   */
+  public static final String APOLLO_PROPERTY_NAMES_CACHE_ENABLE = "apollo.property.names.cache.enable";
+
+  /**
+   * enable property names cache environment variables
+   */
+  public static final String APOLLO_PROPERTY_NAMES_CACHE_ENABLE_ENVIRONMENT_VARIABLES = "APOLLO_PROPERTY_NAMES_CACHE_ENABLE";
 }

+ 20 - 0
docs/zh/usage/java-sdk-user-guide.md

@@ -317,6 +317,26 @@ Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验
    * 可以通过操作系统的System Environment `APOLLO_PATH_SERVER_PROPERTIES`来指定
    * 注意key为全大写,且中间是`_`分隔
 
+#### 1.2.4.6 开启`propertyNames`缓存,在大量配置场景下可以显著改善启动速度
+
+> 适用于1.9.0及以上版本
+
+在使用`@ConfigurationProperties`和存在大量配置项场景下,Spring容器的启动速度会变慢。通过开启该配置可以显著提升启动速度,当配置发生变化时缓存会自动清理,默认为`false`。详见:[issue 3800](https://github.com/ctripcorp/apollo/issues/3800)
+
+配置方式按照优先级从高到低依次为:
+1. 通过Java System Property `apollo.property.names.cache.enable`
+   * 可以通过Java的System Property `apollo.property.names.cache.enable`来指定
+   * 在Java程序启动脚本中,可以指定`-Dapollo.property.names.cache.enable=true`
+      * 如果是运行jar文件,需要注意格式是`java -Dapollo.property.names.cache.enable=true -jar xxx.jar`
+   * 也可以通过程序指定,如`System.setProperty("apollo.property.names.cache.enable", "true");`
+2. 通过系统环境变量
+   * 在启动程序前配置环境变量`APOLLO_PROPERTY_NAMES_CACHE_ENABLE=true`来指定
+   * 注意key为全大写,且中间是`_`分隔
+3. 通过Spring Boot的配置文件
+   * 可以在Spring Boot的`application.properties`或`bootstrap.properties`中指定`apollo.property.names.cache.enable=true`
+4. 通过`app.properties`配置文件
+   * 可以在`classpath:/META-INF/app.properties`指定`apollo.property.names.cache.enable=true`
+
 # 二、Maven Dependency
 Apollo的客户端jar包已经上传到中央仓库,应用在实际使用时只需要按照如下方式引入即可。
 ```xml