spring cloud config配置中心源碼分析之註解@EnableConfigServer

spring cloud config配置中心源碼分析之註解@EnableConfigServer

基於spring-cloud-config-server-2.0.2.RELEASE代碼

spring cloud config的主函數是ConfigServerApplication,其定義如下:

package com.liuwen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
     public static void main(String[] args){
         SpringApplication.run(ConfigServerApplication.class,args);
      }

}

@EnableConfigServer是spring cloud定義的註解,

@EnableConfigServer定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ConfigServerConfiguration.class})
public @interface EnableConfigServer {
}

可以看出,它引入了ConfigServerConfiguration

@Configuration
public class ConfigServerConfiguration {
    public ConfigServerConfiguration() {
    }

    @Bean
    public ConfigServerConfiguration.Marker enableConfigServerMarker() {
        return new ConfigServerConfiguration.Marker();
    }

    class Marker {
        Marker() {
        }
    }
}

ConfigServerConfiguration類裏面並沒有實現太多bean的裝配,這裏利用一種折中方式,引入需要的自動配置。請看下面的類。Marker唯一被引用的地方在ConfigServerAutoConfiguration類。

ConfigServerConfiguration 裝配了一個Marker的Bean。這個bean則有開啓了ConfigServerAutoConfiguration

@Configuration
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({ConfigServerProperties.class})
@Import({EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class})
public class ConfigServerAutoConfiguration {
    public ConfigServerAutoConfiguration() {
    }
}

這裏又引入了多個配置類,包括:

EnvironmentRepositoryConfiguration

CompositeConfiguration

ResourceRepositoryConfiguration

ConfigServerEncryptionConfiguration

ConfigServerMvcConfiguration

TransportConfiguration

接下來介紹EnvironmentRepositoryConfiguration

EnvironmentRepositoryConfiguration是配置中心的關鍵Configuration類。這個配置類中包含很多實現了EnvironmentRepository接口的類,每個實現類都對應一種類型(git/svn/navtie/vault)的配置。 EnvironmentRepositoryConfiguration通過profile註解(對當前應用的環境)決定使用裝配哪個EnvironmentRepository Bean。默認是MultipleJGitEnvironmentRepository(在內部類DefaultRepositoryConfiguration裏面有)。

@Configuration
public class EnvironmentRepositoryConfiguration {

	@Bean
	@ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true)
	public ConfigServerHealthIndicator configServerHealthIndicator(EnvironmentRepository repository) {
		return new ConfigServerHealthIndicator(repository);
	}

	@Configuration
	@ConditionalOnMissingBean(EnvironmentRepository.class)
	protected static class DefaultRepositoryConfiguration {

		@Autowired
		private ConfigurableEnvironment environment;

		@Autowired
		private ConfigServerProperties server;

		@Autowired(required = false)
		private TransportConfigCallback transportConfigCallback;

		@Bean
		public MultipleJGitEnvironmentRepository defaultEnvironmentRepository() {
			MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository(this.environment);
			repository.setTransportConfigCallback(this.transportConfigCallback);
			if (this.server.getDefaultLabel()!=null) {
				repository.setDefaultLabel(this.server.getDefaultLabel());
			}
			return repository;
		}
	}

	@Configuration
	@Profile("native")
	protected static class NativeRepositoryConfiguration {

		@Autowired
		private ConfigurableEnvironment environment;

		@Bean
		public NativeEnvironmentRepository nativeEnvironmentRepository() {
			return new NativeEnvironmentRepository(this.environment);
		}
	}

	@Configuration
	@Profile("git")
	protected static class GitRepositoryConfiguration extends DefaultRepositoryConfiguration {}

	@Configuration
	@Profile("subversion")
	protected static class SvnRepositoryConfiguration {
		@Autowired
		private ConfigurableEnvironment environment;

		@Autowired
		private ConfigServerProperties server;

		@Bean
		public SvnKitEnvironmentRepository svnKitEnvironmentRepository() {
			SvnKitEnvironmentRepository repository = new SvnKitEnvironmentRepository(this.environment);
			if (this.server.getDefaultLabel()!=null) {
				repository.setDefaultLabel(this.server.getDefaultLabel());
			}
			return repository;
		}
	}

	@Configuration
	@Profile("vault")
	protected static class VaultConfiguration {
		@Bean
		public VaultEnvironmentRepository vaultEnvironmentRepository(HttpServletRequest request, EnvironmentWatch watch) {
			return new VaultEnvironmentRepository(request, watch, new RestTemplate());
		}
	}

	@Configuration
	@ConditionalOnProperty(value = "spring.cloud.config.server.consul.watch.enabled")
	protected static class ConsulEnvironmentWatchConfiguration {

		@Bean
		public EnvironmentWatch environmentWatch() {
			return new ConsulEnvironmentWatch();
		}
	}

	@Configuration
	@ConditionalOnMissingBean(EnvironmentWatch.class)
	protected static class DefaultEnvironmentWatch {

		@Bean
		public EnvironmentWatch environmentWatch() {
			return new EnvironmentWatch.Default();
		}
	}
}

EnvironmentRepository

EnvironmentRepository是一個配置管理倉庫接口,抽象了獲取配置的方法:

Environment findOne(String application, String profile, String label);

它的實現類有很多,如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uacHPmwb-1582429220870)(C:\Users\HASEE\AppData\Roaming\Typora\typora-user-images\image-20200222174649403.png)]

從名字中大概可以看出,這些類應該是用於加載不同類型的配置(後面會再介紹)。
有了獲取配置的類,還差對外提供接口的類,就是EnvironmentController

入口:EnvironmentController

EnvironmentControllerspring-cloud-config-server包的一個controller,其他服務一般是通過這個controller獲取相應配置。

img

@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
    
	private EnvironmentRepository repository;
	private ObjectMapper objectMapper;
    
    public EnvironmentController(EnvironmentRepository repository,
			ObjectMapper objectMapper) {
		this.repository = repository;
		this.objectMapper = objectMapper;
	}
    
    // 獲取配置的接口
	...

}

它的關鍵成員變量有兩個:
一般情況Spring爲EnvironmentController注入的類是EnvironmentEncryptorEnvironmentRepository。
ObjectMapper用於當請求json格式的配置時的序列化。

EnvironmentController提供了多種獲取配置的方法,這些方法主要接受application profile label這三個(或者更少)的參數,這三個參數的具體含義可以參考官網的說明,下面列舉了部分方法:

@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
                                @PathVariable String profiles) {
    return labelled(name, profiles, null);
}

@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
                            @PathVariable String label) {
    if (label != null && label.contains("(_)")) {
        // "(_)" is uncommon in a git branch name, but "/" cannot be matched
        // by Spring MVC
        label = label.replace("(_)", "/");
    }
    Environment environment = this.repository.findOne(name, profiles, label);
    return environment;
}

我們訪問http://localhost:8081/config/mysql/dev(這是作者的配置,每個人可能不一樣), 進入defaultLabel方法,它會再調用labelled方法(由於沒有制定label參數,所以label傳了個null)。

@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
                                @PathVariable String profiles) {
    return labelled(name, profiles, null);
}

@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
                            @PathVariable String label) {
    if (label != null && label.contains("(_)")) {
        // "(_)" is uncommon in a git branch name, but "/" cannot be matched
        // by Spring MVC
        label = label.replace("(_)", "/");
    }
    // 調用`EnvironmentRepository`的findOne方法返回對應的配置
    Environment environment = this.repository.findOne(name, profiles, label);
    return environment;
}

labelled方法中,會調用EnvironmentRepository的findOne()來加載配置,然後返回給配置獲取方。

各式各樣的配置倉庫類

EnvironmentEncryptorEnvironmentRepository

前面提到spring config 通過EnvironmentEncryptorEnvironmentRepository加載配置

public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRepository {
    private EnvironmentRepository delegate;
	private EnvironmentEncryptor environmentEncryptor;

	public EnvironmentEncryptorEnvironmentRepository(EnvironmentRepository delegate,
			EnvironmentEncryptor environmentEncryptor) {
		this.delegate = delegate;
		this.environmentEncryptor = environmentEncryptor;
	}
    
    @Override
    public Environment findOne(String name, String profiles, String label) {
        Environment environment = this.delegate.findOne(name, profiles, label);
        if (this.environmentEncryptor != null) {
            environment = this.environmentEncryptor.decrypt(environment);
        }
        if (!this.overrides.isEmpty()) {
            environment.addFirst(new PropertySource("overrides", this.overrides));
        }
        return environment;
    }
}

它有一個解密器environmentEncryptor用於對加密存放的配置進行解密,另外包含一個EnvironmentRepository的實現類delegate,這裏注入的類是SearchPathCompositeEnvironmentRepository

SearchPathCompositeEnvironmentRepository

SearchPathCompositeEnvironmentRepository本身並沒有findOne()方法,由它的父類CompositeEnvironmentRepository實現。

public class SearchPathCompositeEnvironmentRepository extends CompositeEnvironmentRepository implements SearchPathLocator {
	public SearchPathCompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) {
		super(environmentRepositories);
	}
}

CompositeEnvironmentRepository

CompositeEnvironmentRepository有一個EnvironmentRepository的列表。從它的findOne()方法可以看出:當有多個配置存放方式時,CompositeEnvironmentRepository會遍歷所有EnvironmentRepository來獲取所有配置

public class CompositeEnvironmentRepository implements EnvironmentRepository {
   protected List<EnvironmentRepository> environmentRepositories;

   public CompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) {
      //Sort the environment repositories by the priority
      Collections.sort(environmentRepositories, OrderComparator.INSTANCE);
      this.environmentRepositories = environmentRepositories;
   }

   @Override
   public Environment findOne(String application, String profile, String label) {
      Environment env = new Environment(application, new String[]{profile}, label, null, null);
      if(environmentRepositories.size() == 1) {
         Environment envRepo = environmentRepositories.get(0).findOne(application, profile, label);
         env.addAll(envRepo.getPropertySources());
         env.setVersion(envRepo.getVersion());
         env.setState(envRepo.getState());
      } else {
         for (EnvironmentRepository repo : environmentRepositories) {
            env.addAll(repo.findOne(application, profile, label).getPropertySources());
         }
      }
      return env;
   }
}

小結一下:雖然實現了EnvironmentRepository接口。但EnvironmentEncryptorEnvironmentRepository只是一個代理, SearchPathCompositeEnvironmentRepository/CompositeEnvironmentRepository也沒有具體加載配置的邏輯。
而真正加載配置的類存放在CompositeEnvironmentRepository的environmentRepositories列表。
包括:
NativeEnvironmentRepository: 獲取本地配置;
SvnRepositoryConfiguration: 獲取存放在svn中的配置;
VaultEnvironmentRepository: 獲取存放在vault中的配置;
GitRepositoryConfiguration:獲取存放在git中

NativeEnvironmentRepository

NativeEnvironmentRepository 用於加載本地(native)配置。它加載配置時,其實是以特定環境(傳入的profile)啓動了另外一個微型spring boot應用,通過這個應用獲取所有的配置,然後調用clean過濾,得到所需配置。

@ConfigurationProperties("spring.cloud.config.server.native")
public class NativeEnvironmentRepository implements EnvironmentRepository, SearchPathLocator, Ordered {
	@Override
	public Environment findOne(String config, String profile, String label) {
		SpringApplicationBuilder builder = new SpringApplicationBuilder(
				PropertyPlaceholderAutoConfiguration.class);
		ConfigurableEnvironment environment = getEnvironment(profile);
		builder.environment(environment);
		builder.web(false).bannerMode(Mode.OFF);
		if (!logger.isDebugEnabled()) {
			// Make the mini-application startup less verbose
			builder.logStartupInfo(false);
		}
		String[] args = getArgs(config, profile, label);
		// Explicitly set the listeners (to exclude logging listener which would change
		// log levels in the caller)
		builder.application()
				.setListeners(Arrays.asList(new ConfigFileApplicationListener()));
		ConfigurableApplicationContext context = builder.run(args);
		environment.getPropertySources().remove("profiles");
		try {
			return clean(new PassthruEnvironmentRepository(environment).findOne(config,
					profile, label));
		}
		finally {
			context.close();
		}
	}
    
    private ConfigurableEnvironment getEnvironment(String profile) {
		ConfigurableEnvironment environment = new StandardEnvironment();
		environment.getPropertySources()
				.addFirst(new MapPropertySource("profiles",
						Collections.<String, Object>singletonMap("spring.profiles.active",
								profile)));
		return environment;
	}
    
    protected Environment clean(Environment value) {
		Environment result = new Environment(value.getName(), value.getProfiles(),
				value.getLabel(), this.version, value.getState());
		for (PropertySource source : value.getPropertySources()) {
			String name = source.getName();
			if (this.environment.getPropertySources().contains(name)) {
				continue;
			}
			name = name.replace("applicationConfig: [", "");
			name = name.replace("]", "");
			if (this.searchLocations != null) {
				boolean matches = false;
				String normal = name;
				if (normal.startsWith("file:")) {
					normal = StringUtils
							.cleanPath(new File(normal.substring("file:".length()))
									.getAbsolutePath());
				}
				String profile = result.getProfiles() == null ? null
						: StringUtils.arrayToCommaDelimitedString(result.getProfiles());
				for (String pattern : getLocations(result.getName(), profile,
						result.getLabel()).getLocations()) {
					if (!pattern.contains(":")) {
						pattern = "file:" + pattern;
					}
					if (pattern.startsWith("file:")) {
						pattern = StringUtils
								.cleanPath(new File(pattern.substring("file:".length()))
										.getAbsolutePath())
								+ "/";
					}
					if (logger.isTraceEnabled()) {
						logger.trace("Testing pattern: " + pattern
								+ " with property source: " + name);
					}
					if (normal.startsWith(pattern)
							&& !normal.substring(pattern.length()).contains("/")) {
						matches = true;
						break;
					}
				}
				if (!matches) {
					// Don't include this one: it wasn't matched by our search locations
					if (logger.isDebugEnabled()) {
						logger.debug("Not adding property source: " + name);
					}
					continue;
				}
			}
			logger.info("Adding property source: " + name);
			result.add(new PropertySource(name, source.getSource()));
		}
		return result;
	}
}

嘗試自定義EnvironmentRepository 實現

在上面的分析可以知道,所有的配置EnvironmentRepository的Configuration都是在沒有EnvironmentRepository的bean的時候纔會生效,我們可以實現自定義的EnvironmentRepository的bean,然後就可以覆蓋的系統的實現。代碼如下。

@SpringBootApplication
@EnableConfigServer
public class SpringCloudDefineConfigServer {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudDefineConfigServer.class, args);
    }

    @Bean
    public EnvironmentRepository newEnvironmentRepository(){
        return new EnvironmentRepository() {
            @Override
            public Environment findOne(String application, String profile, String label) {
                Environment environment =new Environment(application, profile);
                List<PropertySource> propertySourceList = environment.getPropertySources();
                Map<String, String> map = new HashMap<>();
                map.put("name", "garine-define");
                PropertySource propertySource = new PropertySource("map", map);
                propertySourceList.add(propertySource);
                return environment;
            }
        };
    }
}

spring cloud config client實現原理分析

getRemoteEnvironment

前面說到,調用EnvironmentController接口返回的是Environment的json串,那麼client這邊反序列化應該也是Environment,搜索spring-cloud-config-client包使用Environment的地方,發現這個方法。

org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#getRemoteEnvironment,目測就是獲取遠程服務器配置的地方。代碼如下:

private Environment getRemoteEnvironment(RestTemplate restTemplate,
      ConfigClientProperties properties, String label, String state) {
   String path = "/{name}/{profile}";
   String name = properties.getName();
   String profile = properties.getProfile();
   String token = properties.getToken();
   int noOfUrls = properties.getUri().length;
   if (noOfUrls > 1) {
      logger.info("Multiple Config Server Urls found listed.");
   }

   Object[] args = new String[] { name, profile };
   if (StringUtils.hasText(label)) {
      if (label.contains("/")) {
         label = label.replace("/", "(_)");
      }
      args = new String[] { name, profile, label };
      path = path + "/{label}";
   }
   ResponseEntity<Environment> response = null;

   for (int i = 0; i < noOfUrls; i++) {
      Credentials credentials = properties.getCredentials(i);
      String uri = credentials.getUri();
      String username = credentials.getUsername();
      String password = credentials.getPassword();

      logger.info("Fetching config from server at : " + uri);

      try {
         HttpHeaders headers = new HttpHeaders();
         addAuthorizationToken(properties, headers, username, password);
         if (StringUtils.hasText(token)) {
            headers.add(TOKEN_HEADER, token);
         }
         if (StringUtils.hasText(state) && properties.isSendState()) {
            headers.add(STATE_HEADER, state);
         }

         final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
         response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
               Environment.class, args);
      }
      catch (HttpClientErrorException e) {
         if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
            throw e;
         }
      }
      catch (ResourceAccessException e) {
         logger.info("Connect Timeout Exception on Url - " + uri
               + ". Will be trying the next url if available");
         if (i == noOfUrls - 1)
            throw e;
         else
            continue;
      }

      if (response == null || response.getStatusCode() != HttpStatus.OK) {
         return null;
      }

      Environment result = response.getBody();
      return result;
   }

   return null;
}

上面的代碼主要操作就是拼接一個請求配置地址串,獲取所需的ApplicationName,profile,label參數,利用RestTemplate執行http請求,返回的json反序列化爲Environment,從而獲得所需要的配置信息。

那麼問題來了,client是在什麼時候調用getRemoteEnvironment方法的,推測應該是在boostrap context進行初始化階段。在getRemoteEnvironment打個斷點,重啓client程序,可以查看到以下調用鏈路。

  • org.springframework.boot.SpringApplication#run(java.lang.String…)
    • org.springframework.boot.SpringApplication#prepareContext
    • org.springframework.boot.SpringApplication#applyInitializers
      • org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#initialize
      • org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#locate
      • org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#getRemoteEnvironment

所以,可以知道在spring啓動時就會遠程加載配置信息,SpringApplication#applyInitializers代碼如下,會遍歷所有initializer進行一遍操作,PropertySourceBootstrapConfiguration就是其中之一的initializer。

protected void applyInitializers(ConfigurableApplicationContext context) {
   for (ApplicationContextInitializer initializer : getInitializers()) {
      Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
            initializer.getClass(), ApplicationContextInitializer.class);
      Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
      initializer.initialize(context);
   }
}

當引入了spring-cloud-config後PropertySourceBootstrapConfiguration#propertySourceLocators中會新增一個ConfigServicePropertySourceLocator實例。在PropertySourceBootstrapConfiguration#initialize中遍歷propertySourceLocators的locate方法,然後讀取遠程服務配置信息;如果沒有引入了spring-cloud-config,那麼propertySourceLocators將會是空集合。代碼如下。

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
   CompositePropertySource composite = new CompositePropertySource(
         BOOTSTRAP_PROPERTY_SOURCE_NAME);
   AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
   boolean empty = true;
   ConfigurableEnvironment environment = applicationContext.getEnvironment();
   for (PropertySourceLocator locator : this.propertySourceLocators) {
      PropertySource<?> source = null;
      source = locator.locate(environment);
      if (source == null) {
         continue;
      }
      logger.info("Located property source: " + source);
      composite.addPropertySource(source);
      empty = false;
   }
   if (!empty) {
      MutablePropertySources propertySources = environment.getPropertySources();
      String logConfig = environment.resolvePlaceholders("${logging.config:}");
      LogFile logFile = LogFile.get(environment);
      if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
         propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
      }
      insertPropertySources(propertySources, composite);
      reinitializeLoggingSystem(environment, logConfig, logFile);
      setLogLevels(applicationContext, environment);
      handleIncludedProfiles(environment);
   }
}

PropertySourceBootstrapConfiguration#propertySourceLocators初始化

@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

上面的代碼可以看出,這裏的propertySourceLocators是直接注入上下文中管理的PropertySourceLocator實例,所以PropertySourceLocator一定有別的地方初始化。

搜索ConfigServicePropertySourceLocator的使用處,發現org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration#configServicePropertySource方法裝配了一個ConfigServicePropertySourceLocator的bean,代碼如下。

@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {

@Bean
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
   ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
         properties);
   return locator;
}
   //........ 
}

org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration是config client的類,當引入了spring cloud config時引入,再嘗試搜索使用處,發現在spring cloud config client包裏面的spring.factories裏面引入了ConfigServiceBootstrapConfiguration,熟悉spring boot自動裝配的都知道,程序會自動加載spring.factories裏面的配置類。

也就是說,當引入了spring cloud config client包,就會自動加載ConfigServiceBootstrapConfiguration類,自動裝配ConfigServiceBootstrapConfiguration裏面配置的bean,也就自動實例化一個ConfigServicePropertySourceLocator。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.config.client.ConfigClientAutoConfiguration

# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration,\
org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章