docs/Spring全家桶/Spring源码分析/Spring重要机制探秘/ConfigurationClassPostProcessor(三):处理@Import注解.md
ConfigurationClassPostProcessor ĵƪҪǷ spring @Import עĴ̡
нģǼ spring @Import עĴ̡
@Import ע@Import עĶ壺
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration @Configuration}, {@link ImportSelector},
* {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
*/
Class<?>[] value();
}
@Import һvalue()ֵ֧ Classĵ֧ 4 ֣
@Configuration עǵImportSelectorImportBeanDefinitionRegistrarͨһ demo չʾʹ @Import νർ뵽 spring С
/**
* Element01
*/
public class Element01 {
public String desc() {
return "this is element 01";
}
}
/**
* Element02
*/
public class Element02 {
public String desc() {
return "this is element 02";
}
}
/**
* Element03
*/
public class Element03 {
public String desc() {
return "this is element 03";
}
}
/**
* Element04
*/
public class Element04 {
public String desc() {
return "this is element 04";
}
}
ImportBeanDefinitionRegistrar ࣬ element02 עpublic class Element02ImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* registerBeanDefinitions עelement02ӦBeanDefinition
* Ҳǰ Element02 Ӧ beanDefinition ֶעᵽbeanFactory
* beanDefinitionMap
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("element02", new RootBeanDefinition(Element02.class));
}
}
ImportSelector ࣬ selectImports(...) У Element03 ""public class Element03Selector implements ImportSelector {
/**
* String Ϊ .
* ںҪõ䣬˱"."
* @param importingClassMetadata
* @return
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {Element03.class.getName()};
}
}
@Configuration ע࣬ͨб @Bean ǵķ Element04@Configuration
public class Element04Configuration {
@Bean
public Element04 element04() {
return new Element04();
}
}
@EnableElement ע⣬е @Import ע Element01.class``Element02ImportBeanDefinitionRegistrar.class``Element03Selector.class``Element04Configuration.class@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
// ͨ
Element01.class,
// ʵ ImportBeanDefinitionRegistrar
Element02ImportBeanDefinitionRegistrar.class,
// ʵ ImportSelector
Element03Selector.class,
// @Configuration ǵ
Element04Configuration.class
})
public @interface EnableElement {
}
// ֻҪ @EnableElement ע
@EnableElement
public class Demo04Main {
public static void main(String[] args) {
// Demo04Main.class
ApplicationContext context = new AnnotationConfigApplicationContext(Demo04Main.class);
Element01 element01 = context.getBean(Element01.class);
System.out.println(element01.desc());
Element02 element02 = context.getBean(Element02.class);
System.out.println(element02.desc());
Element03 element03 = context.getBean(Element03.class);
System.out.println(element03.desc());
Element04 element04 = context.getBean(Element04.class);
System.out.println(element04.desc());
}
}
У£
this is element 01
this is element 02
this is element 03
this is element 04
Կ4 bean ɹ spring ͨ spring δġ
ע ConfigurationClassPostProcessor ĵƪǰƪ£ @ComponentScan ע @Bean עͬĴ룬Ļһʴˡ
ConfigurationClassPostProcessor#processConfigBeanDefinitionsḶ̌ǰƪѾᵽˣֱӿؼ룺
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
// 1. , @Component@PropertySources@ComponentScans
// @ImportResourceȵĽ
parser.parse(candidates);
parser.validate();
// ɺõ࣬ౣ parser configurationClasses
Set<ConfigurationClass> configClasses
= new LinkedHashSet<>(parser.getConfigurationClasses());
...
// 2. @Import ࡢд@Beanķ
// @ImportResource ԴתBeanDefinition
this.reader.loadBeanDefinitions(configClasses);
...
// עBeanDefinition candidateNamesбȽ
// ڵĻ˵µBeanDefinitionע
if (registry.getBeanDefinitionCount() > candidateNames.length) {
...
for (String candidateName : newCandidateNames) {
// ˱BeanDefinition
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
// 3. ӵ࣬࣬δӵcandidatesУȴ´ѭ
if (ConfigurationClassUtils.checkConfigurationClassCandidate(
bd, this.metadataReaderFactory)
&&!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
...
}
ϴ뾭һЩֻ˴ @Import ؼ裬ܽ£
࣬ @Component``@PropertySources``@ComponentScans``@ImportResource ȵĽ ǰƪҲˣǾۼ @Import ٴηһҪֻһ main() ע Demo04.class
@Import ࡢд @Bean ķ@ImportResource Դת BeanDefinitionǰ @Bean ʱҲˣҲ
ӵ࣬࣬δӵ candidates Уȴ´ѭ
ConfigurationClassParser#doProcessConfigurationClass@Import νģ ConfigurationClassParser#doProcessConfigurationClass
/**
* ķ
*/
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass,
SourceClass sourceClass) throws IOException {
// 1. @Component ע⣬ݹ鴦ڲ࣬IJע
...
// 2. @PropertySourceע⣬IJע
...
// 3. @ComponentScan/@ComponentScans ע⣬IJע
...
// 4. @Importע
processImports(configClass, sourceClass, getImports(sourceClass), true);
// 5. @ImportResourceע⣬IJע
...
// 6. @Beanע⣬IJע
...
// 7. ĸ࣬ processConfigurationClass(...) һѭʱ
...
return null;
}
@Import עõ processImports(...) Ǽ
@ImportǰĿŵ processImports(...)
processImports(configClass, sourceClass, getImports(sourceClass), true);
4
configClass࣬ demo04Main ӦࣻsourceClass demo04Main ϵעİװgetImports(sourceClass)``getImports(...) ȡ sourceClass @Import עࣻtrueֵǷѭ롣Уȡ @Import ע getImports(...) Ĺܣλȡģ
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
// ȡ
collectImports(sourceClass, imports, visited);
return imports;
}
/**
* Ļȡ
*/
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports,
Set<SourceClass> visited) throws IOException {
if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
// annotationƲimportݹ collectImports(...)
collectImports(annotation, imports, visited);
}
}
// ȡǰ @Import ע
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}
ȡ @Import ķ ConfigurationClassParser#collectImportsȡȡʽ£
@Import ע⣬ȡ @Import value ֵصһ֮ demo04Main ϵ @EnableElement עᱻȡעⲻ @Import ע⣬ͼȡ @EnableElement ϵע⣬ʱ @EnableElement @Import ע⣬ʱͻȡ @Import value() ֵҲ @Import עࡣ
к õĽ£
õĽΪ LinkedHashSetһνͼ㣬˷Ϊ 4 ͼԿ@Import ע 4 ȡˣ
@Importȡ @Import ע processImports(...)
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
...
for (SourceClass candidate : importCandidates) {
// 1. ImportSelector
if (candidate.isAssignable(ImportSelector.class)) {
Class<?> candidateClass = candidate.loadClass();
// ʵ ImportSelectorһִ Aware ӿڷ
// ֵ֧AwareBeanClassLoaderAwareBeanFactoryAwareEnvironmentAwareResourceLoaderAware
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass,
ImportSelector.class, this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// ִ selectImports ȡ࣬Ϊ"."
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// ݹ processImports(...) һδ
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// 2. ImportBeanDefinitionRegistrar
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
Class<?> candidateClass = candidate.loadClass();
// ʵ ImportBeanDefinitionRegistrarִ Aware ӿڵķ
// ֵ֧AwareBeanClassLoaderAwareBeanFactoryAwareEnvironmentAwareResourceLoaderAware
ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils
.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
// ImportBeanDefinitionRegistrar
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// 3. ߣ processConfigurationClass(...) ֱӽ
// ࣨ @Component@Configuration@Import ע⣩н
else {
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
...
}
ǾĴ룬ֻ @Import Ĺؼ裺
ImportSelector£
ImportSelectorִ֮ Aware ӿڷڴ ImportSelector ʱʵ Aware ӿڣֵ֧ Aware BeanClassLoaderAware``BeanFactoryAware``EnvironmentAware``ResourceLoaderAwareImportSelector ʵ selectImports һΪ˻ȡ࣬Ϊ Class[] ""Element03Selector и÷£@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {Element03.class.getName()};
}
һȡ Element03.class; 3. ȡ Class 飬ת SourceClass ϣһε processImports(...)ڵڶεʱ Element03
ImportBeanDefinitionRegistrar£
ImportBeanDefinitionRegistrarִ Aware ӿڵķһͬ ImportSelector ʵһImportBeanDefinitionRegistrar ʵ浽 configClass УٷʵδģͲߣ processConfigurationClass(...) ֱӽǰƪѾἰˣ @Component``@Import``@ComponentScan``@Configuration``@Bean עģһΪ˽еЩע⣬ Element01``Element02``Element03( 1 ᵽģ Element03Selector лȡ Element03.class ת SourceClassٴε processImports(...) Ĺ) һġ
Ƿ֣ ImportBeanDefinitionRegistrar ķʽ⣬뷽ʽͨࡢࡢImportSelector ʵࣩһĴʽǰĽʽյõ processConfigurationClass(...) һ ǰƪѾηˣͲٷˣ ImportBeanDefinitionRegistrar Ĵ
BeanDefinitions``ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsڷ @Bean ʱǷ ConfigurationClassBeanDefinitionReader#loadBeanDefinitions @Bean ̣ @Import ֱ̣ӽؼ ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
...
// @Import
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// @Bean
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
// @ImportResource Դ
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
// @Import ImportBeanDefinitionRegistrar
// ǰ汣configClassеImportBeanDefinitionRegistrarsʹ
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
@Import ĵط
@Import@Import ImportBeanDefinitionRegistrarǷֱδġ
@Import@Import شΪ
// @Import
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
ǽһ̽
private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) {
AnnotationMetadata metadata = configClass.getMetadata();
// BeanDefinitionΪ AnnotatedGenericBeanDefinition
AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata);
// һϵе
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
configBeanDef.setScope(scopeMetadata.getScopeName());
String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
definitionHolder = AnnotationConfigUtils
.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
// ע
this.registry.registerBeanDefinition(definitionHolder.getBeanName(),
definitionHolder.getBeanDefinition());
configClass.setBeanName(configBeanName);
}
һעᵽ BeanDefinition еḶ́ʹõ BeanDefinition AnnotatedGenericBeanDefinition
@Import ImportBeanDefinitionRegistrarù̵ķΪ loadBeanDefinitionsFromRegistrars(...)£
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar,
AnnotationMetadata> registrars) {
registrars.forEach((registrar, metadata) ->
registrar.registerBeanDefinitions(metadata, this.registry,
this.importBeanNameGenerator));
}
÷DZ ImportBeanDefinitionRegistrar ϣȻһе ImportBeanDefinitionRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry, BeanNameGenerator) ÷λ ImportBeanDefinitionRegistrar ӿڣ£
public interface ImportBeanDefinitionRegistrar {
/**
* ĬϷĬʵֽһ
*/
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
/**
* Element02ImportBeanDefinitionRegistrar ʵֵķ
*/
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
}
}
ImportBeanDefinitionRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry, BeanNameGenerator) һãյõ ImportBeanDefinitionRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)ǵ Element02ImportBeanDefinitionRegistrar ʵ˸÷
public class Element02ImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("element02", new RootBeanDefinition(Element02.class));
}
}
˴죬շ ImportBeanDefinitionRegistrar עᵽ beanDefinitionMap Լдģ
ElementXxElement01``Element02``Element03``Element04 ͵עᵽ beanDefinitionMap ˣǿһ beanDefinitionNames еݣ
Է֣Element01 Element03 beanName ͬѰ bean 뷽ʽΪ
ʹ beanFactory.get(beanName) ʱҪע⣺
// ȡᱨ
beanFactory.get("element01");
// ܻȡ
beanFactory.get("element02");
// ȡᱨ
beanFactory.get("element03");
// ܻȡ
beanFactory.get("element04");
ʹ beanFactory.get(beanName) ȡ element01 element03 Ҫȡ
// ܻȡ
beanFactory.get("org.springframework.learn.explore.demo04.element.Element01");
// ܻȡ
beanFactory.get("org.springframework.learn.explore.demo04.element.Element03");
ȻҲʹ beanFactory.get(Class) ķʽȡ
// ܻȡ
beanFactory.get(Element01.class);
// ܻȡ
beanFactory.get(Element02.class);
// ܻȡ
beanFactory.get(Element03.class);
// ܻȡ
beanFactory.get(Element04.class);
DeferredImportSelector Ĵڷ ConfigurationClassParser#processImports ʱ ImportSelector ʱôһδ룺
δж selector ǷΪ DeferredImportSelector ʵǾͰ DeferredImportSelector ͵ʵдͰͨ ImportSelector DeferredImportSelector ͨ ImportSelector кβͬ
DeferredImportSelector Ĵ£
/**
* DeferredImportSelector ImportSelectorӽӿ
*/
public interface DeferredImportSelector extends ImportSelector {
/**
* ص
*/
@Nullable
default Class<? extends Group> getImportGroup() {
return null;
}
/**
* Ķ
*/
interface Group {
/**
* ÷Ĵ
*/
void process(AnnotationMetadata metadata, DeferredImportSelector selector);
/**
* ظ÷
*/
Iterable<Entry> selectImports();
/**
* Ԫض
*/
class Entry {
/**
* ע
*/
private final AnnotationMetadata metadata;
/**
*
*/
private final String importClassName;
public Entry(AnnotationMetadata metadata, String importClassName) {
this.metadata = metadata;
this.importClassName = importClassName;
}
// ʡ get/set ʡ equals/toString/hashCode
...
}
}
}
ĴԿ
DeferredImportSelector ImportSelector ӽӿڣ߱ ImportSelector ĹDeferredImportSelector ṩһClass<? extends Group> getImportGroup()÷صǵǰ DeferredImportSelector ʵڵķ顣עд룺
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
дʲô ConfigurationClassParser#handler
class ConfigurationClassParser {
...
private class DeferredImportSelectorHandler {
@Nullable
private List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
// configClass importSelector װ DeferredImportSelectorHolder
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler
= new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
// ӵ deferredImportSelectors
this.deferredImportSelectors.add(holder);
}
}
...
}
...
}
ԿϴȽ configClass importSelector װ DeferredImportSelectorȻӵ deferredImportSelectors
ĿǰΪֹDeferredImportSelector ಢûндô DeferredImportSelector ﴦأǻص ConfigurationClassParser#parse(Set<BeanDefinitionHolder>)
public class ConfigurationClassParser {
public void parse(Set<BeanDefinitionHolder> configCandidates) {
// ѭ
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
// BeanDefinitionAnnotatedBeanDefinitionʵ
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
...
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
...
}
}
// ﴦ DeferredImportSelector
this.deferredImportSelectorHandler.process();
}
...
}
ԿڴĽ DeferredImportSelectorҲӵ deferredImportSelectors ݣõ deferredImportSelectorHandler.process().
Ǽ
class ConfigurationClassParser {
private class DeferredImportSelectorHandler {
...
public void process() {
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler
= new DeferredImportSelectorGroupingHandler();
// DeferredImportSelector ָ˳@Order/Orderd
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// DeferredImportSelectorGroupingHandler#register
deferredImports.forEach(handler::register);
//
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
...
}
...
}
process() £
@Order ע⣬ʵ Orderd ӿDeferredImportSelectorGroupingHandler#register ʵǽ deferredImports еԪעᵽ handler Уhandler.processGroupImports() 롣handler.processGroupImports()
class ConfigurationClassParser {
...
private class DeferredImportSelectorGroupingHandler {
...
/**
* յĴ
* ﴦ鵼
*/
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
// 飬 grouping.getImports()ǹؼ
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// ͬImportSelectorʵһҲǵ processImports(...)
// ע entry.getImportClassName()һε processImports(...) IJ
// ImportSelector
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (...) {
...
}
});
}
}
...
}
...
}
processGroupImports(...) Ҫ£
grouping.getImports() ȡprocessImports ĵ룬ͬ ImportSelector ӿһgrouping.getImports() ÷Ϊ ConfigurationClassParser.DeferredImportSelectorGrouping#getImports£
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
// ִ Group#process
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
// ִ Group#selectImports
return this.group.selectImports();
}
DeferredImportSelector.Group
DeferredImportSelector.Group#processDeferredImportSelector.Group#selectImportsСڵһʼ뼰עͣ DeferredImportSelector.Group#selectImports ص.
ϰ죬ܽ DeferredImportSelector ImportSelector ߵ
DeferredImportSelector ָ飺ڴʱԸݷͳһDeferredImportSelector ʱ֮ٴDeferredImportSelector ķأͬ ImportSelector ImportSelector#selectImports أ DeferredImportSelector.Group#selectImports ءҪ ConfigurationClassPostProcessor @Import עĴܽ:
@Import ɵ 4 ֱ֣ͨࡢࡢʵ ImportSelector Լʵ ImportBeanDefinitionRegistrar ࣻ@Import ע⣺spring ڻȡϵ @Import ʱȻȡϵע⣬Ȼһжϣǰע @Importȡ @Import (Ϊ Import#value)ȡǰעϵע⣬ظϴ@Import ࣺͨࡢͳһʵ ImportSelector ࣬ selectImports(...) ص õ classȻҲǰʵ ImportBeanDefinitionRegistrar ࣬ѶӦʵ浽ǰУעᵽ beanDefinitionMap ʱǴȡģbeanDefinitionMapʵ ImportSelector յõһͨ࣬ͬͨһֱעʵ ImportBeanDefinitionRegistrar ࣬ ImportBeanDefinitionRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) עᣬעʵж塣ԭӣhttps://my.oschina.net/funcy/blog/4678152 ߸ˮƽд֮ӭָԭףҵתϵȨҵתע