apollo與springboot集成實現動態刷新配置

分佈式apollo簡介

Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,可以集中化管理應用不一樣環境、不一樣集羣的配置,配置修改後可以實時推送到應用端,而且具有規範的權限、流程治理等特性。git

本文主要介紹如何使用apollo與springboot實現動態刷新配置,若是以前不瞭解apollo能夠查看以下文檔github

https://github.com/ctripcorp/apollospring

學習瞭解一下apollo,再來查看本文數據庫

正文

apollo與spring實現動態刷新配置本文主要演示2種刷新,一種基於普通字段刷新、一種基於bean上使用了@ConfigurationProperties刷新bootstrap

一、普通字段刷新

a、pom.xml配置api

<dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>1.6.0</version>
        </dependency>

b、客戶端配置AppId,Apollo Meta Server瀏覽器

此配置有多種方法,本示例直接在application.yml配置,配置內容以下springboot

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註解,形以下app

@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實現刷新

一、pom.xml要額外引入

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>

二、bean上使用@RefreshScope註解

@Component
@ConfigurationProperties(prefix = "product")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@RefreshScope
public class Product {

    private Long id;

    private String productName;

    private BigDecimal price;

}

三、利用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了。其實現流程以下

一、當知足條件註解時,則手動建立bean,而後配合@ApolloConfigChangeListener監聽該bean的屬性變化。當該bean屬性有變化時,手動把屬性注入bean。同時刷新依賴該bean的其餘bean

二、當不知足條件註解時,則手動從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,在裏面能夠找到經常使用的使用場景以及示例代碼,其連接以下

https://github.com/ctripcorp/apollo-use-cases

感興趣的朋友,能夠查看下。

demo連接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apollo

相關文章
相關標籤/搜索