[Spring Topic] Source Code Analysis of Bean Life Cycle in Spring——Phase Two (3) (Analysis of the Underlying Principle of Circular Dependency for Attribute Filling)

foreword

reading preparation

Past review:

reading advice

  1. Look at the source code, remember to get entangled in the details, otherwise it is easy to get stuck. Normally, just look at the main process
  2. If you don't understand, look at class annotations or method annotations. Spring is such an excellent source code, the annotations are really in place
  3. If you are an idea user, use the bookmark function of F11 more.
    • Ctrl + F11 Select files/folders, use mnemonics to set/cancel bookmarks (required)
    • Shift + F11 Pop-up bookmark display layer (required)
    • Ctrl +1,2,3...9 Locate to the bookmark position of the corresponding value (required)

Pre-knowledge

Bean life cycle

The life cycle of a bean refers to: how is a bean generated in Spring? Bean generation steps are as follows: (PS: here will not describe the life cycle of Bean in detail, only describe the general process)

  1. Instantiate beans according to beanDefinition
  2. Populate the properties in the original object (Dependency Injection)
  3. before initialization
  4. initialization
  5. after initialization
  6. Put the generated bean into the singleton pool
    Put the final generated proxy object into the singleton pool (called singletonObjects in the source code), and take it directly from the singleton pool next time you getBean

Generation of circular dependencies

When it comes to circular dependencies, everyone is familiar with it. The code for circular dependencies is as follows:

@Component
public class CircularA {
    
    

    @Autowired
    CircularB b;
}

@Component
public class CircularB {
    
    

    @Autowired
    CircularA a;
}

But have you ever thought about how circular dependencies are generated and how to solve them? Here, I want to give you a deduction, just like we are Spring authors, thinking about how to solve circular dependencies.

Three Maps in Spring

Here, I still want to give you a general introduction in advance, when obtaining a singleton bean, what are the three Maps that appear in the Spring source code, and what is used to store them. They are as follows:

  • Map<String, Object> singletonObjects: Level 1 cache. This is what we often call the singleton pool, the beans stored here,It has gone through the complete Spring life cycle, [completed the life cycle designed by Spring](Experiencing the complete life cycle here does not mean that you have to go through before and after instantiation, before and after initialization. Simply put, it is: Spring-approved, mature Bean)
  • Map<String, Object> earlySingletonObjects: Second level cache. Directly translated, what is stored here is [Early Singleton Bean]. What is early? It is relative to the previous [Mature Bean],[Bean that has not yet completed the life cycle]
  • Map<String, ObjectFactory<?>> singletonFactories: L3 cache. The literal translation is [singleton bean factory]. In fact, I still like to use a proper noun mentioned before to explain:Hook method caching of production beans

Course content

Note: The bean life cycle flowchart in the figure below does not represent the real cycle. For convenience, I just took a few processing.

1. [Level 3 cache] evolutionary reasoning

1. Only the evolutionary reasoning of the first-level cache

Let's look at a picture first. Before there is no third-level cache, when there is only one first-level cache, if A depends on B, and B depends on A, then the following phenomenon will occur:
insert image description here

Obviously, in the process of our initial creation, there will be neither object B nor object A in the singleton pool. After all, they have only reached the second step [injecting attributes], and it is in the last step that they put the generated objects into the singleton pool. Therefore, in the situation in the above picture, if there is no external intervention, a closed loop is formed between the two beans, which cannot be untied. This is obviously not the result we want, right? So how to solve this problem?

1.1 Directly put the object generated after instantiation into the singleton pool

At this time, a very normal thought is, wouldn’t it be fine if I put it in the singleton pool in advance, as shown below:
insert image description here
Isn’t it broken? Hehehe,
I can only say that it makes sense, but not much. Because Spring actually takes objects from the singleton pool, so doing so is equivalent to exposing [ semi -finished objects that have not completed their life cycle ] in advance . In this way, in a multi-threaded environment, if someone comes to access the singleton pool, directly gets this BeanA, and then calls the method in it, if there is no [property injection], wouldn't it be G? Yes, that's a concurrency safety issue! Here we can only pass this scheme directly.
PS: Of course, I know that some people will say that locking the first-level cache can solve it, ah, yes, it can. But have you ever wondered what about the performance...

1.2 Summary

  1. After instantiation, [ semi-finished objects that have not completed the life cycle ] are put into the singleton pool, which will cause thread safety problems
    (PS: Pay attention to the conclusion here, it will be tested later!!! /狗头/狗头)

2. Introducing the evolution reasoning of the second-level cache

2.1 Introduce an early object after instantiation of an intermediate Map storage (suspected second-level cache)

A very normal thinking, I add a new Map, and save it immediately after instantiation. Anyway, it has already been instantiated, and the address has been fixed. No matter how you operate later, you will operate on the object at this address. Exposing this object in advance will not affect the result at all.
insert image description here
As shown in the figure above, then I add an intermediate cache Map to store the previously instantiated objects, is it possible? Well, judging from the flowchart, this really seems to be the final answer.
However, if I ask you [what to do with AOP] or to be precise, what you need is [what to do with the proxy object], how will you respond?? Obviously, this intermediate table stores the original object, but sometimes what I need is a proxy object. See, after a little scrutiny, there is another problem. Well, let's continue to improve this plan.
(PS: This question means that we have to consider AOP proxy in advance at this step. Everyone should remember this conclusion)
(Note: I am just giving an example of the need for AOP. In fact, it refers to any process that requires a proxy. Do you think there is a multi-level proxy situation)
(Note: I am just giving an example of the need for AOP. In fact, it refers to any process that requires a proxy. Do you think there is a multi-level proxy situation)
(Note: I am just giving an example of the need for AOP. In fact, it refers to any process that requires a proxy. Do you think there is a multi-level proxy situation)

2.2 Solve the problem of needing to be proxied in 2.1 (suspected second-level cache)

insert image description here

That's it, just add one more step to the AOP process, hehehe. But according to the usual practice, I have already [hehehe], so I must ask: Is it really okay? Ha, really do! It's really all right. Then why, do we need a third-level cache?

3. Introducing the evolutionary reasoning of the third-level cache

3.1 Why L3 cache

At this point, I'm about to start pretending. (I even suspect that Spring is pretending to write like this, haha, just kidding) In fact, there are
insert image description here
a lot of arguments on the Internet. I also summed up the strengths of hundreds of schools, combined with what my teacher said in class, and concluded the following conclusions :

  1. life cycle is brokenI think this is the most important reason, but it is also difficult to understand. How do you understand it? Do you remember how I first described Spring? What is the core of Spring? Do you know which part of the bean's life cycle the AOP implementation is in?
    • The first question: Spring is an IOC container that implements AOP technology
    • The second question: The core of Spring is IOC and AOP, but all the foundations come from IOC
    • The third question: AOP is implemented in the [post-initialization] stage of the bean life cycle. because,The current implementation of AOP technology is also based on some of the many extension points provided by Spring.. For example, the implementation of AOP uses: BeanPostProcessor. What is the meaning revealed here? I think it means:Inside Spring, AOP is just used as an additional extension.. It's as if we implemented Mybatis based on Spring's extension point and SpringMVC.

PS: So when you get here, do you guys know how to understand this [ life cycle is broken ] ? If we judge whether we need to do AOP after instantiation, it means that we have not done [property injection], [before initialization], [initialization], [after initialization] and other life cycles, we must start to do it AOP, directly move the AOP process from [after initialization] to [before attribute injection]. And, in the process of implementing this AOP, you have to call a method similar to the following:

for(BeanPostProcessor bp : this.beanPostProcessorsCache) {
     
     
		bp.postProcessAfterInitialization(bean);
}

But this code will actually be called later [after initialization]. I guess some friends will say this: Then I can loop through the specified BeanPostProcessors that implement AOP? Well, it really does work. However, as mentioned above, if we look at it from the perspective of Spring: AOP is just an extension of my IOC. From this point of view, this implementation is a bit intrusive, and the semantics have also changed slightly.

  1. Cyclic dependencies occur frequently. I think I would like to ask everyone, do you have a lot of circular dependencies in your actual usage scenarios? Brother, I have been writing Java code for more than 4 years, and I only remember a few times. so, how about you look back at the above solution?It makes a judgment every time it instantiates and generates a bean! Is it a bit redundant?
  2. code style. Saying this is very abstract. This is not a very mainstream statement, but it makes sense. How to understand this sentence?

In fact, 2 and 3 should be combined and built on the last struggle of [1], that is, I still have to start judging [whether AOP is needed] after [instantiation] is completed. First of all, do you still remember, why should AOP be judged in advance? Because of the need for circular dependencies. in other words,We actually need to [judge whether it is AOP] when [there is circular dependency], right? That is, when [there is no circular dependency], there is no need at all. So if you directly judge AOP here, is the granularity too large?

3.2 Solve the problem of 3.1 [large granularity]

I think, in response to the problem of "large granularity", those very smart students have already thought of the design of the "third-level cache" I told you earlier. Hey, I designed it as a hook function cache. , Even the second-level cache composed of Spring's [Level 1 + Level 3] can also solve the problem of large granularity? (This is a bit difficult to understand, please read the lambda expression carefully), as follows:
(PS: The map corresponds to Spring's third-level cache)

Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16)

singletonFactories.put(beanName, ()->{
    
    determinedAop(bean);});

void determinedAop(Object bean) {
    
    
	 Object exposedObject = object;
	 if (object 是否需要被代理) {
    
    
		 exposedObject = 代理对象obejct;
	 }
	 return exposedObject;
}

Yes, it looks a bit like that. However, there is still one judgment missing, that is to judge [whether there is a circular dependency]. This is also easy to handle, just judge whether the map mentioned above has a value or not. (PS: I think so. But a new set has been added in Spring to store the name of the bean being created. For interested friends, please read my last content.) But is this enough
? not enough. Why, what if I have a third class that requires a circular dependency? As follows:

@Component
public class CircularA {
    
    

    @Autowired
    CircularB b;
    
    @Autowired
    CircularC c;
}

@Component
public class CircularB {
    
    

    @Autowired
    CircularA a;
}

@Component
public class CircularC {
    
    

    @Autowired
    CircularA a;
}

The effect is as follows:
insert image description here
You may not be able to see it through the picture, so I will remind you directly. The following:
insert image description here
insert image description here
Because the intermediate map above you stores a callback function, if there is a proxy for this function, do you return a new object every time? ? ? This is obviously not in line with our expectations, single case, the A injected into B and C should be the same! what to do? Just save it. But can it be put directly into the singleton pool? No, do you know why? Ha, the reason is the same as 1.1.

3.3 Solve the problem generated by calling the hook function multiple times in 3.2

Therefore, another cache map is introduced here to cache the beans mentioned above. Call it here: early beans.(PS: The map corresponds to Spring's second-level cache)
So far, the third-level cache has been introduced. Are the students useless?
insert image description here

3.4 How to judge whether there is a circular dependency (not so important)

As mentioned above, when judging whether there is a circular dependency, although I think the third-level map can be used to judge, after all, this existence also represents [creating]. But Spring has added a new set to store the name of the bean being created. Why this is so, I think there are several reasons:

  1. This third-level cache map will theoretically be deleted immediately after use. What are the benefits of doing this? To a certain extent, the risk of multiple calls is reduced;
  2. Now it is singletonsCurrentlyInCreationjudged by a set called Set. Then the big guys can click to see where this judgment is referenced, and you will find that there are many references, and the life cycle spanned is wider. Or let's put it another way, many places need to judge whether the bean is [creating], so a new set is added to save it. The use of the third-level map to judge is only applicable to the scene of [circular dependency]. So, since there is already a set, just use it directly, and the semantics are clearer.

3.5 Special statement

Pay attention, what I did in 3.1, 3.2, and 3.3 is just to improve the 2.2 plan. Do you still remember the conclusion of 2.2? [We have to consider AOP in advance at this step], that is, it is inevitable, and the subsequent improvement plan is just to reduce the granularity as much as possible.

4. Summary

Ah, I'm afraid I didn't make it clear. I'm also afraid that everyone didn't understand. Give another reasoning evolution diagram.
insert image description here

Second, the underlying source code analysis (expansion)

The source code entry of the circular dependency is right here. AbstractAutowireCapableBeanFactory#doCreateBean()Looking at the name, you should be somewhat impressed, right? Don't you also pass this method when instantiating. In fact, strictly speaking, the source code here is nothing to talk about, the main thing is to understand its underlying principles. Let me post it for you casually, please pay attention to the comments inside, I will mark it for you in the code related to [circular dependency]:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {
    
    

		// 实例化bean
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
    
    
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
    
    
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		Object bean = instanceWrapper.getWrappedInstance();
		Class<?> beanType = instanceWrapper.getWrappedClass();
		if (beanType != NullBean.class) {
    
    
			mbd.resolvedTargetType = beanType;
		}
		
		// 合并beanDefinition Bean后置处理器
		synchronized (mbd.postProcessingLock) {
    
    
			if (!mbd.postProcessed) {
    
    
				try {
    
    
					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
				}
				catch (Throwable ex) {
    
    
					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
							"Post-processing of merged bean definition failed", ex);
				}
				mbd.postProcessed = true;
			}
		}

		// 【循环依赖】关键源码一
		// 这里就是我们在分析中说的注册钩子方法,判断是否需要【循环依赖】
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
    
    
			if (logger.isTraceEnabled()) {
    
    
				logger.trace("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// 【循环依赖】关键源码二
		// 但是这里看不出来,因为AOP就是在下面的initializeBean里面的,需要挖掘一下才能找到
		Object exposedObject = bean;
		try {
    
    
			populateBean(beanName, mbd, instanceWrapper);
			exposedObject = initializeBean(beanName, exposedObject, mbd);
		}
		catch (Throwable ex) {
    
    
			if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
    
    
				throw (BeanCreationException) ex;
			}
			else {
    
    
				throw new BeanCreationException(
						mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
			}
		}

		// 【循环依赖】关键源码三
		if (earlySingletonExposure) {
    
    
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
    
    
				if (exposedObject == bean) {
    
    
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
    
    
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
    
    
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
    
    
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
    
    
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] in its raw version as part of a circular reference, but has eventually been " +
								"wrapped. This means that said other beans do not use the final version of the " +
								"bean. This is often the result of over-eager type matching - consider using " +
								"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

		// Register bean as disposable.
		try {
    
    
			registerDisposableBeanIfNecessary(beanName, bean, mbd);
		}
		catch (BeanDefinitionValidationException ex) {
    
    
			throw new BeanCreationException(
					mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
		}

		return exposedObject;
	}

As shown above, there are 3 critical source codes above.

2.1 The first key source code

The first key source code here is as follows:

// 【循环依赖】关键源码一
// 这里就是我们在分析中说的注册钩子方法,判断是否需要【循环依赖】
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
		isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    
    
	if (logger.isTraceEnabled()) {
    
    
		logger.trace("Eagerly caching bean '" + beanName +
				"' to allow for resolving potential circular references");
	}
	addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

allowCircularReferences Determine whether [permit circular dependency], and isSingletonCurrentlyInCreation(beanName)determine whether [circular dependency exists]. Then call addSingletonFactoryregistered a hook method getEarlyBeanReference(beanName, mbd, bean). The hook method is implemented as follows:

	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    
    
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    
    
			for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
    
    
				exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
			}
		}
		return exposedObject;
	}

Look, there is an [intelligent instantiation-aware Bean post-processor SmartInstantiationAwareBeanPostProcessor ]. And we call its getEarlyBeanReferencemethod to obtain a reference to an [early bean] object. Hey, this [early bean] is very familiar as soon as it comes out, the second-level cache, is there nothing wrong with it?
It is not difficult to see from this point that this Bean post-processor has a great relationship with Spring's AOP implementation. But I want to tell you in advance that the core class of AOP is actually an implementation class of this interface. AnnotationAwareAspectJAutoProxyCreatorAs for how to dig it, I will introduce the AOP course later. But in the end, we delved into getEarlyBeanReferencethis method, and finally its source code is as follows: (PS: When we call the following method, it means that the AOP judgment is made in advance

public Object getEarlyBeanReference(Object bean, String beanName) {
    
    
	Object cacheKey = getCacheKey(bean.getClass(), beanName);
	this.earlyProxyReferences.put(cacheKey, bean);
	return wrapIfNecessary(bean, beanName, cacheKey);
}

This source code is very simple, you can basically know what it means by looking at the name. In the end, return wrapIfNecessaryit is nothing more than the original object or the proxy object, depending on [whether AOP is required].
The big guy may wonder, this.earlyProxyReferences.put(cacheKey, bean);what is the second line of cache for? Hey, the normal AOP is [after initialization], you are ahead of time now, why don’t you record it? If you don't record it, do you want to perform AOP again when you go to [after initialization]? Hey, that's it.

2.2 The second key source code

The second key source code says this:

// 【循环依赖】关键源码二
// 但是这里看不出来,因为AOP就是在下面的initializeBean里面的,需要挖掘一下才能找到
Object exposedObject = bean;
try {
    
    
	populateBean(beanName, mbd, instanceWrapper);
	exposedObject = initializeBean(beanName, exposedObject, mbd);
}

But in fact, I want to talk about initializeBeanthe [post-initialization] processing inside. I guess experienced students, or students who have seen my introduction to Spring before, should know which method [after initialization] is. I will not index it, and just show the calling source code:

	@Override
	public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
			throws BeansException {
    
    

		Object result = existingBean;
		for (BeanPostProcessor processor : getBeanPostProcessors()) {
    
    
			Object current = processor.postProcessAfterInitialization(result, beanName);
			if (current == null) {
    
    
				return result;
			}
			result = current;
		}
		return result;
	}

The calling place is like this, but the source code of the Bean post-processor that handles AOP is as follows, I won’t index in the middle, here is just an impression for everyone to see:

	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    
    
		if (bean != null) {
    
    
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    
    
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

Look, here is the use of earlyProxyReferencesjudging whether AOP has been performed in advance. But here is a very important detail, which will be used when talking about the third key source code below!
Right now:When this.earlyProxyReferences.remove(cacheKey) == beanit is, it means that I have performed AOP in advance, and at this time the original object is returned directly, not the proxy object! (Normal logic, when there is no AOP in advance, the proxy object is returned here). Keeping this conclusion in mind, we will examine

2.3 The third key source code

as follows:(Note: There is a key prerequisite for entering this code, namely: earlySingletonExposure==true, which means that there was a circular dependency before!

// 【循环依赖】关键源码三
if (earlySingletonExposure) {
    
    
	Object earlySingletonReference = getSingleton(beanName, false);
	if (earlySingletonReference != null) {
    
    
		if (exposedObject == bean) {
    
    
			exposedObject = earlySingletonReference;
		}
		else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
    
    
			String[] dependentBeans = getDependentBeans(beanName);
			Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
			for (String dependentBean : dependentBeans) {
    
    
				if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
    
    
					actualDependentBeans.add(dependentBean);
				}
			}
			if (!actualDependentBeans.isEmpty()) {
    
    
				throw new BeanCurrentlyInCreationException(beanName,
						"Bean with name '" + beanName + "' has been injected into other beans [" +
						StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
						"] in its raw version as part of a circular reference, but has eventually been " +
						"wrapped. This means that said other beans do not use the final version of the " +
						"bean. This is often the result of over-eager type matching - consider using " +
						"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
			}
		}
	}
}

To be honest, it is quite troublesome to talk about this, and this knowledge is a bit unpopular, and it is difficult to describe. It is estimated that you do not often encounter it. However, we can roughly know what the logic to be processed here is through the exception information thrown at the end. The translation is as follows:

Exception Literal Translation: The Bean with the name "beanName" has been injected into other Beans [xxxx] in its earlier version as part of a circular reference, but was eventually wrapped (wrapped means: proxy). This means that other beans mentioned above are using a non-final version of the bean.
. . . . . . . . . . . . . . . . . . . .
Speaking human words: The Bean named beanName has already been packaged by [AOP proxy] once in the circular dependency, but it was packaged by [proxy package] again later in other processing. As a result, the proxy bean that was previously injected in [Circular Dependency] is not the latest proxy bean. If you encounter this situation, you can only report an error

Look, through the translation above, it is very clear what is going on. That's the thing, that's the problem.

But in fact, there is still a problem with the Spring source code. Due to the existence of the 2.2 conclusion, it may lead to the situation mentioned in the black judgment box in the following figure, and then an error is reported: insert image description here
the process is because, in the process of instantiating A, there is a circular dependency between A and B, and the AOP agent of A The object is generated by using the hook method object in the process of B, so, in the process of creating A, A cannot perceive that it has been generated as an AOP proxy! So in the later stage, when we reached the process of [after initialization], because of the conclusion of 2.2, the original object was used and was proxied by Async! horrible! This is obviously not what we want!

2.3.1 How to solve the above problems

In fact, the idea is very straightforward. Since this problem is caused by circular dependence, let's break the circular dependence! hold on hold on, don't you think I told you to change the class structure? no. Then think about it, is there any way to break the circular dependency without changing the class structure?
Speaking of which, let's recall, do you still remember what caused the circular dependency? Isn't it because of attributes that [serial injection] is needed? The popular explanation is, because I need to inject properties in the process of creating beans. Is there a way to not inject properties when creating beans? There is, @Lazycomment.

summarize

  1. Learn the causes of circular dependencies and the design principles of the three-level cache structure

Guess you like

Origin blog.csdn.net/qq_32681589/article/details/132416307