SpringBoot自動(dòng)配置@EnableAutoConfiguration過程示例
自動(dòng)配置vs自動(dòng)裝配
Spring有兩個(gè)名字聽起來相似的概念:一個(gè)是自動(dòng)裝配,一個(gè)是自動(dòng)配置。他們兩個(gè)只是聽起來相似,實(shí)際根本不同。自動(dòng)裝配是autowire,自動(dòng)配置是autoconfiguration,他們之間沒有任何關(guān)系,概念也相差甚遠(yuǎn)。
Springboot的自動(dòng)配置通過@EnableAutoConfiguration注解實(shí)現(xiàn),@EnableAutoConfiguration是Springboot的注解,通過@EnableAutoConfiguration注解,Springboot實(shí)現(xiàn)了“約定大于配置”:系統(tǒng)猜測你需要哪些配置,從而幫助你自動(dòng)實(shí)現(xiàn)了這些配置,所以程序員就不需要手動(dòng)再進(jìn)行配置了。
這個(gè)說起來非常簡單的約定,實(shí)際上卻大大簡化了程序員的日常工作,讓Spring項(xiàng)目變的非常簡單。
@SpringBootApplication
學(xué)習(xí)SpringBoot,還是要從SpringBoot的入門注解@SpringBootApplication開始:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {可以看到@SpringBootApplication是一個(gè)組合注解,由@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan組成。
@SpringBootConfiguration的主要作用其實(shí)就是實(shí)現(xiàn)@Configuration注解的功能,告訴Spring當(dāng)前類是一個(gè)配置類。
然后再加上@ComponentScan注解,告訴Spring容器包掃描路徑,由于我們在@SpringBootApplication注解上其實(shí)并沒有指定包掃描路徑,所以,Spring容器會(huì)采用默認(rèn)的包掃描路徑:當(dāng)前配置類所在的路徑。因此才有了一個(gè)約定:SpringBoot的啟動(dòng)類(其實(shí)準(zhǔn)確的叫法應(yīng)該是配置類,配置類不一定必須是啟動(dòng)類,編寫一個(gè)其他的配置類送給SpringApplication的run方法也是一樣可以啟動(dòng)SpringBoot的)要放在項(xiàng)目的主路徑下,因?yàn)椴环旁谥髀窂较隆⒍欠旁谔囟ò碌脑?,就可能?huì)導(dǎo)致除這個(gè)特定包之外的其他包下的類不能被Spring容器掃描到。
然后最重要的是這個(gè)@EnableAutoConfiguration注解,@EnableAutoConfiguration也是一個(gè)組合注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {主要包含兩個(gè)注解:@AutoConfigurationPackage和@Import,比較重要的是這個(gè)@Import注解。
其實(shí)我們可以看到,SpringBoot項(xiàng)目啟動(dòng)時(shí)的幾個(gè)注解實(shí)際上都是由Spring注解組合生成的,所以說SpringBoot其實(shí)不算是什么新技術(shù),只不過是Spring框架的一個(gè)應(yīng)用,而已。
@EnableAutoConfiguration
接下來我們要重點(diǎn)分析一下@EnableAutoConfiguration注解,因?yàn)樗菍?shí)現(xiàn)“約定大于配置”思想的關(guān)鍵。
前面已經(jīng)看到了,@EnableAutoConfiguration注解中最重要的其實(shí)是@Import注解,所以我們先對@Import注解做一個(gè)簡單的了解,之后再回來深入學(xué)習(xí)@EnableAutoConfiguration注解。
@Import注解
@Import注解和@Configuration注解一起作用,放在@Configuration配置類中,作用是通過該配置類引入Bean到Spring IoC容器中。
比如:
@Configuration
@Import(Person.class)
public class MyConfiguration(){
}
public class Person(){
@Bean
public Person createPerson(){
return new Person();
}
}以上配置類通過@Import注解將Person對象注入到SpringIoC容器中。
@Import需要通過參數(shù)value指定引入類。
@Import是在Spring Ioc容器刷新的時(shí)候、通過BeanFactoryPostProcessors生效的。具體的調(diào)用路徑:
Spring容器refresh方法 -> AbstractApplicationContext.refresh()->
invokeBeanFactoryPostProcessors()->
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors->
ConfigurationClassPostProcessor.processConfigBeanDefinitions ->
ConfigurationClassParser.parse()->
processConfigurationClass()->
doProcessConfigurationClass()
調(diào)用鏈路特別長,代碼也特別多,所以我們今天也就沒有辦法分析全部代碼。但是看到這個(gè)doProcessConfigurationClass就感覺快要找到核心代碼了,因?yàn)镾pring源碼的特點(diǎn),doxxxx方法是干實(shí)事的。
確實(shí),這個(gè)ConfigurationClassParser類是專門用來處理配置類的,也就是加了@Configuration注解的類,@Import注解也是這個(gè)類負(fù)責(zé)解析,這也是@Import注解必須和@Configuration注解綁定才能生效的原因。
doProcessConfigurationClass方法源碼也特別長,首先處理@Component、@PropertySource、@ComponentScan注解,之后調(diào)用processImports方法處理@Import注解。
processImports方法是正兒八經(jīng)處理@Import注解的地方。
代碼也比較長,我們還是簡單一點(diǎn),只關(guān)心比較重要的部分:
for (SourceClass candidate : importCandidates) {
//1.如果@Import的類實(shí)現(xiàn)了ImportSelector接口
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
if (selector instanceof DeferredImportSelector deferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
//2.如果@Import的類實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
//3.@Import引入的是普通類
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}代碼中加了注釋,其實(shí)主要關(guān)注的是@Import注解支持引入三種類型的類:
- 第一種是實(shí)現(xiàn)了ImportSelector接口的類,這種情況下又判斷是否實(shí)現(xiàn)了DeferredImportSelector接口,會(huì)有不同的處理邏輯,實(shí)現(xiàn)了DeferredImportSelector接口的類,最終會(huì)調(diào)用到其內(nèi)部接口DeferredImportSelector.Group的process方法,否則,如果沒有實(shí)現(xiàn)DeferredImportSelector接口,則會(huì)調(diào)用到ImportSelector的getImports方法。
- 第二種是實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口的類,通過將BeanDefinition注冊到Spring容器中從而實(shí)現(xiàn)bean加載。
- 第三種是普通類,可以直接加載到SpringIoC容器中。
需要注意的是第一種情況中的DeferredImportSelector接口:
A variation of ImportSelector that runs after all @Configuration beans have been processed. This type of selector can be particularly useful when the selected imports are @Conditional.
Implementations can also extend the org.springframework.core.Ordered interface or use the org.springframework.core.annotation.Order annotation to indicate a precedence against other DeferredImportSelectors.
Implementations may also provide an import group which can provide additional sorting and filtering logic across different selectors.
Since:
4.0
Author:
Phillip Webb, Stephane NicollDeferredImportSelector接口實(shí)現(xiàn)類的延遲導(dǎo)入,在所有的@Configuration配置類全部處理之后再運(yùn)行。這個(gè)特性對于@Conditional注解下的條件配置很有用。
實(shí)現(xiàn)類也可以擴(kuò)展Order接口實(shí)現(xiàn)對延遲導(dǎo)入配置類的排序,也可以提供一個(gè)導(dǎo)入的group實(shí)現(xiàn)排序(Group是DeferredImportSelector的子接口)。
以上是DeferredImportSelector類的javaDoc的解釋,基本上說清楚了DeferredImportSelector接口的作用,我們今天理解到這個(gè)程度就OK了,不影響我們對SpringBoot自動(dòng)配置原理的理解了。
好了,對于@Import注解的學(xué)習(xí)就到這里了,有了對@Import注解的基本認(rèn)識(shí),我們就可以繼續(xù)深入分析@EnableAutoConfiguration注解了。
@EnableAutoConfiguration
繼續(xù)研究@EnableAutoConfiguration注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {@Import注解引入了AutoConfigurationImportSelector類,這個(gè)類是SpringBoot自動(dòng)配置的關(guān)鍵實(shí)現(xiàn)類:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {發(fā)現(xiàn)AutoConfigurationImportSelector類實(shí)現(xiàn)了DeferredImportSelector接口,根據(jù)上面我們對@Import注解的分析,Spring容器的refresh方法最終會(huì)調(diào)用到DeferredImportSelector.Group的process方法,我們需要從AutoConfigurationImportSelector源碼中找一下process方法的實(shí)現(xiàn)。
發(fā)現(xiàn)AutoConfigurationImportSelector類實(shí)現(xiàn)了一個(gè)AutoConfigurationGroup類,該類實(shí)現(xiàn)了eferredImportSelector.Group接口,我們找到它的process方法:
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}process方法強(qiáng)轉(zhuǎn)參數(shù)deferredImportSelector為AutoConfigurationImportSelector后,調(diào)用getAutoConfigurationEntry方法,拿到需要實(shí)例化并放入SpringIoC容器中的類的全限定名之后,返回給Spring容器(我們肯定知道Bean的實(shí)例化不會(huì)在當(dāng)前階段完成、一定是Spring IoC容器統(tǒng)一完成的(還記得嗎,入口方法是getBean))。
getAutoConfigurationEntry方法源碼會(huì)揭露SpringBoot自動(dòng)裝配實(shí)現(xiàn)機(jī)制的真相:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//找到自動(dòng)配置,獲取需要自動(dòng)配置的類
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//去重
configurations = removeDuplicates(configurations);
//根據(jù)排除配置,進(jìn)行排除
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//filter方法,實(shí)現(xiàn)條件配置,根據(jù)條件排除掉不必要的配置
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}上面加了備注的方法是關(guān)鍵方法,我們一個(gè)個(gè)看一下。
getCandidateConfigurations
從方法名上可以看出,是要獲取到備選的配置項(xiàng)。
代碼非常簡單:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}關(guān)鍵方法loadFactoryNames:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}又調(diào)用了方法loadSpringFactories:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//讀取配置文件META-INF/spring.factories
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
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[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}我們先有一個(gè)認(rèn)識(shí):類加載器可以在當(dāng)前類加載上下文環(huán)境下找到指定位置的配置文件。
代碼雖然比較長,但是實(shí)現(xiàn)的功能比較簡單:通過類加載器讀取配置文件“META-INF/spring.factories”,加工配置文件內(nèi)容、放入Map之后返回。
我們簡單看一眼META-INF/spring.factories文件的內(nèi)容:

其中的org.springframework.boot.autoconfigure.AutoConfigurationImportFilter、以及org.springframework.boot.autoconfigure.EnableAutoConfiguration是key,后面的內(nèi)容是value(是一個(gè)list),讀取該文件的配置后最終以map的形式返回。
這樣的話,getCandidateConfigurations方法就通過類加載器返回配置在META-INF/spring.factories文件中的內(nèi)容并返回。
這段代碼邏輯其實(shí)就是SpringBoot自動(dòng)配置的核心內(nèi)容,SpringBoot幫助程序員完成配置的機(jī)密就在這個(gè)META-INF/spring.factories文件中,其實(shí)如果沒有META-INF/spring.factories文件,SpringBoot也不可能知道他究竟要自動(dòng)加載哪些Bean!
getExclusions
getExclusions方法完成的功能比較簡單,就是檢查@Import注解是否有exclude的配置,根據(jù)配置對返回的自動(dòng)配置結(jié)合進(jìn)行排除。
getConfigurationClassFilter().filter
getConfigurationClassFilter().filter方法也非常關(guān)鍵,因?yàn)榕渲梦募﨧ETA-INF/spring.factories中的自動(dòng)配置類非常多,但是我們的項(xiàng)目中不一定需要,比如RabbitMQ,在META-INF/spring.factories文件中就有關(guān)于RabbitMQ的配置,但是如果我們的項(xiàng)目不需要mq的話,SpringBoot就沒有必要加載他。
SpringBoot就是通過filter方法來過濾上述不必要加載的組件的。過濾的方法是:
- SpringBoot通過META-INF/spring.factories配置文件指定了AutoConfigurationImportFilter過濾器
- filter方法中通過過濾器,以及各組件的自動(dòng)配置類完成過濾
比如,RabbitMQ的自動(dòng)配置類:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import({ RabbitAnnotationDrivenConfiguration.class, RabbitStreamConfiguration.class })
public class RabbitAutoConfiguration {其中@ConditionalOnClass注解,指定如果當(dāng)前項(xiàng)目中有RabbitTemplate.class, 以及Channel.class,兩個(gè)類存在的情況下就執(zhí)行自動(dòng)配置,否則,如果不存在的話,RabbitMQ的自動(dòng)配置類RabbitAutoConfiguration就會(huì)被這個(gè)filter方法過濾掉,不會(huì)被Spring容器加載進(jìn)來。
而這個(gè)“過濾掉”的具體工作,就是由AutoConfigurationImportFilter過濾器完成的。
OK,SpringBoot自動(dòng)配置的實(shí)現(xiàn)原理就搞清楚了!
但是還是有必要提一下SpringBoot3.0,實(shí)現(xiàn)自動(dòng)配置的邏輯和我們上面看到的有變化。
SpringBoot3.0
如果你的項(xiàng)目使用的是SpringBoot3.0,你會(huì)發(fā)現(xiàn)我上面寫的這些內(nèi)容是在胡說。
因?yàn)閴焊蜎]有META-INF/spring.factories這個(gè)東西!
META-INF/spring.factories其實(shí)是在SpringBoot2.7之后就被移除了。
看一下源碼,從getCandidateConfigurations開始:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();
Assert.notEmpty(configurations,
"No auto configuration classes found in "
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}關(guān)鍵是這個(gè)ImportCandidates.load方法:
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
//獲取類加載器
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
//在類加載器的幫助下,獲取到指定位置的配置
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}要找到的就是location指定的文件,其中LOCATION定義為:
private static final String LOCATION = "META-INF/spring/%s.imports";
發(fā)現(xiàn)配置文件META-INF/spring.factories被META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports取代!
這個(gè)變化對既有項(xiàng)目的影響很大!因?yàn)殡m然SpringBoot自己的自動(dòng)配置很容易就完成了修改,但是你項(xiàng)目中使用的那些第三方包不一定能夠無縫支持!
有關(guān)第三方包的自動(dòng)配置,請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
手把手教你用Java實(shí)現(xiàn)一套簡單的鑒權(quán)服務(wù)
現(xiàn)今大部分系統(tǒng)都會(huì)有自己的鑒權(quán)服務(wù),本文介紹了最常用的鑒權(quán)服務(wù),就是日常用戶的登錄登出,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
springboot 設(shè)置server.port不生效的原因及解決
這篇文章主要介紹了springboot 設(shè)置server.port不生效的原因及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
SpringBoot+Vue項(xiàng)目部署實(shí)現(xiàn)傳統(tǒng)方式
我們在進(jìn)行前后端分離開發(fā)的時(shí)候,一般是將前端項(xiàng)目部署到nginx服務(wù)器上,與后端項(xiàng)目分開部署,這篇文章主要給大家介紹了關(guān)于SpringBoot+Vue項(xiàng)目部署實(shí)現(xiàn)傳統(tǒng)方式的相關(guān)資料,需要的朋友可以參考下2024-01-01
Java 實(shí)戰(zhàn)范例之進(jìn)銷存管理系統(tǒng)的實(shí)現(xiàn)
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+vue+Springboot+ssm+mysql+maven+redis實(shí)現(xiàn)一個(gè)前后端分離的進(jìn)銷存管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11
Spring三級緩存思想解決循環(huán)依賴總結(jié)分析
這篇文章主要為大家介紹了Spring三級緩存思想解決循環(huán)依賴總結(jié)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
SpringBoot整合Redis、ApachSolr和SpringSession的示例
本篇文章主要介紹了SpringBoot整合Redis、ApachSolr和SpringSession的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02
Java+Selenium實(shí)現(xiàn)控制瀏覽器的啟動(dòng)選項(xiàng)Options
這篇文章主要為大家詳細(xì)介紹了如何使用java代碼利用selenium控制瀏覽器的啟動(dòng)選項(xiàng)Options的代碼操作,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-01-01

