什么是循环依赖
循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。或者是 A 依赖 B,B 依赖 C,C 又依赖 A。它们之间的依赖关系如下:
注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
Spring中循环依赖场景有:
- 1、构造器的循环依赖
- 2、field属性的循环依赖
其中,构造器的循环依赖问题是无法解决,只能拋出BeanCurrentlyInCreationException异常,在解决属性循环依赖时,spring采用的是提前暴露对象的方法。
怎么检测是否存在循环依赖
检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。
三级缓存的介绍,用于解决单例Bean的循环依赖
/** Cache of singleton objects: bean name to bean instance. */ //一级缓存:单例对象缓存池,beanName->Bean,其中存储的就是实例化,属性赋值成功之后的单例对象 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name to ObjectFactory. */ //三级缓存:单例工厂的缓存,beanName->ObjectFactory,添加进去的时候实例还未具备属性 // 用于保存beanName和创建bean的工厂之间的关系map,单例Bean在创建之初过早的暴露出去的Factory, // 为什么采用工厂方式,是因为有些Bean是需要被代理的,总不能把代理前的暴露出去那就毫无意义了 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. */ //二级缓存:早期的单例对象,beanName->Bean,其中存储的是实例化之后,属性未赋值的单例对象 // 执行了工厂方法生产出来的Bean,bean被放进去之后, // 那么当bean在创建过程中,就可以通过getBean方法获取到 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);根据缓存变量上面的注释,大家应该能大致了解他们的用途。我这里简单说明一下吧:
缓存 用途 singletonObjects 用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用。 earlySingletonObjects 提前曝光的单例cache,存放原始的 bean 对象(尚未填<br/>充属性),用于解决循环依赖。 singletonFactories 存放 bean 工厂对象,用于解决循环依赖。Spring创建Bean的流程
对Bean的创建最为核心三个方法解释如下:
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
- populateBean:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired)
- initializeBean:回到一些形如initMethod、InitializingBean等方法
从对单例Bean的初始化可以看出,循环依赖主要发生在第二步(populateBean),也就是field属性注入的处理。
Spring怎么解决循环依赖,setter方式单例
Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前)。
循环依赖解决方式: 三级缓存
由refresh()为入口切入, 这里只分析单例bean创建流程:
-
1、AbstractBeanFactory.getBean为入口 并委托 AbstractBeanFactory.doGetBean创建。
-
2、AbstractBeanFactory.doGetBean 会首先从AbstractBeanFactory.getSingleton中获取缓存的bean对象, 如果不存在则调用抽象方法createBean, 即子类实现的AbstractAutowireCapableBeanFactory.createBean方法。
-
3、AbstractAutowireCapableBeanFactory.createBean方法触发doCreateBean依次调用以下方法实现bean创建过程:
- A、createBeanInstance: 实例化bean, 如果需要依赖其他对象则首先创建其他对象(发生循环依赖的地方)
- B、addSingletonFactory: 将实例化bean加入三级缓存
- C、populateBean: 初始化bean, 如果需要依赖其他对象则首先创建其他对象(发生循环依赖的地方)
- D、initializeBean
- E、registerDisposableBeanIfNecessary
如果是构造函数注入的话在createBeanInstance方法中会调用autowireConstructor
-
1、AbstractAutowireCapableBeanFactory.autowireConstructor使用构造函数进行实例化。
-
2、最终调用 ConstructorResolver.autowireConstructor 和 ConstructorResolver.resolveConstructorArguments 进行实例化已经解析构造参数。
-
3、调用BeanDefinitionValueResolver.resolveValueIfNecessary 和 BeanDefinitionValueResolver.resolveReference 模版类解析构造参数。
步骤配上代码:
1、AbstractBeanFactory.getBean为入口 并委托 AbstractBeanFactory.doGetBean创建。
@Override public Object getBean(String name) throws BeansException { // 获取name对应的bean实例,如果不存在,则创建一个 return doGetBean(name, null, null, false); } protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { // 1.解析beanName,主要是解析别名、去掉FactoryBean的前缀“&” final String beanName = transformedBeanName(name); Object bean; // Eagerly check singleton cache for manually registered singletons. // 2.尝试从缓存中获取beanName对应的实例 Object sharedInstance = getSingleton(beanName); if (sharedInstance != null && args == null) { // 3.如果beanName的实例存在于缓存中 if (logger.isTraceEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } // 3.1 返回beanName对应的实例对象(主要用于FactoryBean的特殊处理,普通Bean会直接返回sharedInstance本身) bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // Fail if we're already creating this bean instance: // We're assumably within a circular reference. // 4.scope为prototype的循环依赖校验:如果beanName已经正在创建Bean实例中,而此时我们又要再一次创建beanName的实例,则代表出现了循环依赖,需要抛出异常。 // 例子:如果存在A中有B的属性,B中有A的属性,那么当依赖注入的时候,就会产生当A还未创建完的时候因为对于B的创建再次返回创建A,造成循环依赖 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // Check if bean definition exists in this factory. // 5.获取parentBeanFactory BeanFactory parentBeanFactory = getParentBeanFactory(); // 5.1 如果parentBeanFactory存在,并且beanName在当前BeanFactory不存在Bean定义,则尝试从parentBeanFactory中获取bean实例 if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // Not found -> check parent. // 5.2 将别名解析成真正的beanName String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); } // 5.3 尝试在parentBeanFactory中获取bean对象实例 else if (args != null) { // Delegation to parent with explicit args. return (T) parentBeanFactory.getBean(nameToLookup, args); } else if (requiredType != null) { // No args -> delegate to standard getBean method. return parentBeanFactory.getBean(nameToLookup, requiredType); } else { return (T) parentBeanFactory.getBean(nameToLookup); } } if (!typeCheckOnly) { // 6.如果不是仅仅做类型检测,而是创建bean实例,这里要将beanName放到alreadyCreated缓存 markBeanAsCreated(beanName); } try { // 7.根据beanName重新获取MergedBeanDefinition(步骤6将MergedBeanDefinition删除了,这边获取一个新的) final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); // 7.1 检查MergedBeanDefinition checkMergedBeanDefinition(mbd, beanName, args); // Guarantee initialization of beans that the current bean depends on. // 8.拿到当前bean依赖的bean名称集合,在实例化自己之前,需要先实例化自己依赖的bean String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { // 8.1 遍历当前bean依赖的bean名称集合 for (String dep : dependsOn) { // 8.2 检查dep是否依赖于beanName,即检查是否存在循环依赖 if (isDependent(beanName, dep)) { // 8.3 如果是循环依赖则抛异常 throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } // 8.4 将dep和beanName的依赖关系注册到缓存中 registerDependentBean(dep, beanName); try { // 8.5 获取dep对应的bean实例,如果dep还没有创建bean实例,则创建dep的bean实例 getBean(dep); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } } } // Create bean instance. // 9.针对不同的scope进行bean的创建 if (mbd.isSingleton()) { // 9.1 scope为singleton的bean创建(新建了一个ObjectFactory,并且重写了getObject方法) sharedInstance = getSingleton(beanName, () -> { try { // 9.1.1 创建Bean实例 return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); // 9.1.2 返回beanName对应的实例对象 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. // 9.2 scope为prototype的bean创建 Object prototypeInstance = null; try { // 9.2.1 创建实例前的操作(将beanName保存到prototypesCurrentlyInCreation缓存中) beforePrototypeCreation(beanName); // 9.2.2 创建Bean实例 prototypeInstance = createBean(beanName, mbd, args); } finally { // 9.2.3 创建实例后的操作(将创建完的beanName从prototypesCurrentlyInCreation缓存中移除) afterPrototypeCreation(beanName); } // 9.2.4 返回beanName对应的实例对象 bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { // 9.3 其他scope的bean创建,可能是request之类的 // 9.3.1 根据scopeName,从缓存拿到scope实例 String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { // 9.3.2 其他scope的bean创建(新建了一个ObjectFactory,并且重写了getObject方法) Object scopedInstance = scope.get(beanName, () -> { // 9.3.3 创建实例前的操作(将beanName保存到prototypesCurrentlyInCreation缓存中) beforePrototypeCreation(beanName); try { // 9.3.4 创建bean实例 return createBean(beanName, mbd, args); } finally { // 9.3.5 创建实例后的操作(将创建完的beanName从prototypesCurrentlyInCreation缓存中移除) afterPrototypeCreation(beanName); } }); // 9.3.6 返回beanName对应的实例对象 bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex); } } } catch (BeansException ex) { // 如果创建bean实例过程中出现异常,则将beanName从alreadyCreated缓存中移除 cleanupAfterBeanCreationFailure(beanName); throw ex; } } // Check if required type matches the type of the actual bean instance. // 10.检查所需类型是否与实际的bean对象的类型匹配 if (requiredType != null && !requiredType.isInstance(bean)) { try { // 10.1 类型不对,则尝试转换bean类型 T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); if (convertedBean == null) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } return convertedBean; } catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } } // 11.返回创建出来的bean实例对象 return (T) bean; }2、AbstractBeanFactory.doGetBean 会首先从AbstractBeanFactory.getSingleton中获取缓存的bean对象, 如果不存在则调用抽象方法createBean, 即子类实现的AbstractAutowireCapableBeanFactory.createBean方法。
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { /** Cache of singleton objects: bean name to bean instance. */ //一级缓存:单例对象缓存池,beanName->Bean,其中存储的就是实例化,属性赋值成功之后的单例对象 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name to ObjectFactory. */ //三级缓存:单例工厂的缓存,beanName->ObjectFactory,添加进去的时候实例还未具备属性 // 用于保存beanName和创建bean的工厂之间的关系map,单例Bean在创建之初过早的暴露出去的Factory, // 为什么采用工厂方式,是因为有些Bean是需要被代理的,总不能把代理前的暴露出去那就毫无意义了 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. */ //二级缓存:早期的单例对象,beanName->Bean,其中存储的是实例化之后,属性未赋值的单例对象 // 执行了工厂方法生产出来的Bean,bean被放进去之后, // 那么当bean在创建过程中,就可以通过getBean方法获取到 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); /** Names of beans that are currently in creation. */ //三级缓存是用来解决循环依赖,而这个缓存就是用来检测是否存在循环依赖的 private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); /** Names of beans currently excluded from in creation checks. */ //直接缓存当前不能加载的bean private final Set<String> inCreationCheckExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1.从单例对象缓存中获取beanName对应的单例对象 Object singletonObject = this.singletonObjects.get(beanName); // 2.如果单例对象缓存中没有,并且该beanName对应的单例bean正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { //尝试给一级缓存对象加锁,因为接下来就要对缓存对象操作了 synchronized (this.singletonObjects) { // 4.从早期单例对象缓存中获取单例对象(之所称成为早期单例对象,是因为earlySingletonObjects里 // 的对象的都是通过提前曝光的ObjectFactory创建出来的,还未进行属性填充等操作) //尝试从二级缓存earlySingletonObjects这个存储还没进行属性添加操作的Bean实例缓存中获取 singletonObject = this.earlySingletonObjects.get(beanName); // 5.如果在早期单例对象缓存中也没有,并且允许创建早期单例对象引用 //如果还没有获取到并且第二个参数为true,为true则表示bean允许被循环引用 if (singletonObject == null && allowEarlyReference) { // 6.从单例工厂缓存中获取beanName的单例工厂 //从三级缓存singletonFactories这个ObjectFactory实例的缓存里尝试获取创建此Bean的单例工厂实例 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); //如果获取到工厂实例 if (singletonFactory != null) { // 7.如果存在单例对象工厂,则通过工厂创建一个单例对象 singletonObject = singletonFactory.getObject(); // 8.将通过单例对象工厂创建的单例对象,放到早期单例对象缓存中 this.earlySingletonObjects.put(beanName, singletonObject); // 9.移除该beanName对应的单例对象工厂,因为该单例工厂已经创建了一个实例对象,并且放到earlySingletonObjects缓存了, // 因此,后续获取beanName的单例对象,可以通过earlySingletonObjects缓存拿到,不需要在用到该单例工厂 this.singletonFactories.remove(beanName); } } } } // 10.返回单例对象 return singletonObject; } } public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { if (logger.isTraceEnabled()) { logger.trace("Creating instance of bean '" + beanName + "'"); } RootBeanDefinition mbdToUse = mbd; // Make sure bean class is actually resolved at this point, and // clone the bean definition in case of a dynamically resolved Class // which cannot be stored in the shared merged bean definition. //判断需要创建的Bean是否可以实例化,即是否可以通过当前的类加载器加载。 Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { // 如果resolvedClass存在,并且mdb的beanClass类型不是Class,并且mdb的beanClass不为空(则代表beanClass存的是Class的name), // 则使用mdb深拷贝一个新的RootBeanDefinition副本,并且将解析的Class赋值给拷贝的RootBeanDefinition副本的beanClass属性, // 该拷贝副本取代mdb用于后续的操作 mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } // Prepare method overrides. try { // 2.验证及准备覆盖的方法(对override属性进行标记及验证) mbdToUse.prepareMethodOverrides(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", ex); } try { // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. // 3.实例化前的处理,给InstantiationAwareBeanPostProcessor一个机会返回代理对象来替代真正的bean实例,达到“短路”效果 Object bean = resolveBeforeInstantiation(beanName, mbdToUse); // 4.如果bean不为空,则会跳过Spring默认的实例化过程,直接使用返回的bean if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex); } try { // 5.创建Bean实例(真正创建Bean的方法) Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isTraceEnabled()) { logger.trace("Finished creating instance of bean '" + beanName + "'"); } // 6.返回创建的Bean实例 return beanInstance; } catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { // A previously detected exception with proper bean creation context already, // or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry. throw ex; } catch (Throwable ex) { throw new BeanCreationException( mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); } } }3、AbstractAutowireCapableBeanFactory.createBean方法触发doCreateBean依次调用以下方法实现bean创建过程:
- A、createBeanInstance: 实例化bean, 如果需要依赖其他对象则首先创建其他对象(发生循环依赖的地方)
- B、addSingletonFactory: 将实例化bean加入三级缓存
- C、populateBean: 初始化bean, 如果需要依赖其他对象则首先创建其他对象(发生循环依赖的地方)
- D、initializeBean
- E、registerDisposableBeanIfNecessary
主要的步骤为:
-
1、Bean A的首次创建,会调用doCreateBean方法,在doCreateBean方法里会经过层层包装,在调用createBeanInstance方法之后,会创建一个没有任何属性的Bean的实例,并返回该实例的包装类BeanWrapper。
-
2、在调用addSingletonFactory方法之前,将Bean A实例放入ObjectFactory里面,然后调用addSingletonFactory方法将Bean A相关的ObjectFactory实例添加到三级缓存singletonFactories中,此时只有三级缓存中保存了该Bean 对应的ObjectFactory实例。
-
3、随后调用populateBean方法给属性赋值,由于Bean A直接依赖于Bean B,所以在populateBean方法中会再次调用getBean方法在容器里去尝试获取Bean B的实例。此时由于Bean B实例还没有创建出来,因此又会递归的调用到doCreateBean方法,在doCreateBean方法中调用createBeanInstance方法创建出Bean B实例,并将其对应的ObjectFactory方法放入到三级缓存中,此时三级缓存中就保存了循环依赖的Bean A和B的各自的ObjectFactory实例。
-
4、随后在Bean B实例又会调用populateBean方法给其属性赋值。此时由于Bean B又依赖于Bean A,所以在populateBean方法中又会调用getBean方法尝试获取Bean A实例。
-
5、此时会调用AbstractBeanFactory中的doGetBean方法,doGetBean方法会尝试调用getSingleton方法从三级缓存中去获取Bean A的实例。而在一级缓存singletonObjects和二级缓存earlySingletonObjects中都获取不到,而在三级缓存singletonFactories中获取到Bean A实例对应的ObjectFactory实例,调用其getObject方法获取Bean A实例。通过getObject方法获取到了Bean A实例之后,将A实例放入到二级缓存中,同时清空三级缓存中的实例,并将Bean A实例返回。
-
6、由于Bean A实例实在Bean B实例执行populateBean方法的时候获取到了,此时已经将Bean A实例注入到了Bean B实例中,此时Bean B会执行完doCreateBean的剩余方法,并返回一个初始化完整的Bean实例。
-
7、由于doCreateBean方法是通过doGetBean方法调用的,所以会将完整的Bean B实例逐层返回到doGetBean方法(AbstractBeanFactory)中。而AbstractBeanFactory的getSingleton方法在执行singletonObject = singletonFactory.getObject()代码时,才正式执行createBean方法。
-
8、在getSingleton方法中会最终执行addSingleton(beanName, singletonObject)方法,在addSingleton方法中会将Bean B实例添加进一级缓存singletonObjects中,并将Bean B实例从二级缓存(earlySingletonObjects)和三级缓存(singletonFactories)中清除,此时表明彻底完成了Bean B实例的创建,随后将完整的Bean B实例返回。
-
9、Bean B实例的创建是由于Bean A实例调用populateBean方法触发的,所以此时,又会回到创建Bean A时的populateBean方法中,此时Bean A就赋值上了完整的Bean B,在Bean A实例完整创建之后又会逐层返回到doGetBean方法中,之后又会调用addSingleton将Bean A实例放入到一级缓存中,同时清除二级和三级缓存中的Bean A实例。
循环依赖的情况
- 构造函数循环依赖(singleton、prototype)
- Setter注入循环依赖(singleton、prototype)
对于prototype的Bean,Spring默认是不支持相关的循环依赖
// 4.scope为prototype的循环依赖校验:如果beanName已经正在创建Bean实例中,而此时我们又要再一次创建beanName的实例,则代表出现了循环依赖,需要抛出异常。 // 例子:如果存在A中有B的属性,B中有A的属性,那么当依赖注入的时候,就会产生当A还未创建完的时候因为对于B的创建再次返回创建A,造成循环依赖 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }单例Setter注入循环依赖(singleton)问题的解决,主要是单例的三级缓存,三级缓存除了解决循环依赖之外,还解决了保持单例唯一性的问题,因为从缓存中取出来的Bean实例是要保证唯一的,所以三级缓存支持不了prototype,因为prototype的Bean实例不唯一。
正式因为没有三级缓存的支持,才导致prototype不支持循环依赖。
构造函数循环依赖(singleton)【这个Spring解决不了】
由于单例的构造函数注入方式,实在doCreateBean方法中的createBeanInstance方法中完成的,此时还没有三级缓存。在createBeanInstance方法中的autowireConstructor(beanName, mbd, ctors, args)方法中进行Bean的实例化和参数注入,而此时构造函数的实例参数并没有构造出来,所以构造函数的参数实例也会调用getBean方法去创建实例参数,而实例参数又需要之前依赖的实例参数,最后又会递归到autowireConstructor方法,所以导致无限循环。
参考: https://blog.nowcoder.net/n/2bb528b258b44c7eab1703a52170ef09
https://blog.csdn.net/weixin_30951389/article/details/97471000
https://www.cnblogs.com/liuqing576598117/p/11227007.html