詳解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存儲配置內(nèi)容
- Config client 是config server的客戶端,用于操作存儲在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-10springboot?整合表達(dá)式計(jì)算引擎?Aviator?使用示例詳解
本文詳細(xì)介紹了Google?Aviator?這款高性能、輕量級的?Java?表達(dá)式求值引擎,并通過詳細(xì)的代碼操作演示了相關(guān)API的使用以及如何在springboot項(xiàng)目中進(jìn)行集成,感興趣的朋友一起看看吧2024-08-08Spring 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-08SpringMVC中Controller層獲取前端請求參數(shù)的方式匯總
這篇文章主要介紹了SpringMVC中Controller層獲取前端請求參數(shù)的幾種方式,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08