Today, let's talk about a question that is often asked in an interview. Let's get it across.
Question: Why do we need a three-level cache in spring to solve this problem? Is it OK to use a secondary cache?
Let me start with the answer: Not available.
Here's the first statement:
It is not indicated in this article that by default, all bean s are singletons, that is, all of the following problems are analyzed in the case of singletons.
The comments in the code are very detailed, so be careful to look more at the comments in the code.
1. Circular Dependency Related Issues
1. What is circular dependency?
2. Two ways to inject loosely dependent objects: the way the constructor works, the way the setter works
3. Detailed explanation of the way the constructor works
4. How does spring know it has a circular dependency?
5. Detailed setter approach
6. It should be noted that the circular dependency is injected into semi-finished products
7. Why do I have to use a Level 3 cache?
2. What is circular dependency?
A depends on B, B depends on A, such as the following code
public class A { private B b; } public class B { private A a; }
3. Two ways of injecting objects with circular dependency
3.1. How the constructor works
Inject each other through the constructor, code as follows
public class A { private B b; public A(B b) { this.b = b; } } public class B { private A a; public B(A a) { this.a = a; } }
3.2. Ways of setter
Inject the other party through the setter method with the following code
public class A { private B b; public B getB() { return b; } public void setB(B b) { this.b = b; } } public class B { private A a; public A getA() { return a; } public void setA(A a) { this.a = a; } }
4. How the constructor works
4.1. Way knowledge points of constructors
1. How is the constructor injected?
2. Circular dependency, how constructors work, what is the spring process like?
3. Ways to Cyclic Dependency Constructor Case Code Analysis
4.2. How is the constructor injected?
Let's look at the two classes below, which are interdependent and inject each other through constructors.
public class A { private B b; public A(B b) { this.b = b; } } public class B { private A a; public B(A a) { this.a = a; } }
Let's think about a question: two classes can only create one object. Try hard coding to see if you can create objects of these two classes?
I think you can see at a glance that it can't be created.
A needs to be created with B first, and B needs to be created with A first, which prevents successful creation.
4.3. Circular dependency, how constructors work, what is the spring process like?
spring puts the name of the beans currently being created in a list before it creates the beans. This list is called singletonsCurrentlyInCreation and records the list of beans being created. When it is created, it removes the name from the singletonsCurrentlyInCreation list. And you'll put the beans you've created into another list of singletonObjects, which is called singletonObjects. Here's a look at the code for these two collections, as follows:
Code in org.springframework.beans.factory.support.DefaultSingletonBeanRegistry In class //Used to store items that are being created bean Name List private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); //Used to store created singletons bean,key by bean Name, value by bean Examples private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
Let's look at the creation of the next two bean s
@Compontent public class A { private B b; public A(B b) { this.b = b; } } @Compontent public class B { private A a; public B(A a) { this.a = a; } }
The process is as follows
1,from singletonObjects Check to see if there is a,Not at this time 2,Ready to create a 3,judge a Is it singletonsCurrentlyInCreation List, which is obviously not there at this time, will a join singletonsCurrentlyInCreation list 4,call a Constructor A(B b)Establish A 5,spring find A Constructor needs to use b 6,Then to spring Container Lookup b,from singletonObjects Check to see if there is b,Not at this time 7,spring Ready to create b 8,judge b Is it singletonsCurrentlyInCreation List, which is obviously not there at this time, will b join singletonsCurrentlyInCreation list 9,call b Constructor B(A a)Establish b 10,spring find B Constructor needs to use a,Then to spring Container Lookup a 11,Then to spring Container Lookup a,from singletonObjects Check to see if there is a,Not at this time 12,Ready to create a 13,judge a Is it singletonsCurrentlyInCreation List, step 3 above a Was put on this list, at this time a In this list, go here and explain a The creation list already exists, and the program creates it a,It means that if you keep going like this, you'll end up in a loop, and then spring An exception will pop up and terminate bean Creation operation.
4.4. Through this process, we reach two conclusions
1. If circular dependency is the way of a constructor, beans cannot be created successfully, on the premise that beans are all singletons, and beans are multiple cases, you can analyze and analyze them yourself.
2. spring discovers circular dependencies through the singletonsCurrentlyInCreation list, which records the beans being created. When beans are found in this list, it indicates that there is a circular dependency and that the circular dependency cannot continue. If you continue, it will enter a dead loop, and spring throws an exception to terminate the system.
The source code to determine the circular dependency is at this location below. SiletonsCurrentlyInCreation is of type Set. Set's add method returns false, indicating that the added element already exists in Set, and then throws the circular dependency exception.
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#beforeSingletonCreation private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); protected void beforeSingletonCreation(String beanName) { //bean If the name already exists in the create list, a circular dependency exception is thrown if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { //Throw a cyclic dependency exception throw new BeanCurrentlyInCreationException(beanName); } } //Circular Dependency Exception public BeanCurrentlyInCreationException(String beanName) { super(beanName, "Requested bean is currently in creation: Is there an unresolvable circular reference?"); }
4.5. spring Constructor Cyclic Dependency Case
Create Class A
package com.javacode2018.cycledependency.demo1; import org.springframework.stereotype.Component; @Component public class A { private B b; public A(B b) { this.b = b; } }
Create Class B
package com.javacode2018.cycledependency.demo1; import org.springframework.stereotype.Component; @Component public class B { private A a; public B(A a) { this.a = a; } }
Startup Class
package com.javacode2018.cycledependency.demo1; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class MainConfig { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig.class); //Refresh container context, trigger singleton bean Establish context.refresh(); //Close Context context.close(); } }
Running the main method above caused an exception. Some of the exception information is as follows, indicating that there was a circular dependency when creating beans, which prevented the creation of beans from proceeding. You will encounter this error in the future, and you should be able to locate the problem quickly.
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347)
5. Detailed setter approach
Let's look at the source code for the two classes of setter
public class A { private B b; public B getB() { return b; } public void setB(B b) { this.b = b; } } public class B { private A a; public A getA() { return a; } public void setA(A a) { this.a = a; } }
Let's try hard coding to inject each other. It's easy, like this
A a = new A(); B b = new B(); a.setB(b); b.setA(a);
We can do it hard-coded. Spring will certainly do it. Indeed, setter is circularly dependent and spring can work properly.
Here's a look at the setter cycle dependency injection process in spring.
6. setter Cyclic Dependency Injection Process in spring
spring uses a third-level cache in the process of creating a single bean, so you need to understand the third-level cache first.
6.1, which level is the third level cache?
spring uses three map s as a three-level cache, one for each level
Level 1 Cache | Corresponding map | Explain |
---|---|---|
Level 1 | Map<String, Object> singletonObjects | Used to store a fully created single bean BeanName->bean instance |
Level 2 | Map<String, Object> earlySingletonObjects | Used to store early bean s BeanName->bean instance |
Level 3 | Map<String, ObjectFactory<?>> singletonFactories | ObjectFactory used to store a single bean BeanName->ObjectFactory instance |
The source code for these three map s is in the org.springframework.beans.factory.support.DefaultSingletonBeanRegistry class.
6.2, Source Parsing for Single bean Creation Process
Code Entry
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
step1: doGetBean
As follows, this method first calls getSingleton to get the beans, and if it can, it returns directly, otherwise it executes the process of creating beans
step2: getSingleton(beanName, true)
The source code is as follows. This method calls getSingleton(beanName, true) internally to get the beans. Note that the second parameter is true, which indicates whether the earlier beans can be obtained. This parameter is true, it tries to get the beans from the third-level cache singletonFactories, and then drops the beans from the third-level cache into the second-level cache.
public Object getSingleton(String beanName) { return getSingleton(beanName, true); } protected Object getSingleton(String beanName, boolean allowEarlyReference) { //Get from Level 1 Cache bean Object singletonObject = this.singletonObjects.get(beanName); //No in Level 1,And currently beanName In Create List if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { //Get from Level 2 Cache Summary bean singletonObject = this.earlySingletonObjects.get(beanName); //Not in Level 2 Cache && allowEarlyReference by true,That is, Level 2 cache was not found bean And beanName You don't want to go on until you create the list. if (singletonObject == null && allowEarlyReference) { //Get from Level 3 Cache bean ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); //Acquired in Level 3 if (singletonFactory != null) { //3 Level cache summary is ObjectFactory,So it's called getObject Method acquisition bean singletonObject = singletonFactory.getObject(); //Cache Level 3 bean Drop into Level 2 this.earlySingletonObjects.put(beanName, singletonObject); //take bean Eliminate from Level 3 Cache this.singletonFactories.remove(beanName); } } } } return singletonObject; }
step3: getSingleton(String beanName, ObjectFactory<?> singletonFactory)
The call to getSingleton above (beanName, true) did not get the beans, so it will continue with the beans creation logic, going to the following code, as follows
Enter getSingleton (String BeanName, ObjectFactory<?> singletonFactory) with the following source code, leaving only the important parts
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { //Get from Level 1 Cache bean,If available, return by yourself Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { //take beanName Add to Current Creation List beforeSingletonCreation(beanName); //①: Create a singleton bean singletonObject = singletonFactory.getObject(); //take beanName Remove from current creation list afterSingletonCreation(beanName); //The singleton that will be created bean Place in Level 1 Cache,And remove it from level 2 or 3 cache addSingleton(beanName, singletonObject); } return singletonObject; }
Notice that code 1 calls singletonFactory.getObject() to create a single bean. Looking back at the contents of the singletonFactory variable, you can see in the following figure that the main call is to create Bean
Now let's go into the createBean method, which ultimately calls doCreateBean to create a bean, so we'll focus on doCreateBean.
step4: doCreateBean
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { // ①: Establish bean Instances, instantiated by reflection bean,Amount to new X()Establish bean Examples BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args); // bean = Get just new Out bean Object bean = instanceWrapper.getWrappedInstance(); // ②: Whether early bean Exposed, so-called early bean Equivalent to this bean Is through new The method creates this object, but the object is not populated with attributes, so it is a semi-finished product // Whether early bean Expose, rule of thumb( bean Is singleton && Is circular dependency allowed && bean Is it in the list being created) boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { //③: call addSingletonFactory Method, which drops it internally into the level 3 cache, getEarlyBeanReference As you can see, some methods are called internally to get the earlier bean Objects, such as those that can be passed through here aop Generate Proxy Object addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // This variable is used to store the final returned bean Object exposedObject = bean; //Fill in the property, which is called setter Method or by reflection will depend on bean Inject in populateBean(beanName, mbd, instanceWrapper); //④: Initialization bean,Called internally BeanPostProcessor Some methods for bean Processing, where you can bean Wrap, such as build agent exposedObject = initializeBean(beanName, exposedObject, mbd); //Early bean Is it exposed if (earlySingletonExposure) { /** *⑤: getSingleton(beanName, false),Notice that the second parameter is false, when this is false, * Beans are only fetched from Levels 1 and 2, which is definitely not in Level 1 (Level 1 caches are only put after the bean s have been created) */ Object earlySingletonReference = getSingleton(beanName, false); /** * ⑥: If earlySingletonReference is not empty, it means that the second level cache has this bean, and the second level cache has this bean, what does it mean? * Let's go back to the analysis above and see when the bean s will be put into Level 2 cache. * (If the bean exists in a Level 3 cache && beanName calls the getSingleton(beanName, false) method elsewhere when the list is currently created, the bean moves from the Level 3 cache to the Level 2 cache. */ if (earlySingletonReference != null) { //⑥: exposedObject==bean,Explain bean Once created, no later modifications if (exposedObject == bean) { //earlySingletonReference Obtained from a secondary cache, in a secondary cache bean From Level 3 Cache, Level 3 Cache may be paired with bean Wrapped, such as proxy object generation //So this place needs to earlySingletonReference As final bean exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { //Look back at the code above, just started exposedObject=bean, // Now you can come here and explain exposedObject and bean Different. What do they mean differently? // Explain initializeBean Internal pair bean Modified // allowRawInjectionDespiteWrapping(Default is false): Is early exposure allowed bean(earlySingletonReference)And ultimately bean Atypism // hasDependentBean(beanName): Indicates there is another bean In favor of beanName // getDependentBeans(beanName): What to Get bean rely on beanName String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { //judge dependentBean Whether or not it has been marked as created is a judgment dependentBean Has it been created if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } /** * * You can go here to show that the earlier bean s were used by someone else, and the later program modified the exposedObject * That is, the bean s created earlier are A, which has been used somewhere, but A may become B after initializeBean, for example, B is a proxy object for A * This time it will be a pit. A used by others and A created in the final container are not the same A object, so there may be problems in the process of using it. * For example, late A enhancements (Aop s) were made, whereas earlier A uses were not enhanced. */ if (!actualDependentBeans.isEmpty()) { //Pop-up exception (given to others early on) bean And final container created bean Inconsistent, eject exception) throw new BeanCurrentlyInCreationException(beanName,"See the source code for the exception."); } } } } return exposedObject; }
The above step1~step4, you have to read it several times**, the following questions are clear before you can continue to look down, do not understand the combination source continue to look at the above steps**
1. When is the bean put into Level 3 cache?
Earlier bean s were placed in Level 3 cache
2. When will bean s be put into Level 2 cache?
When beanX is still in the process of creation, it is added to the list created by the current beanName, but at this time the beans are not created (beans are dropped into the first level cache before they are created), and the beans are still semi-finished. At this time other beans need to use beanX, which is then retrieved from the third level cache. BeanX is dropped from the Level 3 cache into the Level 2 cache.
3. When will bean s be placed in Level 1 cache?
The bean s are instantiated, initialized, injected, and fully assembled before being dropped into the first level cache.
4. What is the populateBean method?
Filling in attributes, such as injecting dependent objects.
6.3. Let's look at the process of creating a class A and B setter circular dependency
1. getSingleton("a", true) get a: will look for a from three levels of cache in turn, at which point none of the three levels of cache has a
2. Drop a in the list of beanName s being created (Set <String> singletonsCurrentlyInCreation)
3. Instantiate a:A = new A(); At this point the object a is an early a and belongs to a semi-finished product
4. Drop the earlier a into the third level cache (Map<String, ObjectFactory<?> > singletonFactories)
5. Call the populateBean method to inject the dependent object and find that setB needs to inject b
6. Call getSingleton("b", true) to get b: a will be found in three levels of cache in turn, at which point none of the three levels of cache has b
7. Drop b in the list of beanName s being created
8. Instantiate b:B b = new B(); At this point the B object is an early B and belongs to a semi-finished product
9. Drop early b into the third level cache (Map<String, ObjectFactory<?> > singletonFactories)
10. Call the populateBean method to inject the dependent object and find that setA needs to inject a
11. Call getSingleton("a", true) to get a: at this point a will be moved from the Level 3 cache to the Level 2 cache, and then returned to b for use, when a is a semi-finished product (the properties have not been populated yet)
12. b Inject a from 11 into b through setA
13. b is created, at which point b is removed from the Level 3 cache and dropped into the Level 1 cache
14, b returns to a, and b is injected into a through setB in class A
15. A's populateBean is finished, that is, property filling is completed, by which time a has been injected into b
16. Call a= initializeBean("a", a, mbd) to process a. This internal change to a may cause a to be different from the original a.
17. Call getSingleton("a", false) to get a. Note that at this time the second parameter is false. When this parameter is false, only the first two levels of cache will attempt to get a. A has been dropped into the second level cache in step 11, so this can get a. This a has already been injected into b.
18. If at this point it is determined whether a injected into b and a generated by initializeBean method are the same a, not the same, an exception will pop up
From the above process we can draw a very important conclusion
When a bean enters the level 2 cache, it means that the early object of the bean is injected by other beans. That is, the bean is still semi-finished and has been taken away and used by others before it is fully created. Therefore, there must be a level 3 cache. The Level 2 cache stores the early object used by others. If there is no level 2 cache, It is impossible to determine whether this object was taken away and used by others during its creation.
Level 3 caching is designed to solve a very important question: is the bean that was taken away and used early and the bean that was finally formed a bean, if it is not the same, an exception will occur, so later interviews ask why Level 3 caching is needed? You just need to answer this question: Level 3 caches are used to determine if the beans that were exposed earlier and the final beans that were used by others are the same beans, if they are not the same, then an exception pops up. If the early objects are not used by other beans and later modified, no exceptions will be generated. If there is no Level 3 cache, It is impossible to determine if there is a circular dependency, and earlier beans are used by beans in a circular dependency.
spring containers do not allow inconsistencies between beans exposed earlier and final beans by default, but this configuration can be modified and there is a lot of sharing after modification, so do not change, control by this variable below
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#allowRawInjectionDespiteWrapping private boolean allowRawInjectionDespiteWrapping = false;
6.4, Simulate BeanCurrentlyInCreationException exception
Come to a login interface ILogin
package com.javacode2018.cycledependency.demo2; //Login Interface public interface ILogin { }
Come two implementations of class LoginA
This is annotated with @Component, and X needs to be injected internally
package com.javacode2018.cycledependency.demo2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class LoginA implements ILogin { @Autowired private X x; public X getX() { return x; } public void setX(X x) { this.x = x; } }
LoginC, no spring needed to manage
package com.javacode2018.cycledependency.demo2; //agent public class LoginC implements ILogin { private ILogin target; public LoginC(ILogin target) { this.target = target; } }
Class X, has @Component, and needs to be injected into the Ilogin object, where LoginA is injected, where LoginA and X are circularly dependent on parameters
package com.javacode2018.cycledependency.demo2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class X { @Autowired private ILogin login; public ILogin getLogin() { return login; } public void setLogin(ILogin login) { this.login = login; } }
Add a BeanPostProcessor class that implements the postProcessAfterInitialization method, which wraps the beans as LoginC returns when it finds they are loginA, and this method is called when initializeBean is called during bean creation
package com.javacode2018.cycledependency.demo2; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("loginA")) { //loginA Implemented ILogin return new LoginC((ILogin) bean); } else { return bean; } } }
spring Configuration Class
package com.javacode2018.cycledependency.demo2; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class MainConfig { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig.class); context.refresh(); context.close(); } }
Running the output produces a BeanCurrentlyInCreationException exception because the object injected into x is of the class LoginA, and the beanname:loginA in the container corresponds to LoginC, which causes inconsistencies between the object injected into someone else and the final object, resulting in an exception.
Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'loginA': Bean with name 'loginA' has been injected into other beans [x] 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.
7. Case: What happens if only Level 2 cache is used?
The following example simulates the consequences of a secondary cache by setting breakpoints in the source code.
Add Class A
We want loginA to be created before class A, so the @DependsOn annotation is used here.
package com.javacode2018.cycledependency.demo3; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; @Component @DependsOn("loginA") //class A Dependent on loginA,But you don't want to rely heavily on property injection public class A { }
Interface ILogin
package com.javacode2018.cycledependency.demo3; //Login Interface public interface ILogin { }
For the next two implementation classes, LoginA requires spring management and LoginC does not require spring management.
LoginA
package com.javacode2018.cycledependency.demo3; import org.springframework.stereotype.Component; @Component public class LoginA implements ILogin { }
LoginC
package com.javacode2018.cycledependency.demo3; //agent public class LoginC implements ILogin { private ILogin target; public LoginC(ILogin target) { this.target = target; } }
MyBeanPostProcessor
Responsible for packing loginA as LoginC
package com.javacode2018.cycledependency.demo3; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("loginA")) { //loginA Implemented ILogin return new LoginC((ILogin) bean); } else { return bean; } } }
Start class MainConfig
package com.javacode2018.cycledependency.demo3; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class MainConfig { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig.class); context.refresh(); context.close(); } }
The following simulation simulates the use of a secondary cache only
After the bean s have been placed in the Level 3 cache, set the breakpoint at the next line of code as follows
A box pops up and fills in the following configuration, which indicates that the breakpoint will not work until the condition is met
debug run program
When we reach this breakpoint, loginA is already in the Level 3 cache. If we call this.getSingleton(beanName,true),loginA will move from Level 3 cache to Level 3, which is equivalent to only Level 2 cache, as follows
Clicking on the button below will pop up a window where you can execute code, execute this.getSingleton(beanName,true), putting loginA from Level 3 cache to Level 2 cache, which is equivalent to no Level 3 cache.
As a result, the BeanCurrentlyInCreationException exception was also generated. In fact, the program does not have a circular dependency, but if only a secondary cache is used, there are also exceptions to the parameters of the inconsistency between the earlier exposed beans and the final beans.
Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException
This program can run normally without intervention, only in the case of level 3 cache.
8. Summary
Today's content is a little too much, everyone slowly digest, there are questions welcome to leave a message!
9. Case Source
git Address: https://gitee.com/javacode2018/spring-series This case corresponds to the source code: spring-series\lesson-009-cycledependency
Let's star t, all the series of codes will be in this, as well as links to all original articles, so it's easy to read!!!
Source: https://mp.weixin.qq.com/s?__ Biz=MzA5MTkxMDQ4MQ==&mid=26489328&idx=1&sn=4eecdb72f0cb1206ebbcc8af59886980&scene=21#wechat_ Redirect