SpringCloud中使用Apollo实现动态刷新

发布时间 2023-04-21 17:14:35作者: 甜菜波波

SpringCloud中使用Apollo实现动态刷新



普通字段

在需要刷新的字段上使用@value注解即可,例如:

    @Value("${test.user.name}")
    private String name;

    @Value("${test.user.age}")
    private Integer age;

    @Value("${test.user.sex}")
    private Boolean sex;
 

bean使用@ConfigurationProperties动态刷新

bean使用@ConfigurationProperties注解目前还不支持自动刷新,得编写一定的代码实现刷新。目前官方提供2种刷新方案:

  • 基于RefreshScope实现刷新
  • 基于EnvironmentChangeEvent实现刷新

方法一:基于RefreshScope实现刷新

  1. 确保项目中已引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
</dependency>
 
  1. 实体类上使用@RefreshScope注解
@ConfigurationProperties("test.user")
@Data
@Component
@RefreshScope
public class TestUserProperties implements Serializable {
    private Integer age;
    private String name;
    private Boolean sex;
}
 

在namespace=config的命名空间中定义配置:

test.user.name = zhangsan
test.user.age = 10
test.user.sex = 1
 
  1. 利用RefreshScope搭配@ApolloConfigChangeListener监听实现bean的动态刷新
@Component
public class ApolloDynamicConfigPropertiesRefresh {

    @Resource
    RefreshScope refreshScope;

    @ApolloConfigChangeListener(value="config")
    private void refresh(ConfigChangeEvent changeEvent){

        refreshScope.refresh("testUserProperties");

        PrintChangeKeyUtils.printChange(changeEvent);
    }
}
 
  • @ApolloConfigChangeListener(value="config") 表示监听namespace=config的配置文件的变化
  • refreshScope.refresh("testUserProperties"); 表示如果触发监听事件,则刷新名为testUserProperties的bean;
  • PrintChangeKeyUtils.printChange(changeEvent); 表示打印发送变化的熟悉(可选),PrintChangeKeyUtils定义为:
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import org.springframework.util.CollectionUtils;

import java.util.Set;

public class PrintChangeKeyUtils {

    public static void printChange(ConfigChangeEvent changeEvent) {
        Set<String> changeKeys = changeEvent.changedKeys();
        if (!CollectionUtils.isEmpty(changeKeys)) {
            for (String changeKey : changeKeys) {
                ConfigChange configChange = changeEvent.getChange(changeKey);
                System.out.println("key:" + changeKey + ";oldValue:" + configChange.getOldValue() + ";newValue:" + configChange.getNewValue());
            }
        }
    }
}
 

方法二:基于EnvironmentChangeEvent实现刷新

  1. 定义实体类
@ConfigurationProperties("test.user")
@Data
@Component
public class TestUserProperties implements Serializable {
    private Integer age;
    private String name;
    private Boolean sex;
}
 

与方法一的差异是不使用@RefreshScope注解

  1. 利用spring的事件驱动配合@ApolloConfigChangeListener监听实现bean的动态刷新:
@Component
public class ApolloDynamicConfigPropertiesRefresh  implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    RefreshScope refreshScope;

    @ApolloConfigChangeListener(value="config")
    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;
    }
}
 

两种方式动态刷新原理浅析:

RefreshScope

首先了解Spring中几个相关的类:

  • 注解@RefreshScope(org.springframework.cloud.context.config.annotation.RefreshScope)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
 
  • 注解@Scope(org.springframework.context.annotation.Scope)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {


	@AliasFor("scopeName")
	String value() default "";

	@AliasFor("value")
	String scopeName() default "";

	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}
 
  • 接口Scope(org.springframework.beans.factory.config.Scope)
public interface Scope {

	Object get(String name, ObjectFactory<?> objectFactory);

	@Nullable
	Object remove(String name);

	void registerDestructionCallback(String name, Runnable callback);

	@Nullable
	Object resolveContextualObject(String key);

	@Nullable
	String getConversationId();

}
 
  • 类RefreshScope(org.springframework.cloud.context.scope.refresh.RefreshScope)
@ManagedResource
public class RefreshScope extends GenericScope implements ApplicationContextAware,ApplicationListener<ContextRefreshedEvent>, Ordered {

	private ApplicationContext context;

	private BeanDefinitionRegistry registry;

	private boolean eager = true;

	private int order = Ordered.LOWEST_PRECEDENCE - 100;

	/**
	 * Creates a scope instance and gives it the default name: "refresh".
	 */
	public RefreshScope() {
		super.setName("refresh");
	}

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

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

	public void setEager(boolean eager) {
		this.eager = eager;
	}

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
			throws BeansException {
		this.registry = registry;
		super.postProcessBeanDefinitionRegistry(registry);
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		start(event);
	}

	public void start(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.context && this.eager
				&& this.registry != null) {
			eagerlyInitialize();
		}
	}

	private void eagerlyInitialize() {
		for (String name : this.context.getBeanDefinitionNames()) {
			BeanDefinition definition = this.registry.getBeanDefinition(name);
			if (this.getName().equals(definition.getScope())
					&& !definition.isLazyInit()) {
				Object bean = this.context.getBean(name);
				if (bean != null) {
					bean.getClass();
				}
			}
		}
	}

	@ManagedOperation(description = "Dispose of the current instance of bean name "
			+ "provided and force a refresh on next method execution.")
	public boolean refresh(String name) {
		if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
			// User wants to refresh the bean with this name but that isn't the one in the
			// cache...
			name = SCOPED_TARGET_PREFIX + name;
		}
		// Ensure lifecycle is finished if bean was disposable
		if (super.destroy(name)) {
			this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
			return true;
		}
		return false;
	}

	@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
	public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

	@Override
	public void setApplicationContext(ApplicationContext context) throws BeansException {
		this.context = context;
	}
}
 

带有@RefreshScope注解后的Bean,在初始话的过程中,会通过AnnotationScopeMetadataResolver#resolveScopeMetadata提取元数据:

@Override
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
    ScopeMetadata metadata = new ScopeMetadata();
    if (definition instanceof AnnotatedBeanDefinition) {
        AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
        AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
                annDef.getMetadata(), this.scopeAnnotationType);
        if (attributes != null) {
            metadata.setScopeName(attributes.getString("value"));
            ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
            if (proxyMode == ScopedProxyMode.DEFAULT) {
                proxyMode = this.defaultProxyMode;
            }
            metadata.setScopedProxyMode(proxyMode);
        }
    }
    return metadata;
}
 

可以理解为 @RefreshScope 是scopeName="refresh"的 @Scope,其Bean的注册将通过AnnotatedBeanDefinitionReader#registerBean完成的:

private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
			@Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
			@Nullable BeanDefinitionCustomizer[] customizers) {

		AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
		if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
			return;
		}

		abd.setInstanceSupplier(supplier);
		ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
		abd.setScope(scopeMetadata.getScopeName());
		String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

		AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
		if (qualifiers != null) {
			for (Class<? extends Annotation> qualifier : qualifiers) {
				if (Primary.class == qualifier) {
					abd.setPrimary(true);
				}
				else if (Lazy.class == qualifier) {
					abd.setLazyInit(true);
				}
				else {
					abd.addQualifier(new AutowireCandidateQualifier(qualifier));
				}
			}
		}
		if (customizers != null) {
			for (BeanDefinitionCustomizer customizer : customizers) {
				customizer.customize(abd);
			}
		}

		BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
		definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
		BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
	}
 

测试

@RestController
@RequestMapping("/test/dynamic/config")
public class DynamicConfigTestController {

    @Value("${test.user.name}")
    private String name;

    @Value("${test.user.age}")
    private Integer age;

    @Value("${test.user.sex}")
    private Boolean sex;

    @Resource
    private TestUserProperties userProperties;

    @GetMapping("/user")
    public Map<String, Object> properties() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", name);
        map.put("age", age);
        map.put("sex", sex);
        map.put("user", userProperties.toString());
        return map;
    }

}
 

参考

  1. apollo与springboot集成实现动态刷新配置
  2. @RefreshScope那些事