分散式apollo簡介
Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具備規範的許可權、流程治理等特性。
本文主要介紹如何使用apollo與springboot實現動態重新整理配置,如果之前不瞭解apollo可以檢視如下文件
學習瞭解一下apollo,再來檢視本文
正文
apollo與spring實現動態重新整理配置本文主要演示2種重新整理,一種基於普通欄位重新整理、一種基於bean上使用了@ConfigurationProperties重新整理
1、普通欄位重新整理
a、pom.xml配置
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.6.0</version>
</dependency>
b、客戶端配置AppId,Apollo Meta Server
此配置有多種方法,本示例直接在application.yml配置,配置內容如下
app:
id: ${spring.application.name}
apollo:
meta: http://192.168.88.128:8080,http://192.168.88.129:8080
bootstrap:
enabled: true
eagerLoad:
enabled: true
c、專案中啟動類上加上@EnableApolloConfig註解,形如下
@SpringBootApplication
@EnableApolloConfig(value = {"application","user.properties","product.properties","order.properties"})
public class ApolloApplication {
public static void main(String[] args) {
SpringApplication.run(ApolloApplication.class, args);
}
}
@EnableApolloConfig不一定要加在啟動類上,加在被spring管理的類上即可
d、在需重新整理的欄位上配置@Value註解,形如
@Value("${hello}")
private String hello;
通過以上三步就可以實現普通欄位的動態重新整理
2.bean使用@ConfigurationProperties動態重新整理
bean使用@ConfigurationProperties註解目前還不支援自動重新整理,得編寫一定的程式碼實現重新整理。目前官方提供2種重新整理方案
- 基於RefreshScope實現重新整理
- 基於EnvironmentChangeEvent實現重新整理
- 本文再提供一種,當bean上如果使用了@ConditionalOnProperty如何實現重新整理
a、基於RefreshScope實現重新整理
1、pom.xml要額外引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
2、bean上使用@RefreshScope註解
@Component
@ConfigurationProperties(prefix = "product")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@RefreshScope
public class Product {
private Long id;
private String productName;
private BigDecimal price;
}
3、利用RefreshScope搭配@ApolloConfigChangeListener監聽實現bean的動態重新整理,其程式碼實現如下
@ApolloConfigChangeListener(value="product.properties",interestedKeyPrefixes = {"product."})
private void refresh(ConfigChangeEvent changeEvent){
refreshScope.refresh("product");
PrintChangeKeyUtils.printChange(changeEvent);
}
b、基於EnvironmentChangeEvent實現重新整理
利用spring的事件驅動配合@ApolloConfigChangeListener監聽實現bean的動態重新整理,其程式碼如下
@Component
@Slf4j
public class UserPropertiesRefresh implements ApplicationContextAware {
private ApplicationContext applicationContext;
@ApolloConfigChangeListener(value="user.properties",interestedKeyPrefixes = {"user."})
private void refresh(ConfigChangeEvent changeEvent){
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
PrintChangeKeyUtils.printChange(changeEvent);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
c、當bean上有@ConditionalOnProperty如何實現重新整理
當bean上有@ConditionalOnProperty註解時,上述的兩種方案可以說失效了,因為@ConditionalOnProperty是一個條件註解,當不滿足條件註解時,bean是沒法註冊到spring容器中的。如果我們要實現此種情況的下的動態重新整理,我們就得自己手動註冊或者銷燬bean了。其實現流程如下
1、當滿足條件註解時,則手動建立bean,然後配合@ApolloConfigChangeListener監聽該bean的屬性變化。當該bean屬性有變化時,手動把屬性注入bean。同時重新整理依賴該bean的其他bean
2、當不滿足條件註解時,則手動從spring容器中移除bean,同時重新整理依賴該bean的其他bean
其重新整理核心程式碼如下
public class OrderPropertiesRefresh implements ApplicationContextAware {
private ApplicationContext applicationContext;
@ApolloConfig(value = "order.properties")
private Config config;
@ApolloConfigChangeListener(value="order.properties",interestedKeyPrefixes = {"order."},interestedKeys = {"model.isShowOrder"})
private void refresh(ConfigChangeEvent changeEvent){
for (String basePackage : listBasePackages()) {
Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
if(!CollectionUtils.isEmpty(conditionalClasses)){
for (Class conditionalClass : conditionalClasses) {
ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
String beanChangeCondition = this.getChangeKey(changeEvent,conditionalOnPropertyKeys);
String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
boolean isChangeBean = this.changeBean(conditionalClass, beanChangeCondition, conditionalOnPropertyValue);
if(!isChangeBean){
// 更新相應的bean的屬性值,主要是存在@ConfigurationProperties註解的bean
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
}
}
}
PrintChangeKeyUtils.printChange(changeEvent);
printAllBeans();
}
/**
* 根據條件對bean進行註冊或者移除
* @param conditionalClass
* @param beanChangeCondition bean發生改變的條件
* @param conditionalOnPropertyValue
*/
private boolean changeBean(Class conditionalClass, String beanChangeCondition, String conditionalOnPropertyValue) {
boolean isNeedRegisterBeanIfKeyChange = this.isNeedRegisterBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
boolean isNeedRemoveBeanIfKeyChange = this.isNeedRemoveBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
if(isNeedRegisterBeanIfKeyChange){
boolean isAlreadyRegisterBean = this.isExistBean(beanName);
if(!isAlreadyRegisterBean){
this.registerBean(beanName,conditionalClass);
return true;
}
}else if(isNeedRemoveBeanIfKeyChange){
this.unregisterBean(beanName);
return true;
}
return false;
}
/**
* bean註冊
* @param beanName
* @param beanClass
*/
public void registerBean(String beanName,Class beanClass) {
log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
setBeanField(beanClass, beanDefinition);
getBeanDefinitionRegistry().registerBeanDefinition(beanName,beanDefinition);
}
/**
* 設定bean欄位值
* @param beanClass
* @param beanDefinition
*/
private void setBeanField(Class beanClass, BeanDefinition beanDefinition) {
ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
if(ObjectUtils.isNotEmpty(configurationProperties)){
String prefix = configurationProperties.prefix();
for (String propertyName : config.getPropertyNames()) {
String fieldPrefix = prefix + ".";
if(propertyName.startsWith(fieldPrefix)){
String fieldName = propertyName.substring(fieldPrefix.length());
String fieldVal = config.getProperty(propertyName,null);
log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
beanDefinition.getPropertyValues().add(fieldName,fieldVal);
}
}
}
}
/**
* bean移除
* @param beanName
*/
public void unregisterBean(String beanName){
log.info("unregisterBean->beanName:{}",beanName);
getBeanDefinitionRegistry().removeBeanDefinition(beanName);
}
public <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
public <T> T getBean(Class<T> clz) {
return (T) applicationContext.getBean(clz);
}
public boolean isExistBean(String beanName){
return applicationContext.containsBean(beanName);
}
public boolean isExistBean(Class clz){
try {
Object bean = applicationContext.getBean(clz);
return true;
} catch (BeansException e) {
// log.error(e.getMessage(),e);
}
return false;
}
private boolean isNeedRegisterBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
if(StringUtils.isEmpty(changeKey)){
return false;
}
String apolloConfigValue = config.getProperty(changeKey,null);
return conditionalOnPropertyValue.equals(apolloConfigValue);
}
private boolean isNeedRemoveBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
if(!StringUtils.isEmpty(changeKey)){
String apolloConfigValue = config.getProperty(changeKey,null);
return !conditionalOnPropertyValue.equals(apolloConfigValue);
}
return false;
}
private boolean isChangeKey(ConfigChangeEvent changeEvent,String conditionalOnPropertyKey){
Set<String> changeKeys = changeEvent.changedKeys();
if(!CollectionUtils.isEmpty(changeKeys) && changeKeys.contains(conditionalOnPropertyKey)){
return true;
}
return false;
}
private String getChangeKey(ConfigChangeEvent changeEvent, String[] conditionalOnPropertyKeys){
if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
return null;
}
String changeKey = null;
for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
if(isChangeKey(changeEvent,conditionalOnPropertyKey)){
changeKey = conditionalOnPropertyKey;
break;
}
}
return changeKey;
}
private BeanDefinitionRegistry getBeanDefinitionRegistry(){
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
BeanDefinitionRegistry beanDefinitionRegistry = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
return beanDefinitionRegistry;
}
private List<String> listBasePackages(){
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
return AutoConfigurationPackages.get(configurableContext.getBeanFactory());
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void printAllBeans() {
String[] beans = applicationContext.getBeanDefinitionNames();
Arrays.sort(beans);
for (String beanName : beans) {
Class<?> beanType = applicationContext.getType(beanName);
System.out.println(beanType);
}
}
}
如果條件註解的值也是配置在apollo上,可能會出現依賴條件註解的bean的其他bean,在專案拉取apollo配置時,就已經注入spring容器中,此時就算條件註解滿足條件,則引用該條件註解bean的其他bean,也會拿不到條件註解bean。此時有2種方法解決,一種是在依賴條件註解bean的其他bean注入之前,先手動註冊條件註解bean到spring容器中,其核心程式碼如下
@Component
@Slf4j
public class RefreshBeanFactory implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
Config config = ConfigService.getConfig("order.properties");
List<String> basePackages = AutoConfigurationPackages.get(configurableListableBeanFactory);
for (String basePackage : basePackages) {
Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
if(!CollectionUtils.isEmpty(conditionalClasses)){
for (Class conditionalClass : conditionalClasses) {
ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
String beanConditionKey = this.getConditionalOnPropertyKey(config,conditionalOnPropertyKeys);
String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
this.registerBeanIfMatchCondition((DefaultListableBeanFactory)configurableListableBeanFactory,config,conditionalClass,beanConditionKey,conditionalOnPropertyValue);
}
}
}
}
private void registerBeanIfMatchCondition(DefaultListableBeanFactory beanFactory,Config config,Class conditionalClass, String beanConditionKey, String conditionalOnPropertyValue) {
boolean isNeedRegisterBean = this.isNeedRegisterBean(config,beanConditionKey,conditionalOnPropertyValue);
String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
if(isNeedRegisterBean){
this.registerBean(config,beanFactory,beanName,conditionalClass);
}
}
public void registerBean(Config config,DefaultListableBeanFactory beanFactory, String beanName, Class beanClass) {
log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
setBeanField(config,beanClass, beanDefinition);
beanFactory.registerBeanDefinition(beanName,beanDefinition);
}
private void setBeanField(Config config,Class beanClass, BeanDefinition beanDefinition) {
ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
if(ObjectUtils.isNotEmpty(configurationProperties)){
String prefix = configurationProperties.prefix();
for (String propertyName : config.getPropertyNames()) {
String fieldPrefix = prefix + ".";
if(propertyName.startsWith(fieldPrefix)){
String fieldName = propertyName.substring(fieldPrefix.length());
String fieldVal = config.getProperty(propertyName,null);
log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
beanDefinition.getPropertyValues().add(fieldName,fieldVal);
}
}
}
}
public boolean isNeedRegisterBean(Config config,String beanConditionKey,String conditionalOnPropertyValue){
if(StringUtils.isEmpty(beanConditionKey)){
return false;
}
String apolloConfigValue = config.getProperty(beanConditionKey,null);
return conditionalOnPropertyValue.equals(apolloConfigValue);
}
private String getConditionalOnPropertyKey(Config config, String[] conditionalOnPropertyKeys){
if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
return null;
}
String changeKey = null;
for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
if(isConditionalOnPropertyKey(config,conditionalOnPropertyKey)){
changeKey = conditionalOnPropertyKey;
break;
}
}
return changeKey;
}
private boolean isConditionalOnPropertyKey(Config config,String conditionalOnPropertyKey){
Set<String> propertyNames = config.getPropertyNames();
if(!CollectionUtils.isEmpty(propertyNames) && propertyNames.contains(conditionalOnPropertyKey)){
return true;
}
return false;
}
}
其次利用懶載入的思想,在使用條件註解bean時,使用形如下方法
Order order = (Order) SpringContextUtils.getBean("order");
總結
本文主要介紹了常用的動態重新整理,但本文的程式碼示例實現的功能不侷限於此,本文的程式碼還實現如何通過自定義註解與apollo整合來實現一些業務操作,同時也實現了基於hystrix註解與apollo整合,實現基於執行緒隔離的動態熔斷,感興趣的朋友可以複製文末連結到瀏覽器,進行檢視
apollo基本上是能滿足我們日常的業務開發要求,但是對於一些需求,比如動態重新整理線上資料庫資源啥,我們還是得做一定的量的改造,好在攜程也提供了apollo-use-cases,在裡面可以找到常用的使用場景以及示例程式碼,其連結如下
感興趣的朋友,可以檢視下。
demo連結
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apollo