SpringBoot Starter依賴原理與實(shí)例詳解
1 Starter
在開發(fā) SpringBoot 項(xiàng)目的時(shí)候,我們常常通過 Maven 導(dǎo)入自動(dòng)各種依賴,其中很多依賴都是以 xxx-starter 命名的。
像這種 starter 依賴是怎么工作的呢?
2 了解 spring.factories機(jī)制
導(dǎo)入一個(gè)依賴,我們就可以調(diào)用包內(nèi)的公共類,這是因?yàn)楣差惪梢员划惏{(diào)用。很多時(shí)候我們添加依賴它會(huì)自動(dòng)往我們的主程序注入一些對(duì)象或者監(jiān)聽器,這個(gè)是怎么做到的?
2.1 不同包路徑下的依賴注入
SpringBoot
默認(rèn)只掃描啟動(dòng)類所在目錄里面的對(duì)象
而我們導(dǎo)入的依賴是在另外一個(gè)包里,SpringBoot
是掃描不到的!
如何讓主項(xiàng)目注入(加載)異包對(duì)象呢?通常有兩種方法:
- 在啟動(dòng)類上加上
@SpringBootApplication
注解,配置scanBasePackages
屬性,指定掃描路徑。 - 在
resources/META-INF
目錄下創(chuàng)建spring.factories
配置文件,在里面配置需要加載的類
2.2 spring.factories 機(jī)制
spring.factories
機(jī)制是springboot
的核心基礎(chǔ)之一,這可以描述為一種 可插拔結(jié)構(gòu),模仿自java
中的SPI
擴(kuò)展機(jī)制。
spring.factories 實(shí)現(xiàn)例子
1.在任意一個(gè)項(xiàng)目中新建一個(gè)starter
模塊(springboot
項(xiàng)目)
導(dǎo)入 springboot 的自動(dòng)配置依賴,這里我們主要用到它的@Configuration
、@Bean
注解和ApplicationListener
監(jiān)聽器接口
<!-- SpringBoot 自動(dòng)配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency>
2.隨便創(chuàng)建一個(gè)bean
類
public class User { private String name; public void setName(String name) { this.name = name; } public String getName() { return name; } }
3.創(chuàng)建一個(gè)初始化監(jiān)聽器
一般的starter
會(huì)在容器啟動(dòng)時(shí)做一些初始化的操作,這里作為演示只打印一句話。
public class ApplicationInitialize implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { System.out.println("應(yīng)用初始化"); } }
4.創(chuàng)建配置類
這個(gè)配置類就是主項(xiàng)目注入starter
模塊依賴的入口,當(dāng)它掃描到這個(gè)配置類的時(shí)候就會(huì)加載里面的Bean
對(duì)象
@Configuration public class StarterGenericConfig { @Bean public User getUser() { User user = new User(); user.setName("我來自starter模塊"); return user; } @Bean public ApplicationInitialize getAppli() { return new ApplicationInitialize(); } }
5.創(chuàng)建spring.factories
配置文件
配置類有了,但是因?yàn)楹椭黜?xiàng)目不同包啟動(dòng)類它掃描不到,這時(shí)我們就要通過spring.factories
機(jī)制讓它能掃描到這個(gè)配置類,完成依賴注入。
先在resource
資源目錄下創(chuàng)建META-INF
文件夾,然后創(chuàng)建一個(gè)名為spring.factories
的文件
內(nèi)容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.echoo.cloud.nacos.starter.config.StarterGenericConfig
這里需要配置StarterGenericConfig
配置類的全限定名。
把這個(gè)項(xiàng)目導(dǎo)入主項(xiàng)目(添加到主項(xiàng)目的pom
依賴中),運(yùn)行主項(xiàng)目,看看是否注入成功
這就是starter
依賴注入的基本思路,實(shí)際可能復(fù)雜得多,需要繼續(xù)摸索。
3 spring.factories 機(jī)制的實(shí)現(xiàn)源碼分析
在 springframework
框架中有這樣一個(gè)類
package org.springframework.core.io.support; public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap(); ... /** SpringFactories * 靜態(tài)方法, 加載spring.factories文件 * */ private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { ... try { // 通過類加載器加載資源目錄下的"META-INF/spring.factories"文件 Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryImplementationName = var9[var11]; result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } } ... }
SpringFactoriesLoader
是Spring
容器初始化時(shí)會(huì)加載的一個(gè)類,而它的靜態(tài)方法loadSpringFactories()
里會(huì)用類加載器去加載資源文件resourece/META-INF/spring.factories
,然后讀取里面的配置參數(shù)(應(yīng)該都是待加載Bean
類的映射數(shù)據(jù)),用集合封裝返回Spring
容器,后面應(yīng)該就是Spring
容器加載對(duì)應(yīng)的Bean
類。
4 程序運(yùn)行入口run()
前面知道了Spring
容器會(huì)加載加載資源文件resourece/META-INF/spring.factories
然后加載里面對(duì)應(yīng)的類,那為什么對(duì)應(yīng)的key
是org.springframework.boot.autoconfigure.EnableAutoConfiguration
?
先說結(jié)論:Spring
容器初始化會(huì)加載org.springframework.boot.autoconfigure.EnableAutoConfiguration
這個(gè)類,完了還會(huì)去掃描resourece/META-INF/spring.factories
加載里面的Bean
類,配置文件是鍵值對(duì)形式的,那key
用org.springframework.boot.autoconfigure.EnableAutoConfiguration
是因?yàn)檫@個(gè)是一個(gè)注解,本身就是為了注入拓展類用的,它會(huì)在容器初始化或刷新的適當(dāng)時(shí)機(jī)注入對(duì)應(yīng)的類。因?yàn)閽呙璨坏疆惏渲妙惿系?code>@Configuration注解,所以創(chuàng)建了一個(gè)@EnableAutoConfiguration
注解配合spring.factories
配置文件的形式來注入配置類
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); // 創(chuàng)建stopWatch對(duì)象 stopWatch.start(); // 開始計(jì)算時(shí)間 ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList(); this.configureHeadlessProperty(); // 是否使用模擬輸入輸出設(shè)備(默認(rèn)是,因?yàn)榉?wù)器不一定有鼠標(biāo)鍵盤顯示器) SpringApplicationRunListeners listeners = this.getRunListeners(args); // 獲取并啟動(dòng)監(jiān)聽器 listeners.starting(); // 獲取的監(jiān)聽器為 Event PublishingRunListener,監(jiān)聽并發(fā)布啟動(dòng)事件 Collection exceptionReporters; try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 準(zhǔn)備應(yīng)用環(huán)境 ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); this.configureIgnoreBeanInfo(environment); Banner printedBanner = this.printBanner(environment); // 打印 banner context = this.createApplicationContext(); 創(chuàng)建容器 // 加載 SpringFactories 實(shí)例(返回的是實(shí)例加載的記錄、報(bào)告) exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context); // 準(zhǔn)備上下文環(huán)境 this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); this.refreshContext(context); // 刷新容器 this.afterRefresh(context, applicationArguments); // 容器刷新后的動(dòng)作,這里默認(rèn)沒有做任何實(shí)現(xiàn) stopWatch.stop(); // 停止計(jì)算時(shí)間 if (this.logStartupInfo) { (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); } listeners.started(context); this.callRunners(context, applicationArguments); } catch (Throwable var10) { ... } ... }
里面有個(gè)創(chuàng)建應(yīng)用容器的方法createApplicationContext()
,深入進(jìn)去發(fā)現(xiàn)他是根據(jù)webApplicationType
類型去決定創(chuàng)建那種容器,而webApplicationType
類型在SpringApplication
初始化的時(shí)候指定。
protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch(this.webApplicationType) { case SERVLET: contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext"); break; case REACTIVE: contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"); break; default: contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext"); } } catch (ClassNotFoundException var3) { throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3); } } // 反射創(chuàng)建容器實(shí)例 return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass); }
這里一共有三種類型的容器
SERVLET
類型創(chuàng)建AnnotationConfigServletWebServerApplicationContext
(Servlet
容器)REACTIVE
類型創(chuàng)建AnnotationConfigReactiveWebServerApplicationContext
(Reactive
容器)- 默認(rèn)創(chuàng)建
AnnotationConfigApplicationContext
(Application
容器)
SpringApplication
的構(gòu)造器中對(duì)webApplicationType
類型進(jìn)行了初始化,默認(rèn)返回SERVLET
類型。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { ... this.webApplicationType = WebApplicationType.deduceFromClasspath(); ... }
也就是說默認(rèn)創(chuàng)建AnnotationConfigServletWebServerApplicationContext
(Servlet
容器)
在上面入口方法run()
方法中,有一個(gè)refreshContext()
方式,這個(gè)刷新容器的方法里面
跟蹤這個(gè)refreshContext()
底層是一個(gè)refresh()
方法,三種容器都分別實(shí)現(xiàn)了這個(gè)方法
這里著重看ServletWebServerApplicationContext.refresh()
public final void refresh() throws BeansException, IllegalStateException { try { super.refresh(); } catch (RuntimeException var3) { ... } }
發(fā)現(xiàn)它調(diào)用的是父類AbstractApplicationContext
的refresh()
函數(shù)
public void refresh() throws BeansException, IllegalStateException { synchronized(this.startupShutdownMonitor) { // 上鎖,防止并發(fā) this.prepareRefresh(); // 刷新準(zhǔn)備工作,記錄開始時(shí)間,校驗(yàn)配置文件 ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); // 獲取Bean工廠 this.prepareBeanFactory(beanFactory); // Bean工廠準(zhǔn)備工作,不詳談 try { this.postProcessBeanFactory(beanFactory); this.invokeBeanFactoryPostProcessors(beanFactory); // Spring拓展點(diǎn)之一 ... } catch (BeansException var9) { ... } finally { this.resetCommonCaches(); } } }
重點(diǎn)在invokeBeanFactoryPostProcessors(beanFactory)
方法上,這是SpringBoot
實(shí)現(xiàn)Spring
拓展的關(guān)鍵節(jié)點(diǎn),這個(gè)方法執(zhí)行時(shí)會(huì)調(diào)用實(shí)現(xiàn)了BeanFactoryPostProcessors
接口的實(shí)現(xiàn)類的postProcessBeanFactory(factory)
方法
(也會(huì)調(diào)用BeanDefinitionRegistryPostProcessor
接口的各個(gè)實(shí)現(xiàn)類的postProcessBeanDefinitionRegistry(registry)
方法)
進(jìn)入invokeBeanFactoryPostProcessors(beanFactory)
方法
public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) { ... currentRegistryProcessors = new ArrayList(); // 獲取所有 BeanDefinitionRegistryPostProcessor 接口實(shí)現(xiàn)類的全限定名集合 postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); String[] var16 = postProcessorNames; var9 = postProcessorNames.length; int var10; String ppName; for(var10 = 0; var10 < var9; ++var10) { ppName = var16[var10]; if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { // 根據(jù) Bean 名獲取 Bean 對(duì)象放入 currentRegistryProcessors currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } sortPostProcessors(currentRegistryProcessors, beanFactory); // 排序,暫時(shí)沒看,不知道排啥 registryProcessors.addAll(currentRegistryProcessors); // 調(diào)用 Bean 定義注冊(cè)后處理器 invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); ... }
每一個(gè)Bean
類的定義注冊(cè)是在Spring
容器中完成的,在上面invokeBeanFactoryPostProcessors()
方法中,通過 Bean 工廠獲取 了所有BeanDefinitionRegistryPostProcessor
接口的實(shí)現(xiàn)類名,然后再通過 invokeBeanDefinitionRegistryPostProcessors()
方法調(diào)用所有實(shí)現(xiàn)類的postProcessBeanDefinitionRegistry()
方法去做 Bean 類注冊(cè)后的相關(guān)處理動(dòng)作。
BeanDefinitionRegistryPostProcessor
接口:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException; }
知道這個(gè)有什么用呢?前面我們知道了Springboot
容器中的AnnotationConfigServletWebServerApplicationContext
(Servlet
容器)是通過反射獲取AnnotationConfigServletWebServerApplicationContext
的構(gòu)造器創(chuàng)建實(shí)例的,所以我們看看AnnotationConfigServletWebServerApplicationContext
的構(gòu)造器長(zhǎng)什么樣兒。
public AnnotationConfigServletWebServerApplicationContext(DefaultListableBeanFactory beanFactory) { super(beanFactory); this.annotatedClasses = new LinkedHashSet(); this.reader = new AnnotatedBeanDefinitionReader(this); // 注解 Bean 定義讀取器 this.scanner = new ClassPathBeanDefinitionScanner(this); }
進(jìn)入AnnotatedBeanDefinitionReader
看它的構(gòu)造器
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) { ... // 通過 AnnotationConfigUtils 工具注冊(cè) 注解配置處理器 AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); }
再進(jìn)入AnnotationConfigUtils
工具的registerAnnotationConfigProcessors()
看看它是如何注冊(cè)的
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) { ... Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet(8); // 存儲(chǔ) BeanDefinitionHolder 對(duì)象的集合 RootBeanDefinition def; if (!registry.containsBeanDefinition("org.springframework.context.annotation.internalConfigurationAnnotationProcessor")) { // 創(chuàng)建一個(gè) ConfigurationClassPostProcessor 的 RootBeanDefinition 對(duì)象 def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); def.setSource(source); // 把 ConfigurationClassPostProcessor 的 RootBeanDefinition 對(duì)象裝入一個(gè) BeanDefinitionHolder (容器) // 并映射名字為 org.springframework.context.annotation.internalConfigurationAnnotationProcessor beanDefs.add(registerPostProcessor(registry, def, "org.springframework.context.annotation.internalConfigurationAnnotationProcessor")); } ... }
在它的registerAnnotationConfigProcessors()
方法中看到了它用注冊(cè)后處理器registerPostProcessor
去注冊(cè)org.springframework.context.annotation.internalConfigurationAnnotationProcessor
的BeanDefinition
(Bean定義描述對(duì)象)和BeanDefinitionHolder
(Bean定義描述對(duì)象容器),然后返回這個(gè)BeanDefinitionHolder
(Bean定義描述對(duì)象容器)存儲(chǔ)到beanDefs
(Bean定義描述對(duì)象容器集合)里面。
到這里就是說明在初始化AnnotationConfigServletWebServerApplicationContext
(Servlet
容器)時(shí),會(huì)用org.springframework.context.annotation.internalConfigurationAnnotationProcessor
這個(gè)名字注冊(cè)ConfigurationClassPostProcessor
這個(gè) Bean
對(duì)象,然后就能根據(jù)它的BeanDefinitionHolder
(Bean定義描述對(duì)象容器)去創(chuàng)建ConfigurationClassPostProcessor
對(duì)象。
現(xiàn)在問題就來到了ConfigurationAnnotationProcessor
對(duì)象身上了,為啥要?jiǎng)?chuàng)建它?因?yàn)樗褪羌虞dspring.factories
配置文件的關(guān)鍵。
進(jìn)入它的postProcessBeanDefinitionRegistry()
方法
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { int registryId = System.identityHashCode(registry); if (this.registriesPostProcessed.contains(registryId)) { // 判斷是否有對(duì)應(yīng)的注冊(cè)記錄 throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); } else if (this.factoriesPostProcessed.contains(registryId)) { // 是否有對(duì)應(yīng)的 Bean 工廠 throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry); } else { // 都沒有,說明這個(gè) BeanDefinition 沒有注冊(cè)加載過 this.registriesPostProcessed.add(registryId); // 添加注冊(cè)記錄 this.processConfigBeanDefinitions(registry); // 處理這個(gè) BeanDefinition 的配置 } }
深入processConfigBeanDefinitions()
看它怎么處理這個(gè) BeanDefinition
的配置
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList(); // 待處理配置集合 String[] candidateNames = registry.getBeanDefinitionNames(); String[] var4 = candidateNames; int var5 = candidateNames.length; for(int var6 = 0; var6 < var5; ++var6) { String beanName = var4[var6]; BeanDefinition beanDef = registry.getBeanDefinition(beanName); // 根據(jù)名稱獲取 BeanDefinition // 判斷這個(gè) BeanDefinition 的配置屬性是不是空 if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (this.logger.isDebugEnabled()) { // 如果不是空就說明這個(gè) BeanDefinition 已經(jīng)被當(dāng)作配置類處理過了 this.logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } } else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { // 如果為空,放入待處理配置集合里等待后續(xù)處理 configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // 指令排序相關(guān),不深究 if (!configCandidates.isEmpty()) { configCandidates.sort((bd1, bd2) -> { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); }); ... // 根據(jù)環(huán)境創(chuàng)建了一個(gè)配置類解析器 ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet(configCandidates); HashSet alreadyParsed = new HashSet(configCandidates.size()); do { parser.parse(candidates); // 解析配置類 parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } ... } while(!candidates.isEmpty()) ... } }
進(jìn)入ConfigurationClassParser.parse()
方法,看看它怎么解析配置類
public void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { // 對(duì) BeanDefinition 做解析操作 if (bd instanceof AnnotatedBeanDefinition) { parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); } else { parse(bd.getBeanClassName(), holder.getBeanName()); } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); } } // 延遲導(dǎo)入選擇器 this.deferredImportSelectorHandler.process(); }
解析發(fā)現(xiàn)不了什么線索,進(jìn)入this.deferredImportSelectorHandler.process()
延遲導(dǎo)入選擇處理器看看
層層追蹤:
this.deferredImportSelectorHandler.process()
↓
DeferredImportSelectorGroupingHandler.processGroupImports()
↓
grouping.getImports()
↓
grouping.getImports()
↓
this.group.selectImports()
追蹤到public interface ImportSelector {...}
接口
在找到它的實(shí)現(xiàn)類AutoConfigurationImportSelector
在實(shí)現(xiàn)類AutoConfigurationImportSelector
里層層追蹤
selectImports()
→
getAutoConfigurationEntry()
→
getCandidateConfigurations()
→
getSpringFactoriesLoaderFactoryClass()
最后追蹤到 getSpringFactoriesLoaderFactoryClass()
方法
protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }
里面返回的是一個(gè)EnableAutoConfiguration.class
類,這個(gè)類就是我們?cè)?code>spring.factories配置文件里面配置的org.springframework.boot.autoconfigure.EnableAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.echoo.cloud.nacos.starter.config.StarterGenericConfig
到此這篇關(guān)于SpringBoot Starter依賴原理與實(shí)例詳解的文章就介紹到這了,更多相關(guān)SpringBoot Starter依賴內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java swing實(shí)現(xiàn)簡(jiǎn)單計(jì)算器界面
這篇文章主要為大家詳細(xì)介紹了java swing實(shí)現(xiàn)簡(jiǎn)單計(jì)算器界面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Java中高效的判斷數(shù)組中某個(gè)元素是否存在詳解
相信大家在操作Java的時(shí)候,經(jīng)常會(huì)要檢查一個(gè)數(shù)組(無序)是否包含一個(gè)特定的值?這是一個(gè)在Java中經(jīng)常用到的并且非常有用的操作。同時(shí),這個(gè)問題在Stack Overflow中也是一個(gè)非常熱門的問題。本文將分析幾種常見用法及其時(shí)間成本,有需要的朋友們可以參考借鑒。2016-11-11Spring學(xué)習(xí)之開發(fā)環(huán)境搭建的詳細(xì)步驟
本篇文章主要介紹了Spring學(xué)習(xí)之開發(fā)環(huán)境搭建的詳細(xì)步驟,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07Java通過反射將 Excel 解析成對(duì)象集合實(shí)例
這篇文章主要介紹了Java通過反射將 Excel 解析成對(duì)象集合實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08webuploader 實(shí)現(xiàn)圖片批量上傳功能附實(shí)例代碼
這篇文章主要介紹了webuploader 實(shí)現(xiàn)圖片批量上傳功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-11-11