深入理解SpringCloud之分布式配置

  Spring Cloud Config Server能够统一管理配置,我们绝大多数情况都是基于git或者svn作为其配置仓库,其实SpringCloud还可以把数据库作为配置仓库,今天我们就来了解一下。顺便分析一下其实现原理。

一、PropertySourceLocator接口

1.1、代码分析

  这个接口的作用用于定制化引导配置,通过这个接口我们可以通过代码动态的向Environment中添加PropertySource,该接口定义如下:

/*
 * Copyright 2013-2014 the original author or 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 org.springframework.cloud.bootstrap.config;

import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;

/**
 * Strategy for locating (possibly remote) property sources for the Environment.
 * Implementations should not fail unless they intend to prevent the application from
 * starting.
 * 
 * @author Dave Syer
 *
 */
public interface PropertySourceLocator {

    /**
     * @param environment the current Environment
     * @return a PropertySource or null if there is none
     * 
     * @throws IllegalStateException if there is a fail fast condition
     */
    PropertySource<?> locate(Environment environment);

}
View Code

  那么此接口在SpringCloud类引导类PropertySourceBootstrapConfiguration里有处理,核心代码如下:

@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
        ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

      @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);
        }
    }

//.....


  private void insertPropertySources(MutablePropertySources propertySources,
            CompositePropertySource composite) {
        MutablePropertySources incoming = new MutablePropertySources();
        incoming.addFirst(composite);
        PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
        new RelaxedDataBinder(remoteProperties, "spring.cloud.config")
                .bind(new PropertySourcesPropertyValues(incoming));
        if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
                && remoteProperties.isOverrideSystemProperties())) {
            propertySources.addFirst(composite);
            return;
        }
        if (remoteProperties.isOverrideNone()) {
            propertySources.addLast(composite);
            return;
        }
        if (propertySources
                .contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
            if (!remoteProperties.isOverrideSystemProperties()) {
                propertySources.addAfter(
                        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                        composite);
            }
            else {
                propertySources.addBefore(
                        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                        composite);
            }
        }
        else {
            propertySources.addLast(composite);
        }
    }
    
    

}

  在这里我们可以清楚的看到,首先会获取所有的PropertySourceLocator,并调用其locate方法,只有当propertySouceLocator有实现类时,它才会获取当前引导上下文的Environment,并在 insertPropertySources方法里,把PropertySourceLocator的自定义属性值添加到引导上下文的环境当中。

1.2、代码示例

代码目录结构如下:

在这里注意,自定义实现的PropertySourceLocator是我们的引导程序,因此一定不能被主程序componentScan到

MyTestPropertySourceLocator代码如下:

package com.bdqn.lyrk.config.bootstrap;

import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class MyTestPropertySourceLocator implements PropertySourceLocator {
    @Override
    public PropertySource<?> locate(Environment environment) {
        Map<String, Object> propertySource = new HashMap<>();
        propertySource.put("student.name", "admin");
        MapPropertySource mapPropertySource = new MapPropertySource("customer", propertySource);

        return mapPropertySource;
    }
}
View Code

spring.factories文件:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.bdqn.lyrk.config.bootstrap.MyTestPropertySourceLocator
View Code

ConfigServer:

package com.bdqn.lyrk.config.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;

@SpringBootApplication
@EnableConfigServer
public class ConfigServer {


    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(ConfigServer.class, args);
        Environment environment = applicationContext.getBean(Environment.class);
        System.out.println(environment);
        System.out.println(environment.getProperty("student.name"));
    }
}
View Code

运行结果如下:

 我们可以看到,当我们把自定义的PropertySourceLocator作为引导程序配置时,该接口的locate方法返回值会添加到Environment当中

二、ConfigServer

  ConfigServer是配置中心的服务端,它负责统一管理配置,当我们以http://地址:端口号/{application}-{profile}.properties发送请求时会被EnvironmentController处理,我们来看一下EnvironmentController的源码:

@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {

    public EnvironmentController(EnvironmentRepository repository) {
        this(repository, new ObjectMapper());
    }

    public EnvironmentController(EnvironmentRepository repository,
            ObjectMapper objectMapper) {
        this.repository = repository;
        this.objectMapper = objectMapper;
    }

    @RequestMapping("/{name}/{profiles}/{label:.*}")
    public Environment labelled(@PathVariable String name, @PathVariable String profiles,
            @PathVariable String label) {
        if (name != null && name.contains("(_)")) {
            // "(_)" is uncommon in a git repo name, but "/" cannot be matched
            // by Spring MVC
            name = name.replace("(_)", "/");
        }
        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;
    }

    @RequestMapping("/{name}-{profiles}.properties")
    public ResponseEntity<String> properties(@PathVariable String name,
            @PathVariable String profiles,
            @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
            throws IOException {
        return labelledProperties(name, profiles, null, resolvePlaceholders);
    }

    @RequestMapping("/{label}/{name}-{profiles}.properties")
    public ResponseEntity<String> labelledProperties(@PathVariable String name,
            @PathVariable String profiles, @PathVariable String label,
            @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
            throws IOException {
        validateProfiles(profiles);
        Environment environment = labelled(name, profiles, label);
        Map<String, Object> properties = convertToProperties(environment);
        String propertiesString = getPropertiesString(properties);
        if (resolvePlaceholders) {
            propertiesString = resolvePlaceholders(prepareEnvironment(environment),
                    propertiesString);
        }
        return getSuccess(propertiesString);
    }

// .....省略其他代码
}

  在这里的核心代码是labelled,该方法首先会解析(_)将其替换为/ ,然后调用的EnvironmentRepository的findOne方法。

/*
 * Copyright 2013-2015 the original author or 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 org.springframework.cloud.config.server.environment;

import org.springframework.cloud.config.environment.Environment;

/**
 * @author Dave Syer
 * @author Roy Clarkson
 */
public interface EnvironmentRepository {

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

}
View Code

  此接口主要是根据application profiles label这三个参数拿到对应的Environment 注意这里的Environment不是Springframework下的Environment接口

/*
 * Copyright 2013-2015 the original author or 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 org.springframework.cloud.config.environment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * Simple plain text serializable encapsulation of a list of property sources. Basically a
 * DTO for {@link org.springframework.core.env.Environment}, but also applicable outside
 * the domain of a Spring application.
 *
 * @author Dave Syer
 * @author Spencer Gibb
 *
 */
public class Environment {

    private String name;

    private String[] profiles = new String[0];

    private String label;

    private List<PropertySource> propertySources = new ArrayList<>();

    private String version;

    private String state;

    public Environment(String name, String... profiles) {
        this(name, profiles, "master", null, null);
    }

    /**
     * Copies all fields except propertySources
     * @param env
     */
    public Environment(Environment env) {
        this(env.getName(), env.getProfiles(), env.getLabel(), env.getVersion(), env.getState());
    }

    @JsonCreator
    public Environment(@JsonProperty("name") String name,
            @JsonProperty("profiles") String[] profiles,
            @JsonProperty("label") String label,
            @JsonProperty("version") String version,
            @JsonProperty("state") String state) {
        super();
        this.name = name;
        this.profiles = profiles;
        this.label = label;
        this.version = version;
        this.state = state;
    }

    public void add(PropertySource propertySource) {
        this.propertySources.add(propertySource);
    }

    public void addAll(List<PropertySource> propertySources) {
        this.propertySources.addAll(propertySources);
    }

    public void addFirst(PropertySource propertySource) {
        this.propertySources.add(0, propertySource);
    }

    public List<PropertySource> getPropertySources() {
        return propertySources;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public String[] getProfiles() {
        return profiles;
    }

    public void setProfiles(String[] profiles) {
        this.profiles = profiles;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    @Override
    public String toString() {
        return "Environment [name=" + name + ", profiles=" + Arrays.asList(profiles)
                + ", label=" + label + ", propertySources=" + propertySources
                + ", version=" + version
                + ", state=" + state + "]";
    }

}
View Code

  SpringCloud中的Environment类与Springframework的Environment接口相仿,前者中的属性终将会添加至后者当中,下面我们可以看一下它是怎么实现的

  首先我们下找到spring-cloud-config-server-xxx.jar下的spring.factories文件:

# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.server.bootstrap.ConfigServerBootstrapConfiguration,\
org.springframework.cloud.config.server.config.EncryptionAutoConfiguration

# Application listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.config.server.bootstrap.ConfigServerBootstrapApplicationListener

# Autoconfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration,\
org.springframework.cloud.config.server.config.EncryptionAutoConfiguration
View Code

  我们可以看到,此处配置了引导类有一个叫:ConfigServerBootstrapConfiguration,我们不妨看看这个引导类:

/*
 * Copyright 2013-2015 the original author or 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 org.springframework.cloud.config.server.bootstrap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.server.config.ConfigServerProperties;
import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration;
import org.springframework.cloud.config.server.config.TransportConfiguration;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.environment.EnvironmentRepositoryPropertySourceLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;

/**
 * Bootstrap configuration to fetch external configuration from a (possibly
 * remote) {@link EnvironmentRepository}. Off by default because it can delay
 * startup, but can be enabled with
 * <code>spring.cloud.config.server.bootstrap=true</code>. This would be useful,
 * for example, if the config server were embedded in another app that wanted to
 * be configured from the same repository as all the other clients.
 *
 * @author Dave Syer
 * @author Roy Clarkson
 */
@Configuration
@ConditionalOnProperty("spring.cloud.config.server.bootstrap")
public class ConfigServerBootstrapConfiguration {

	@EnableConfigurationProperties(ConfigServerProperties.class)
	@Import({ EnvironmentRepositoryConfiguration.class, TransportConfiguration.class })
	protected static class LocalPropertySourceLocatorConfiguration {

		@Autowired
		private EnvironmentRepository repository;

		@Autowired
		private ConfigClientProperties client;

		@Autowired
		private ConfigServerProperties server;

		@Bean
		public EnvironmentRepositoryPropertySourceLocator environmentRepositoryPropertySourceLocator() {
			return new EnvironmentRepositoryPropertySourceLocator(this.repository, this.client.getName(),
					this.client.getProfile(), getDefaultLabel());
		}

		private String getDefaultLabel() {
			if (StringUtils.hasText(this.client.getLabel())) {
				return this.client.getLabel();
			} else if (StringUtils.hasText(this.server.getDefaultLabel())) {
				return this.server.getDefaultLabel();
			}
			return null;
		}

	}

}
View Code

  该引导中装配了一个EnvironmentRepositoryPropertySourceLocator的类,我们继续看看这个类:

/*
 * Copyright 2013-2014 the original author or 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 org.springframework.cloud.config.server.environment;

import java.util.Map;

import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;

/**
 * A PropertySourceLocator that reads from an EnvironmentRepository.
 * 
 * @author Dave Syer
 *
 */
public class EnvironmentRepositoryPropertySourceLocator implements PropertySourceLocator {

    private EnvironmentRepository repository;
    private String name;
    private String profiles;
    private String label;

    public EnvironmentRepositoryPropertySourceLocator(EnvironmentRepository repository,
            String name, String profiles, String label) {
        this.repository = repository;
        this.name = name;
        this.profiles = profiles;
        this.label = label;
    }

    @Override
    public org.springframework.core.env.PropertySource<?> locate(Environment environment) {
        CompositePropertySource composite = new CompositePropertySource("configService");
        for (PropertySource source : repository.findOne(name, profiles, label)
                .getPropertySources()) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) source.getSource();
            composite.addPropertySource(new MapPropertySource(source.getName(), map));
        }
        return composite;
    }

}
View Code

  这个类很明显实现了PropertySourceLocator接口,在locate方法里会调用EnvironmentRepository的findOne方法,此时会将SpringCloud的Environment类和Spring中的Environment相关联

三、config-client

  当我们添加config-client时,启动时会去服务端请求远程的配置进而加载至当前的Environment当中。我们先看一看它的spring.factories文件:

# 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
View Code

  根据引导配置,我们去追溯一下ConfigServiceBootstrapConfiguration的源代码:

/*
 * Copyright 2013-2017 the original author or 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 org.springframework.cloud.config.client;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;

/**
 * @author Dave Syer
 * @author Tristan Hanson
 *
 */
@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {

	@Autowired
	private ConfigurableEnvironment environment;

	@Bean
	public ConfigClientProperties configClientProperties() {
		ConfigClientProperties client = new ConfigClientProperties(this.environment);
		return client;
	}

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

	@ConditionalOnProperty(value = "spring.cloud.config.failFast", matchIfMissing=false)
	@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
	@Configuration
	@EnableRetry(proxyTargetClass = true)
	@Import(AopAutoConfiguration.class)
	@EnableConfigurationProperties(RetryProperties.class)
	protected static class RetryConfiguration {

		@Bean
		@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
		public RetryOperationsInterceptor configServerRetryInterceptor(
				RetryProperties properties) {
			return RetryInterceptorBuilder
					.stateless()
					.backOffOptions(properties.getInitialInterval(),
							properties.getMultiplier(), properties.getMaxInterval())
					.maxAttempts(properties.getMaxAttempts()).build();
		}
	}

}
View Code

  与config-server端类似,我们可以发现其装配了一个ConfigServicePropertySourceLocator的Bean,这里我贴出关键代码部分:

@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {

private static Log logger = LogFactory
            .getLog(ConfigServicePropertySourceLocator.class);

    private RestTemplate restTemplate;
    private ConfigClientProperties defaultProperties;

    public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
        this.defaultProperties = defaultProperties;
    }

    @Override
    @Retryable(interceptor = "configServerRetryInterceptor")
    public org.springframework.core.env.PropertySource<?> locate(
            org.springframework.core.env.Environment environment) {
        ConfigClientProperties properties = this.defaultProperties.override(environment);
        CompositePropertySource composite = new CompositePropertySource("configService");
        RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties)
                : this.restTemplate;
        Exception error = null;
        String errorBody = null;
        logger.info("Fetching config from server at: " + properties.getRawUri());
        try {
            String[] labels = new String[] { "" };
            if (StringUtils.hasText(properties.getLabel())) {
                labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
            }

            String state = ConfigClientStateHolder.getState();

            // Try all the labels until one works
            for (String label : labels) {
                Environment result = getRemoteEnvironment(restTemplate,
                        properties, label.trim(), state);
                if (result != null) {
                    logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
                            result.getName(),
                            result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()),
                            result.getLabel(), result.getVersion(), result.getState()));

                    if (result.getPropertySources() != null) { // result.getPropertySources() can be null if using xml
                        for (PropertySource source : result.getPropertySources()) {
                            @SuppressWarnings("unchecked")
                            Map<String, Object> map = (Map<String, Object>) source
                                    .getSource();
                            composite.addPropertySource(new MapPropertySource(source
                                    .getName(), map));
                        }
                    }

                    if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
                        HashMap<String, Object> map = new HashMap<>();
                        putValue(map, "config.client.state", result.getState());
                        putValue(map, "config.client.version", result.getVersion());
                        composite.addFirstPropertySource(new MapPropertySource("configClient", map));
                    }
                    return composite;
                }
            }
        }
        catch (HttpServerErrorException e) {
            error = e;
            if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders()
                    .getContentType())) {
                errorBody = e.getResponseBodyAsString();
            }
        }
        catch (Exception e) {
            error = e;
        }
        if (properties.isFailFast()) {
            throw new IllegalStateException(
                    "Could not locate PropertySource and the fail fast property is set, failing",
                    error);
        }
        logger.warn("Could not locate PropertySource: "
                + (errorBody == null ? error==null ? "label not found" : error.getMessage() : errorBody));
        return null;

    }

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();
        String uri = properties.getRawUri();

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

        try {
            HttpHeaders headers = new HttpHeaders();
            if (StringUtils.hasText(token)) {
                headers.add(TOKEN_HEADER, token);
            }
            if (StringUtils.hasText(state)) { //TODO: opt in to sending state?
                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;
            }
        }

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

//。。。省略其他代码
}

  在这里我们可以发现,当client端启动时,通过RestTemplate请求服务端的EnvironmentController进而添加至当前的Environment

三、使用数据库作为配置中心的仓库

  我们先看一下自动装配类:

@Configuration
@Profile("jdbc")
class JdbcRepositoryConfiguration {
    @Bean
    public JdbcEnvironmentRepository jdbcEnvironmentRepository(JdbcTemplate jdbc) {
        return new JdbcEnvironmentRepository(jdbc);
    }
}

    

  这里面创建了JdbcEnvironmentRepostiory,紧接着我们在看一下这个类的源码:

/*
 * Copyright 2016-2017 the original author or 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 org.springframework.cloud.config.server.environment;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.util.StringUtils;

/**
 * An {@link EnvironmentRepository} that picks up data from a relational database. The
 * database should have a table called "PROPERTIES" with columns "APPLICATION", "PROFILE",
 * "LABEL" (with the usual {@link Environment} meaning), plus "KEY" and "VALUE" for the
 * key and value pairs in {@link Properties} style. Property values behave in the same way
 * as they would if they came from Spring Boot properties files named
 * <code>{application}-{profile}.properties</code>, including all the encryption and
 * decryption, which will be applied as post-processing steps (i.e. not in this repository
 * directly).
 * 
 * @author Dave Syer
 *
 */
@ConfigurationProperties("spring.cloud.config.server.jdbc")
public class JdbcEnvironmentRepository implements EnvironmentRepository, Ordered {

    private static final String DEFAULT_SQL = "SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?";
    private int order = Ordered.LOWEST_PRECEDENCE - 10;
    private final JdbcTemplate jdbc;
    private String sql = DEFAULT_SQL;
    private final PropertiesResultSetExtractor extractor = new PropertiesResultSetExtractor();

    public JdbcEnvironmentRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
    
    public void setSql(String sql) {
        this.sql = sql;
    }

    public String getSql() {
        return this.sql;
    }

    @Override
    public Environment findOne(String application, String profile, String label) {
        String config = application;
        if (StringUtils.isEmpty(label)) {
            label = "master";
        }
        if (StringUtils.isEmpty(profile)) {
            profile = "default";
        }
        if (!profile.startsWith("default")) {
            profile = "default," + profile;
        }
        String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
        Environment environment = new Environment(application, profiles, label, null,
                null);
        if (!config.startsWith("application")) {
            config = "application," + config;
        }
        List<String> applications = new ArrayList<String>(new LinkedHashSet<>(
                Arrays.asList(StringUtils.commaDelimitedListToStringArray(config))));
        List<String> envs = new ArrayList<String>(new LinkedHashSet<>(Arrays.asList(profiles)));
        Collections.reverse(applications);
        Collections.reverse(envs);
        for (String app : applications) {
            for (String env : envs) {
                Map<String, String> next = (Map<String, String>) jdbc.query(this.sql,
                        new Object[] { app, env, label }, this.extractor);
                if (!next.isEmpty()) {
                    environment.add(new PropertySource(app + "-" + env, next));
                }
            }
        }
        return environment;
    }

    @Override
    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

}

class PropertiesResultSetExtractor implements ResultSetExtractor<Map<String, String>> {

    @Override
    public Map<String, String> extractData(ResultSet rs)
            throws SQLException, DataAccessException {
        Map<String, String> map = new LinkedHashMap<>();
        while (rs.next()) {
            map.put(rs.getString(1), rs.getString(2));
        }
        return map;
    }

}
View Code

  我们可以看到该类实现了EnvironmentRepository接口,在findone方法里通过JDBCTemplate来获取数据库中的配置信息。

  下面我们来修改config-server端代码

4.1、gradle配置

dependencies {
    compile('org.springframework.cloud:spring-cloud-config-server')
    compile('org.springframework.boot:spring-boot-starter-jdbc')
    compile group: 'mysql', name: 'mysql-connector-java'
}

4.2、bootstrap.yml

spring:
  profiles:
    active: jdbc
  application:
    name: config-server
  cloud:
    config:
      server:
        jdbc:
          sql: SELECT `KEY`,`VALUE` FROM PROPERTIES  where APPLICATION=? and PROFILE=? and LABEL=?
      profile: local
      label: master
  datasource:
    url: jdbc:mysql://localhost:3306/MySchool?characterEncoding=utf-8&useSSL=false
    username: root
    password: root
server:
  port: 8888

3、DDL脚本

create table PROPERTIES
(
    ID int auto_increment
        primary key,
    `KEY` varchar(32) null,
    VALUE varchar(32) null,
    APPLICATION varchar(64) null,
    PROFILE varchar(32) null,
    LABEL varchar(16) null,
    CREATE_DATE datetime null
) CHARSET='utf8'
;

4、验证结果

四、总结

  1.ConfigServer利用了SpringCloud引导机制,当主程序启动时,通过PropertySourceLocator的方法把相关配置读到当前的Environment中,同时提供了EnvironmentController使外界能够根据不同的请求获取不同格式的配置结果,由于是引导程序是核心,因此务必使用bootstrap.yml(properties)进行配置操作。

  2.SpringCloud的客户端同样利用引导,通过实现PropertySourceLocator接口在程序启动前利用RestTemplate访问ConfigServer获取到配置并加载到当前Environment中

猜你喜欢

转载自www.cnblogs.com/niechen/p/9068479.html