詳解SpringCloud是如何動(dòng)態(tài)更新配置的
前言
對于單體應(yīng)用架構(gòu)來說,會(huì)使用配置文件管理我們的配置,這就是之前項(xiàng)目中的application.properties或application.yml。
如果需要在多環(huán)境下使用,傳統(tǒng)的做法是復(fù)制這些文件命名為application-xxx.properties,并且在啟動(dòng)時(shí)配置spring.profiles.active={profile}來指定環(huán)境。
在微服務(wù)架構(gòu)下我們可能會(huì)有很多的微服務(wù),所以要求的不只是在各自微服務(wù)中進(jìn)行配置,我們需要將所有的配置放在統(tǒng)一平臺上進(jìn)行操作,不同的環(huán)境進(jìn)行不同的配置,運(yùn)行期間動(dòng)態(tài)調(diào)整參數(shù)等等。
于是Spring Cloud為我們提供了一個(gè)統(tǒng)一的配置管理,那就是Spring Cloud Config。
spring cloud config簡介
它為分布式系統(tǒng)外部配置提供了服務(wù)器端和客戶端的支持,它包括config server端和 config client端兩部分
- Config server端是一個(gè)可以橫向擴(kuò)展、集中式的配置服務(wù)器,它用于集中管理應(yīng)用程序各個(gè)環(huán)境下的配置,默認(rèn) 使用Git存儲(chǔ)配置內(nèi)容
- Config client 是config server的客戶端,用于操作存儲(chǔ)在server中的配置屬性
啟動(dòng)加載擴(kuò)展點(diǎn)
spring boot提供在 META-INF/spring.factories 文件中增加配置,來實(shí)現(xiàn)一些程序中預(yù)定義的擴(kuò)展點(diǎn)。
通過這種方式配置的擴(kuò)展點(diǎn)好處是不局限于某一種接口的實(shí)現(xiàn),而是同一類別的實(shí)現(xiàn)。
我們查看 spring-cloud-context 包中的 spring.factories 文件,如下所示:
# AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\ org.springframework.cloud.autoconfigure.LifecycleMvcEndpointAutoConfiguration,\ org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\ org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration,\ org.springframework.cloud.autoconfigure.WritableEnvironmentEndpointAutoConfiguration # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.cloud.bootstrap.BootstrapApplicationListener,\ org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\ org.springframework.cloud.context.restart.RestartListener # Bootstrap components org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\ org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\ org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\ org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\ org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration
可以看到BootstrapConfiguration下面有一個(gè)item,PropertySourceBootstrapConfiguration,進(jìn)入其代碼,查看即繼承關(guān)系,發(fā)現(xiàn)其實(shí)現(xiàn)了 ApplicationContextInitializer 接口,其目的就是在應(yīng)用程序上下文初始化的時(shí)候做一些額外的操作。
在 Bootstrap 階段,會(huì)通過 Spring Ioc 的整個(gè)生命周期來初始化所有通過key為org.springframework.cloud.bootstrap.BootstrapConfiguration 在 spring.factories 中配置的 Bean。
初始化的過程中,會(huì)獲取所有 ApplicationContextInitializer 類型的 Bean,并設(shè)置回SpringApplication主流程當(dāng)中。通過在 SpringApplication 的主流程中回調(diào)這些 ApplicationContextInitializer 的實(shí)例,做一些初始化的操作,即調(diào)用initialize方法。
下面我們就來看看PropertySourceBootstrapConfiguration這個(gè)方法:
@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;
//回調(diào)所有實(shí)現(xiàn)PropertySourceLocator接口實(shí)例的locate方法,
source = locator.locate(environment);
if (source == null) {
continue;
}
composite.addPropertySource(source);
empty = false;
}
if (!empty) {
//從當(dāng)前Enviroment中獲取 propertySources
MutablePropertySources propertySources = environment.getPropertySources();
//省略...
//將composite中的PropertySource添加到當(dāng)前應(yīng)用上下文的propertySources中
insertPropertySources(propertySources, composite);
//省略...
}在這個(gè)方法中會(huì)回調(diào)所有實(shí)現(xiàn) PropertySourceLocator 接口實(shí)例的locate方法, locate 方法返回一個(gè) PropertySource 的實(shí)例,統(tǒng)一add到CompositePropertySource實(shí)例中。如果 composite 中有新加的PropertySource,最后將composite中的PropertySource添加到當(dāng)前應(yīng)用上下文的propertySources中。
SpringCloudConsul的配置加載
正如上面說的,在 Bootstrap 階段,會(huì)通過 Spring Ioc 的整個(gè)生命周期來初始化所有通過key為org.springframework.cloud.bootstrap.BootstrapConfiguration 在 spring.factories 中配置的 Bean。同樣的在spring.factories文件中:
# Auto Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
我們確實(shí)看到了又這樣一個(gè)key存在,對應(yīng)value為ConsulConfigBootstrapConfiguration類,我們看看該類的實(shí)現(xiàn):
@Configuration(proxyBeanMethods = false)
@ConditionalOnConsulEnabled
public class ConsulConfigBootstrapConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@Import(ConsulAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cloud.consul.config.enabled",
matchIfMissing = true)
protected static class ConsulPropertySourceConfiguration {
@Autowired
private ConsulClient consul;
@Bean
@ConditionalOnMissingBean
public ConsulConfigProperties consulConfigProperties() {
return new ConsulConfigProperties();
}
@Bean
public ConsulPropertySourceLocator consulPropertySourceLocator(
ConsulConfigProperties consulConfigProperties) {
return new ConsulPropertySourceLocator(this.consul, consulConfigProperties);
}
}
}我們看到,這里只是注入了一些bean,我們注意下ConsulPropertySourceLocator這個(gè)類。
正如上面說的,SpringCloudConfig在啟動(dòng)的時(shí)候會(huì)回調(diào)所有實(shí)現(xiàn) PropertySourceLocator 接口實(shí)例的locate方法,consul就是實(shí)現(xiàn)了PropertySourceLocator接口,具體類為ConsulPropertySourceLocator,實(shí)現(xiàn)了locate方法:
@Override
@Retryable(interceptor = "consulRetryInterceptor")
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = this.properties.getName();
if (appName == null) {
appName = env.getProperty("spring.application.name");
}
List<String> profiles = Arrays.asList(env.getActiveProfiles());
String prefix = this.properties.getPrefix();
List<String> suffixes = new ArrayList<>();
// 不是文件類型的時(shí)候,后綴為 /,否則就是配置文件的后綴
if (this.properties.getFormat() != FILES) {
suffixes.add("/");
} else {
suffixes.add(".yml");
suffixes.add(".yaml");
suffixes.add(".properties");
}
// 路徑
String defaultContext = getContext(prefix, this.properties.getDefaultContext());
for (String suffix : suffixes) {
this.contexts.add(defaultContext + suffix);
}
// 追加環(huán)境及文件類型
for (String suffix : suffixes) {
addProfiles(this.contexts, defaultContext, profiles, suffix);
}
String baseContext = getContext(prefix, appName);
// 應(yīng)用名稱前綴
for (String suffix : suffixes) {
this.contexts.add(baseContext + suffix);
}
for (String suffix : suffixes) {
addProfiles(this.contexts, baseContext, profiles, suffix);
}
Collections.reverse(this.contexts);
CompositePropertySource composite = new CompositePropertySource("consul");
for (String propertySourceContext : this.contexts) {
try {
ConsulPropertySource propertySource = null;
if (this.properties.getFormat() == FILES) {
// 獲取值
Response<GetValue> response = this.consul.getKVValue(propertySourceContext, this.properties.getAclToken());
// 添加當(dāng)前索引
addIndex(propertySourceContext, response.getConsulIndex());
// 如果值不為空,則更新值并初始化
if (response.getValue() != null) {
ConsulFilesPropertySource filesPropertySource = new ConsulFilesPropertySource(propertySourceContext, this.consul, this.properties);
// 解析配置內(nèi)容
filesPropertySource.init(response.getValue());
propertySource = filesPropertySource;
}
} else {
propertySource = create(propertySourceContext, this.contextIndex);
}
if (propertySource != null) {
composite.addPropertySource(propertySource);
}
} catch (Exception e) {
if (this.properties.isFailFast()) {
log.error("Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
} else {
log.warn("Unable to load consul config from " + propertySourceContext, e);
}
}
}
return composite;
}
return null;
}獲取配置時(shí),根據(jù)應(yīng)用名稱,路徑,環(huán)境及配置類型拼接相應(yīng)的路徑,然后調(diào)用 Consul 獲取 KV 值的接口,獲取相應(yīng)的配置,根據(jù)類型解析后放入環(huán)境中
配置動(dòng)態(tài)刷新
感知到外部化配置的變更這部分代碼的操作是需要用戶來完成的。Spring Cloud Config 只提供了具備外部化配置可動(dòng)態(tài)刷新的能力,并不具備自動(dòng)感知外部化配置發(fā)生變更的能力。
比如如果你的配置是基于Mysql來實(shí)現(xiàn)的,那么在代碼里面肯定要有能力感知到配置發(fā)生變化了,然后再顯示的調(diào)用 ContextRefresher 的 refresh方法,從而完成外部化配置的動(dòng)態(tài)刷新(只會(huì)刷新使用RefreshScope注解的Bean)。
下面我們來看看config框架是怎么進(jìn)行動(dòng)態(tài)刷新的?主要類是這個(gè)ContextRefresher,刷新方法如下:
public synchronized Set refresh() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
//1、加載最新的值,并替換Envrioment中舊值
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
//2、將refresh scope中的Bean 緩存失效: 清空
this.scope.refreshAll();
return keys;
}上面ContextRefresher的refresh的方法主要做了兩件事:
- 1、觸發(fā)PropertySourceLocator的locator方法,需要加載最新的值,并替換 Environment 中舊值
- 2、Bean中的引用配置值需要重新注入一遍。重新注入的流程是在Bean初始化時(shí)做的操作,那也就是需要將refresh scope中的Bean 緩存失效,當(dāng)再次從refresh scope中獲取這個(gè)Bean時(shí),發(fā)現(xiàn)取不到,就會(huì)重新觸發(fā)一次Bean的初始化過程。
可以看到上面代碼中有這樣一句this.scope.refreshAll(),其中的scope就是RefreshScope。是用來存放scope類型為refresh類型的Bean(即使用RefreshScope注解標(biāo)識的Bean),也就是說當(dāng)一個(gè)Bean既不是singleton也不是prototype時(shí),就會(huì)從自定義的Scope中去獲取(Spring 允許自定義Scope),然后調(diào)用Scope的get方法來獲取一個(gè)實(shí)例,Spring Cloud 正是擴(kuò)展了Scope,從而控制了整個(gè) Bean 的生命周期。當(dāng)配置需要?jiǎng)討B(tài)刷新的時(shí)候, 調(diào)用this.scope.refreshAll()這個(gè)方法,就會(huì)將整個(gè)RefreshScope的緩存清空,完成配置可動(dòng)態(tài)刷新的可能。
注:關(guān)于ContextRefresh和RefreshScope的初始化配置是在RefreshAutoConfiguration類中完成的。而RefreshAutoConfiguration類初始化的入口是在spring-cloud-context中的META-INF/spring.factories中配置的。從而完成整個(gè)和動(dòng)態(tài)刷新相關(guān)的Bean的初始化操作。
SpringCloudConsul的配置刷新
Consul 監(jiān)聽配置是通過定時(shí)任務(wù)實(shí)現(xiàn)的,涉及的類為ConfigWatch
public class ConfigWatch implements ApplicationEventPublisherAware, SmartLifecycle {}該類的初始化是在 org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration 中實(shí)現(xiàn)的:
@Bean
@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
public ConfigWatch configWatch(ConsulConfigProperties properties,
ConsulPropertySourceLocator locator,
ConsulClient consul,
@Qualifier(CONFIG_WATCH_TASK_SCHEDULER_NAME) TaskScheduler taskScheduler) {
return new ConfigWatch(properties, consul, locator.getContextIndexes(), taskScheduler);
}我們看到ConfigWatch 類實(shí)現(xiàn)了 ApplicationEventPublisherAware 和 SmartLifecycle 接口. 當(dāng)應(yīng)用啟動(dòng)后,會(huì)調(diào)用 其實(shí)現(xiàn)的SmartLifecycle 的 start 方法,然后初始化配置監(jiān)聽,通過向線程池添加一個(gè)定時(shí)任務(wù),實(shí)現(xiàn)配置的定時(shí)拉取,定時(shí)任務(wù)默認(rèn)周期是 1s
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::watchConfigKeyValues, this.properties.getWatch().getDelay());
}
}1、發(fā)布事件
定時(shí)任務(wù)的監(jiān)聽邏輯如下:
// Timed 是 Prometheus 的監(jiān)控
@Timed("consul.watch-config-keys")
public void watchConfigKeyValues() {
if (this.running.get()) {
// 遍歷所有的配置的 key
for (String context : this.consulIndexes.keySet()) {
// turn the context into a Consul folder path (unless our config format
// are FILES)
if (this.properties.getFormat() != FILES && !context.endsWith("/")) {
context = context + "/";
}
// 根據(jù)配置返回的 index 判斷是否發(fā)生變化
try {
Long currentIndex = this.consulIndexes.get(context);
if (currentIndex == null) {
currentIndex = -1L;
}
log.trace("watching consul for context '" + context + "' with index " + currentIndex);
// use the consul ACL token if found
String aclToken = this.properties.getAclToken();
if (StringUtils.isEmpty(aclToken)) {
aclToken = null;
}
// 獲取指定的 key
Response<List<GetValue>> response = this.consul.getKVValues(context, aclToken, new QueryParams(this.properties.getWatch().getWaitTime(), currentIndex));
// if response.value == null, response was a 404, otherwise it was a
// 200
// reducing churn if there wasn't anything
if (response.getValue() != null && !response.getValue().isEmpty()) {
Long newIndex = response.getConsulIndex();
// 判斷 key 的 index 是否相等,如果發(fā)生變化,則發(fā)出 RefreshEvent 事件
if (newIndex != null && !newIndex.equals(currentIndex)) {
// don't publish the same index again, don't publish the first
// time (-1) so index can be primed
// 沒有發(fā)布過這個(gè) index 的事件,且不是第一次發(fā)布
if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
log.trace("Context " + context + " has new index " + newIndex);
// 發(fā)送事件
RefreshEventData data = new RefreshEventData(context, currentIndex, newIndex);
this.publisher.publishEvent(new RefreshEvent(this, data, data.toString()));
} else if (log.isTraceEnabled()) {
log.trace("Event for index already published for context " + context);
}
this.consulIndexes.put(context, newIndex);
} else if (log.isTraceEnabled()) {
log.trace("Same index for context " + context);
}
} else if (log.isTraceEnabled()) {
log.trace("No value for context " + context);
}
} catch (Exception e) {
// only fail fast on the initial query, otherwise just log the error
if (this.firstTime && this.properties.isFailFast()) {
log.error("Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
} else if (log.isTraceEnabled()) {
log.trace("Error querying consul Key/Values for context '" + context + "'", e);
} else if (log.isWarnEnabled()) {
// simplified one line log message in the event of an agent
// failure
log.warn("Error querying consul Key/Values for context '" + context + "'. Message: " + e.getMessage());
}
}
}
}
this.firstTime = false;
}監(jiān)聽時(shí)會(huì)遍歷所有的key,根據(jù) key 從 Consul 獲取相應(yīng)的數(shù)據(jù),判斷 Index 是否發(fā)生變化,如果發(fā)生變化,則發(fā)送 RefreshEvent 事件,需要手動(dòng)實(shí)現(xiàn)事件監(jiān)聽以響應(yīng)配置變化。
至于spring是怎樣發(fā)布事件,監(jiān)聽者又是怎樣接收到的,這里面的細(xì)節(jié)后續(xù)有時(shí)間再詳細(xì)剖析。
2、事件監(jiān)聽
現(xiàn)在我們主要來看下RefreshEvent發(fā)出去之后,監(jiān)聽者的邏輯。
通過函數(shù)調(diào)用棧,我們找到了這樣一個(gè)監(jiān)聽者RefreshEventListener:
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}我們知道,在spring中,監(jiān)聽者都需要實(shí)現(xiàn)這樣一個(gè)方法onApplicationEvent,該方法中我們發(fā)現(xiàn)有這樣一個(gè)分支
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}這個(gè)事件就是上面發(fā)出來的,因此這里能夠監(jiān)聽到,然后執(zhí)行回調(diào)方法handle
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}主要的邏輯在refreshEnvironment方法中:
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}我們知道this.context.getEnvironment().getPropertySources()是獲取env中的所有配置源,然后將其交給extract進(jìn)行處理。extract方法就是將配置源中的各種格式的配置,比如map、yml、properties類型等等,統(tǒng)一轉(zhuǎn)換為map類型,這樣就可以通過統(tǒng)一key-value形式獲取到任意想要的配置值。上面這段代碼的主要邏輯就是:
- 1、獲取所有的舊的(更新之前的)配置值
- 2、重新通過應(yīng)用初始方式更新所有的配置值
addConfigFilesToEnvironment - 3、將最新的值跟舊的值進(jìn)行對比,找出所有的更新過的key
- 4、重新發(fā)布配置變更時(shí)間
EnvironmentChangeEvent,將更新過的key傳遞給該事件
3、Env配置更新
下面來說下第二點(diǎn):重新通過應(yīng)用初始方式更新所有的配置值addConfigFilesToEnvironment,
/* For testing. */ ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null;
try {
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
// update targetName to preserve ordering
targetName = name;
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
}
......
|我們看到有這樣一句SpringApplicationBuilder,這里它是生成了一個(gè)spring應(yīng)用對象的生成器,然后執(zhí)行它的run方法,
也就是說,這里會(huì)重新執(zhí)行一遍spring的啟動(dòng)流程,所有的啟動(dòng)初始類都會(huì)重新執(zhí)行,包括上面提到的ConsulPropertySourceLocator類的locate方法,這里就會(huì)再次向consul server發(fā)起請求獲取最新的配置數(shù)據(jù),寫入env中。
因此后面通過this.context.getEnvironment().getPropertySources()得到的就是最新的配置源了。同時(shí)業(yè)務(wù)中也可以通過context.getEnvironment().getProperty(key)拿到任意key的最新值了。
刷新scope域
在上面的refresh方法中,我們還剩下這樣一句沒有講解:
this.scope.refreshAll();
這里主要就是刷新spring容器該scope類型下的所有bean,就可以通過@RefreshScope的bean實(shí)例的get方法獲取到最新的值了。前提條件是我們需要監(jiān)聽這個(gè)事件RefreshScopeRefreshedEvent:
@EventListener(classes = RefreshScopeRefreshedEvent.class)
public void updateChange(RefreshScopeRefreshedEvent event) {
//這里獲取到的新的值
String pwd = redisProperties.getPassword();
System.out.print("new pwd: " + pwd);
}上面的EnvironmentChangeEvent這個(gè)事件發(fā)生時(shí),@RefreshScope的bean實(shí)例還是老的bean,在這個(gè)事件里拿到的還是老的值:
@EventListener(classes = EnvironmentChangeEvent.class)
public void updateChange(EnvironmentChangeEvent event) {
Set<String> updatedKeys = event.getKeys();
System.out.print(updatedKeys);
for (String key : updatedKeys) {
if (key.equals("redis.password")) {
System.out.print("new password: " + context.getEnvironment().getProperty(key));
// do something
}
}
//這里獲取到的還是舊的值
String pwd = redisProperties.getPassword();
System.out.print("old pwd: " + pwd);
}具體是怎樣刷新scope域的,后面有時(shí)間再專門講解。
以上就是詳解SpringCloud是如何動(dòng)態(tài)更新配置的的詳細(xì)內(nèi)容,更多關(guān)于SpringCloud動(dòng)態(tài)更新配置的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J(rèn)樣式的移除方法
這篇文章主要給大家介紹了關(guān)于struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J(rèn)樣式的移除方法,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10
springboot?整合表達(dá)式計(jì)算引擎?Aviator?使用示例詳解
本文詳細(xì)介紹了Google?Aviator?這款高性能、輕量級的?Java?表達(dá)式求值引擎,并通過詳細(xì)的代碼操作演示了相關(guān)API的使用以及如何在springboot項(xiàng)目中進(jìn)行集成,感興趣的朋友一起看看吧2024-08-08
Spring Boot參數(shù)校驗(yàn)及分組校驗(yàn)的使用教程
在日常的開發(fā)中,參數(shù)校驗(yàn)是非常重要的一個(gè)環(huán)節(jié),嚴(yán)格參數(shù)校驗(yàn)會(huì)減少很多出bug的概率,增加接口的安全性,下面這篇文章主要給大家介紹了關(guān)于Spring Boot參數(shù)校驗(yàn)及分組校驗(yàn)使用的相關(guān)資料,需要的朋友可以參考下2021-08-08
SpringMVC中Controller層獲取前端請求參數(shù)的方式匯總
這篇文章主要介紹了SpringMVC中Controller層獲取前端請求參數(shù)的幾種方式,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08

