携程apollo配置中心搭建:https://blog.csdn.net/itwxming/article/details/104776170
目录
4.3.1、ConfigurationProperties注解配置的类使用Apollo没有随配置动态变化
4.3.3、zuul做网关时动态路由等相关配置变化可发布事件RoutesRefreshedEvent
一、 服务接入配置中心
添加了apollo客户端依赖的微服务工程要接入配置中心,按以下后续步骤即可:
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.3.0</version>
</dependency>
1.1、调整微服务配置
微服务的boostrap.yml文件中,开发apollo的连接并配置正确的apollo地址:
app:
id: mystart #apollo配置中心应用名
apollo:
meta: http://192.168.232.128:9001 #apollo地址
bootstrap:
enabled: true #是否启用apollo
namespaces: application.yml #Namespace
# namespaces: application.yml,application,TEST1.public #多个用逗号隔开,默认properties后缀
配置说明:
app.id:在配置中心配置的应用名。
apollo.bootstrap.enabled:在应用启动阶段是否向Spring容器注入被托管的properties文件配置信息。
apollo.bootstrap.namespaces:配置的命名空间,多个逗号分隔,一个namespace相当于一个配置文件。
apollo.meta:当前环境服务配置地址,生产环境建议至少双节点,可以填写多个逗号分隔,使用一个单独的域,如http://config.xxx.com(由nginx等软件负载平衡器支持),而不是多个IP地址,因为服务器可能会扩展或缩小。
apollo.bootstrap.eagerLoad.enabled:将Apollo配置加载提到初始化日志系统之前。如果希望把日志相关的配置(如logging.level.root=info
或logback-spring.xml
中的参数)也放在Apollo管理,那么可以额外配置apollo.bootstrap.eagerLoad.enabled=true
来使Apollo的加载顺序放到日志系统加载之前,不过这会导致Apollo的启动过程无法通过日志的方式输出(因为执行Apollo加载的时候,日志系统压根没有准备好呢!所以在Apollo代码中使用Slf4j的日志输出便没有任何内容),更多信息可以参考PR 1614。
1.2 在配置中心中创建项目
登录配置中心(默认用户密码为apollo/admin),然后创建项目
应用Id为微服务接入Apollo的唯一标识。一般取工程创建后的配置文件boostrap.yml的app.id配置项的值,或自定义。
务必保证此应用Id于微服务配置文件boostrap.yml中的app.id配置项保持一致!!!
1.3 在项目中创建Namespace
点击左下角的“创建Namespace”
然后创建类型为"private",名称为application,后缀为yml的Namespace
创建完成后,直接返回到项目首页
1.4 添加配置信息
点击右侧的“修改配置”按钮
将工程中的application.yml文件中的配置信息调整后,复制到文本框中,然后点击“修改提交”
1.5 发布
点击“发布”按钮后,会出现本次修改信息的弹框,检查无误后,点击最下方的“发布”按钮即可。
如上常规的配置使用即可以了。
二、 公共空间使用
一些多个微服务都会使用到的配置可以使用公共空间来配置
2.1 新建公共配置项目
2.2 新建公共Namespace
2.3 公共配置放入公共Namespace
2.4 微服务端bootstrap.yml配置
在原私有空间后,追加公共Namespace,如上的“TEST1.public-config”,英文逗号隔开:
app:
id: mystart #apollo配置中心应用名
apollo:
meta: http://192.168.232.128:9001 #apollo地址
bootstrap:
enabled: true #是否启用apollo
namespaces: application.yml,TEST1.public-config #Namespace
三、单个服务覆盖公共配置
如果在使用公共空间配置的时候,个别微服务的某个配置需要跟公共的项不一样,需要覆盖,则可通过关联公共Namespace来做。
3.1 新建Namespace
注意是新建Namespace,不需要再新建项目。进入要实现上述需求的那个项目中,点击左下角的“添加Namespace”。
3.2 关联公共Namespace
namespace中选择要关联的公共Namespace。
提交后,先直接返回到项目首页,权限全部默认。
3.3 修改要覆盖的配置
注:1、关联公共配置只是单独给这个微服务修改了公共配置项,别的不影响。且服务的bootstrap.yml中namespaces任需配上上述公共空间TEST1.public-config。2、如果apollo上没有读到某个配置项,会去本地的application.yml配置文件找。3、Apollo会从远程拉取到本地生成缓存文件,如果远程连接有问题也会走本地的缓存文件。默认缓存目录:https://github.com/ctripcorp/apollo/wiki/Java客户端使用指南#123-本地缓存路径
- Mac/Linux: /opt/data/{appId}/config-cache
- Windows: C:\opt\data\{appId}\config-cache
四、代码中具体使用相关
4.1客户端获取配置
详见:https://github.com/ctripcorp/apollo/wiki/Java客户端使用指南#三客户端用法
4.1.1、最直接的Api方式
API方式是最简单、高效使用Apollo配置的方式,不依赖Spring框架即可使用。
4.1.1.1、获取默认namespace的配置(application)
Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
String someKey = "someKeyFromDefaultNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
通过上述的config.getProperty可以获取到someKey对应的实时最新的配置值。
另外,配置值从内存中获取,所以不需要应用自己做缓存。
4.1.1.2、监听配置变化事件
监听配置变化事件只在应用真的关心配置变化,需要在配置变化时得到通知时使用,比如:数据库连接串变化后需要重建连接等。
如果只是希望每次都取到最新的配置的话,只需要按照上面的例子,调用config.getProperty即可。
Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
System.out.println("Changes for namespace " + changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
}
});
4.2、Spring Annotation支持
Apollo同时还增加了几个新的Annotation来简化在Spring环境中的使用。
- @ApolloConfig:用来自动注入Config对象
- @ApolloConfigChangeListener:用来自动注册ConfigChangeListener
- @ApolloJsonValue:用来把配置的json字符串自动注入为对象
使用样例如下:
public class TestApolloAnnotationBean {
@ApolloConfig
private Config config; //inject config for namespace application
@ApolloConfig("application")
private Config anotherConfig; //inject config for namespace application
@ApolloConfig("FX.apollo")
private Config yetAnotherConfig; //inject config for namespace FX.apollo
@ApolloConfig("application.yml")
private Config ymlConfig; //inject config for namespace application.yml
/**
* ApolloJsonValue annotated on fields example, the default value is specified as empty list - []
* <br />
* jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}]
*/
@ApolloJsonValue("${jsonBeanProperty:[]}")
private List<JsonBean> anotherJsonBeans;
@Value("${batch:100}")
private int batch;
//config change listener for namespace application
@ApolloConfigChangeListener
private void someOnChange(ConfigChangeEvent changeEvent) {
//update injected value of batch if it is changed in Apollo
if (changeEvent.isChanged("batch")) {
batch = config.getIntProperty("batch", 100);
}
}
//config change listener for namespace application
@ApolloConfigChangeListener("application")
private void anotherOnChange(ConfigChangeEvent changeEvent) {
//do something
}
//config change listener for namespaces application, FX.apollo and application.yml
@ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"})
private void yetAnotherOnChange(ConfigChangeEvent changeEvent) {
//do something
}
//example of getting config from Apollo directly
//this will always return the latest value of timeout
public int getTimeout() {
return config.getIntProperty("timeout", 200);
}
//example of getting config from injected value
//the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above
public int getBatch() {
return this.batch;
}
private static class JsonBean{
private String someString;
private int someInt;
}
}
4.3、常见问题解决与实践
4.3.1、ConfigurationProperties注解配置的类使用Apollo没有随配置动态变化
比如:person01相关的配置变化之后程序没有动态变化。
/**
* @author twotiger-wxm.
* @date 2019/6/5.
*/
//prefix取配置的前缀,随便命名。
@EnableCaching//最简单的在启动类上开启此注解就可用spring那套注解缓存。但在单独配置类上加也行。但需要自己配置CacheManager,如下。
@ConfigurationProperties(prefix = "person01")
public class Person {
private static final Logger logger = LoggerFactory.getLogger(Person.class);
private String name;
private Integer age;
private Date birthDate;
private List<String> pets;
private List<String> cars;
private Map maps;
//myGirlFriend1必须是另一个类的类型,不能是Person类本身,否则myGirlFriend1=null。
private GirlFriend myGirlFriend1;
解决方案:使用@ApolloConfigChangeListener监听配置变化,监听到之后去修改上下文中的内容(修改上下文spring已有实现,自己监听到之后将对应事件和内容传入即可。)
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
/**
* 写一个配置类,类中去实现监听Apollo配置变化。
*/
@Configuration //或者@Component都可。
public class ApolloConfigListener implements ApplicationContextAware {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ApolloConfigListener.class);
private ApplicationContext applicationContext;
/**
* 配置监听
* @ApolloConfigChangeListener > value属性默认命名空间 "application",默认点properties结尾的。
*
* 示例: @ApolloConfigChangeListener(value = {"application", "test_space"})
*/
@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent) {
LOGGER.info("【Apollo-config-change】start");
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
LOGGER.info("key={} , propertyName={} , oldValue={} , newValue={} ", key, change.getPropertyName(), change.getOldValue(), change.getNewValue());
}
//发布EnvironmentChangeEvent事件,去更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
LOGGER.info("【Apollo-config-change】end");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
SpringCloud配置刷新监听实现源码:
该类监听了ChangeEnvrionmentEvent事件,它最主要作用是拿到更新的配置以后,重新绑定@ConfigurationProperties标记的类使之能够读取最新的属性:
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
this.beans = beans;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
/**
* A map of bean name to errors when instantiating the bean.
*
* @return the errors accumulated since the latest destroy
*/
public Map<String, Exception> getErrors() {
return this.errors;
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = getTargetObject(bean);
}
this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
return false;
}
@SuppressWarnings("unchecked")
private static <T> T getTargetObject(Object candidate) {
try {
if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) {
return (T) ((Advised) candidate).getTargetSource().getTarget();
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to unwrap proxied object", ex);
}
return (T) candidate;
}
@ManagedAttribute
public Set<String> getBeanNames() {
return new HashSet<String>(this.beans.getBeanNames());
}
@Override//此处监听上下文变化事件
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
}
4.3.2、日志级别配置没有更新
解决方案:使用@ApolloConfigChangeListener监听配置变化,监听到之后去修改日志级别。
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* 日志Apollo动态配置监听
*/
@Configuration
public class LoggerConfigListener {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerConfigListener.class);
private static final String LOGGER_TAG = "logging.level.";
@Resource
private LoggingSystem loggingSystem;//注入日志系统抽象类接口
/**
* 只监听对应的日志配置的变化
*/
@ApolloConfigChangeListener(interestedKeyPrefixes = LOGGER_TAG)
private void onChangeLogger(ConfigChangeEvent changeEvent) {
LOGGER.info("【Apollo-logger-config-change】>> start");
refreshLoggingLevel(changeEvent);
LOGGER.info("【Apollo-logger-config-change】>> end");
}
/**
* 刷新日志级别
*/
private void refreshLoggingLevel(ConfigChangeEvent changeEvent) {
if (null == loggingSystem) {
return;
}
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if (!StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
continue;//非日志配置的变化跳过。
}
LOGGER.info("【Apollo-logger-config-change】>> key={} , propertyName={} , oldValue={} , newValue={} ",
key, change.getPropertyName(), change.getOldValue(), change.getNewValue());
String newLevel = change.getNewValue();//获取新配置值
LogLevel level = LogLevel.valueOf(newLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);//修改日志系统级别。
LOGGER.info("【Apollo-logger-config-change】>> {} -> {}", key, newLevel);
}
}
}
日志配置跟4.3.1的都是系统级别的配置,可整合在一起,中间做判断去做不同操作。
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* Apollo动态配置监听
*/
@Configuration
public class ApolloConfigListener implements ApplicationContextAware {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ApolloConfigListener.class);
/**
* 日志配置常量
*/
private static final String LOGGER_TAG = "logging.level.";
@Resource
private LoggingSystem loggingSystem;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 监听所有配置变化
*/
@ApolloConfigChangeListener
private void onChangeConfig(ConfigChangeEvent changeEvent) {
LOGGER.info("【Apollo-config-change】>> start");
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
LOGGER.info("【Apollo-config-change】>> key={} , propertyName={} , oldValue={} , newValue={} ",
key, change.getPropertyName(), change.getOldValue(), change.getNewValue());
//是否为日志配置
if (StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
//日志配置刷新
changeLoggingLevel(key, change);
continue;
}
// 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
LOGGER.info("【Apollo-config-change】>> end");
}
/**
* 刷新日志级别
*/
private void changeLoggingLevel(String key, ConfigChange change) {
if (null == loggingSystem) {
return;
}
String newLevel = change.getNewValue();
LogLevel level = LogLevel.valueOf(newLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
LOGGER.info("【Apollo-logger-config-change】>> {} -> {}", key, newLevel);
}
}
4.3.3、zuul做网关时动态路由等相关配置变化可发布事件RoutesRefreshedEvent
@Autowired
private RouteLocator routeLocator;
/**
* refresh routes。zuul已经实现的刷新事件监听见ZuulRefreshListener
* @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent
*/
this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));
4.3.4、不同类型配置文件的监听
//不同类型的配置环境
public class ApolloConstants {
public final static String NAMESPACE_APPLICATION_YAML = "application.yaml";
public final static String NAMESPACE_APPLICATION_YML = "application.yml";
public final static String NAMESPACE_APPLICATION_PROPERTIES = "application.properties";
public final static String NAMESPACE_APPLICATION_XML = "application.xml";
public final static String NAMESPACE_APPLICATION_JSON = "application.json";
}
以动态日志为例:
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 动态日志配置.
*/
public class DynamicRefreshLogLevelAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(DynamicRefreshLogLevelAutoConfiguration.class);
private static final String LOGGER_TAG = "logging.level.";
/**
* application.properties配置文件修改日志.
* 默认开启.
*/
@Configuration
public class PropertiesRefreshLogLevel {
@ApolloConfig
private Config config;
@ApolloConfigChangeListener(interestedKeyPrefixes = {LOGGER_TAG})
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("Properties configuration modify log level...");
DynamicRefreshLogLevelAutoConfiguration.this.onChange(config, changeEvent);
}
}
/**
* application.yml配置文件修改日志
*/
@Configuration
@ConditionalOnExpression("'${apollo.bootstrap.namespaces}'.contains('" + ApolloConstants.NAMESPACE_APPLICATION_YML + "')")
public class YmlRefreshLogLevel {
@ApolloConfig(ApolloConstants.NAMESPACE_APPLICATION_YML)
private Config config;
@ApolloConfigChangeListener(interestedKeyPrefixes = LOGGER_TAG, value = ApolloConstants.NAMESPACE_APPLICATION_YML)
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("yml configuration modify log level...");
DynamicRefreshLogLevelAutoConfiguration.this.onChange(config, changeEvent);
}
}
/**
* application.yaml配置文件修改日志
*/
@Configuration
@ConditionalOnExpression("'${apollo.bootstrap.namespaces}'.contains('" + ApolloConstants.NAMESPACE_APPLICATION_YAML + "')")
public class YamlRefreshLogLevel {
@ApolloConfig(ApolloConstants.NAMESPACE_APPLICATION_YAML)
private Config config;
@ApolloConfigChangeListener(interestedKeyPrefixes = LOGGER_TAG, value = ApolloConstants.NAMESPACE_APPLICATION_YAML)
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("yml configuration modify log level...");
DynamicRefreshLogLevelAutoConfiguration.this.onChange(config, changeEvent);
}
}
/**
* application.xml配置文件修改日志
*
*/
/*@Configuration
public class XmlRefreshLogLevel {
@ApolloConfig(ApolloConstants.NAMESPACE_APPLICATION_XML)
private Config config;
@ApolloConfigChangeListener(interestedKeyPrefixes = LOGGER_TAG, value = ApolloConstants.NAMESPACE_APPLICATION_XML)
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("xml配置修改日志级别...");
DynamicRefreshLogLevelAutoConfiguration.this.onChange(config, changeEvent);
}
}*/
/**
* application.json配置文件修改日志
*/
/*@Configuration
public class JsonRefreshLogLevel {
@ApolloConfig(ApolloConstants.NAMESPACE_APPLICATION_JSON)
private Config config;
@ApolloConfigChangeListener(interestedKeyPrefixes = LOGGER_TAG, value = ApolloConstants.NAMESPACE_APPLICATION_JSON)
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("json配置修改日志级别...");
DynamicRefreshLogLevelAutoConfiguration.this.onChange(config, changeEvent);
}
}*/
@Autowired
private LoggingSystem loggingSystem;
private void onChange(Config config, ConfigChangeEvent event) {
Set<String> loggingKeys = event.changedKeys();
Map<String, String> loggings = new HashMap<>();
loggingKeys.forEach(s -> loggings.put(s, config.getProperty(s, "info")));
refreshLoggingLevels(loggings);
}
private void refreshLoggingLevels(Map<String, String> loggings) {
for (String key : loggings.keySet()) {
String level = loggings.get(key);
LogLevel logLevel = LogLevel.valueOf(level.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), logLevel);
logger.info("{}:{}", key, level);
}
}
}