Spring三级缓存解决循环依赖
我们都知道Spring中的BeanFactory是一个IOC容器,负责创建Bean和缓存一些单例的Bean对象,以供项目运行过程中使用。
创建Bean的大概的过程:
- 实例化Bean对象,为Bean对象在内存中分配空间,各属性赋值为默认值
- 初始化Bean对象,为Bean对象填充属性
- 将Bean放入缓存
首先,容器为了缓存这些单例的Bean需要一个数据结构来存储,比如Map {k:name; v:bean}。
而我们创建一个Bean就可以往Map中存入一个Bean。这时候我们仅需要一个Map就可以满足创建+缓存的需求。
但是创建Bean过程中可能会遇到循环依赖问题,比如A对象依赖了一个B对象,而B对象内部又依赖了一个A,如下:
public class A { B b; } public class B { A a; }
假设A和B我都定义为单例的对象,并且需要在项目启动过程中自动注入,如下:
@Component public class A { @Autowired B b; } @Component public class B { @Autowired A a; }
一级缓存
- 实例化A对象。
- 填充A的属性阶段时需要去填充B对象,而此时B对象还没有创建,所以这里为了完成A的填充就必须要先去创建B对象;
- 实例化B对象。
- 执行到B对象的填充属性阶段,又会需要去获取A对象,而此时Map中没有A,因为A还没有创建完成,导致又需要去创建A对象。
- 这样,就会循环往复,一直创建下去,只到堆栈溢出。
为什么不能在实例化A之后就放入Map?
因为此时A尚未创建完整,所有属性都是默认值,并不是一个完整的对象,在执行业务时可能会抛出未知的异常。所以必须要在A创建完成之后才能放入Map。
二级缓存
此时我们引入二级缓存用另外一个Map2 {k:name; v:earlybean} 来存储尚未已经开始创建但是尚未完整创建的对象。
- 实例化A对象之后,将A对象放入Map2中。
- 在填充A的属性阶段需要去填充B对象,而此时B对象还没有创建,所以这里为了完成A的填充就必须要先去创建B对象。
- 创建B对象的过程中,实例化B对象之后,将B对象放入Map2中。
- 执行到B对象填充属性阶段,又会需要去获取A对象,而此时Map中没有A,因为A还没有创建完成,但是我们继续从Map2中拿到尚未创建完毕的A的引用赋值给a字段。这样B对象其实就已经创建完整了,尽管B.a对象是一个还未创建完成的对象。
- 此时将B放入Map并且从Map2中删除。
- 这时候B创建完成,A继续执行b的属性填充可以拿到B对象,这样A也完成了创建。
- 此时将A对象放入Map并从Map2中删除。
二级缓存已然解决了循环依赖问题,为什么还需要三级缓存?
从上面的流程中我们可以看到使用两级缓存可以完美解决循环依赖的问题,但是Spring中还有另外一个问题需要解决,这就是初始化过程中的AOP实现。
AOP是Spring的重要功能,实现方式就是使用代理模式动态增强类的功能。
动态单例目前有两种技术可以实现,一种是JDK自带的基于接口的动态Proxy技术,一种是CGlib基于字节码动态生成的Proxy技术,这两种技术都是需要原始对象创建完毕,之后基于原始对象生成代理对象的。
那么我们发现,在二级缓存的设计下,我们需要在放入缓存Map之前将代理对象生成好。
将流程改为:
- 实例化Bean对象,为Bean对象在内存中分配空间,各属性赋值为默认值
- 如果有动态单例,生成Bean对象的代理Proxy对象
- 初始化Proxy对象,为Bean对象填充属性
- 将Proxy放入缓存
这样虽然也可以解决,AOP的问题,但是我们知道Spring中AOP的实现是通过后置处理器BeanPostProcessor机制来实现的,而后置处理器是在填充属性结束后才执行的。流程如下:
- 实例化对象
- 对象填充属性
- BeanPostProcessor doBefore
- init-method
- BeanPostProcessor doAfter -- AOP是在这个阶段实现的
所以要实现上面的方案,势必需要将BeanPostProcessor阶段提前或者侵入到填充属性的流程中,那么从程序设计上来说,这样做肯定是不美的。
三级缓存
Spring引入了第三级缓存来解决这个问题, Map3 {k:name v:ObjectFactory} ,这个缓存的value就不是Bean对象了,而是一个接口对象由一段lamda表达式实现。在这段lamda表达式中去完成一些BeanPostProcessor的执行。
- 实例化A对象之后,将A的ObjectFactory对象放入Map3中。
- 在填充A的属性阶段需要去填充B对象,而此时B对象还没有创建,所以这里为了完成A的填充就必须要先去创建B对象。
- 创建B对象的过程中,实例化B的ObjectFactory对象之后,将B对象放入Map2中。
- 执行到B对象填充属性阶段,又会需要去获取A对象,而此时Map1中没有A,因为A还没有创建完成,但是我们继续从Map2中也拿不到,到Map3中获取了A的ObjectFactory对象,通过ObjectFactory对象获取A的早期对象,并将这个早期对象放入Map2中,同时删除Map3中的A,将尚未创建完毕的A的引用赋值给a字段。这样B对象其实就已经创建完整了,尽管B.a对象是一个还未创建完成的对象。
- 此时将B放入Map并且从Map3中删除。
- 这时候B创建完成,A继续执行b的属性填充可以拿到B对象,这样A也完成了创建。
- 此时将A对象放入Map并从Map2中删除。
源码步骤解析
- SpringBoot项目启动执行到SpringApplication#run 中的refreshContext(context);,最终调用Spring容器的AbstractApplicationContext#refresh方法,开始初始化BeanFactory。
- 在AbstractApplicationContext#refresh步骤中,执行到AbstractApplicationContext#finishBeanFactoryInitialization方法,开始完成 Bean 工厂初始化。
- 执行到AbstracBeanFactory.preInstantiateSingletons(),开始根据BeanFactory中的BeanDefinition信息初始化Bean对象。
- 在AbstracBeanFactory.preInstantiateSingletons()方法中,发现A对象的BeanDefinition,执行AbstracBeanFactory.getBean方法,获取A对象。
- 在AbstracBeanFactory.getBean方法中,执行AbstracBeanFactory.doGetBean方法,获取A对象。
- 在AbstracBeanFactory.doGetBean方法中执行DefaultSingletonBeanRegistry#getSingleton方法,尝试从缓存中获取A对象的单例对象缓存。
- 到一级缓存singletonObjects中找,未找到;
- 到二级缓存earlySingletonObjects中找,未找到;
- 到三级缓存singletonFactories中找,未找到;
- 再次执行DefaultSingletonBeanRegistry#getSingleton的重载方法,传入lamda表达式形式的ObjectFactory对象,内部调用AbstractAutowireCapableBeanFactory#createBean方法,尝试创建A对象。
- 在AbstractAutowireCapableBeanFactory#createBean方法中,调用AbstractAutowireCapableBeanFactory#doCreateBean方法,实际执行创建A对象。
- 实例化A对象,给字段赋值默认值后,调用DefaultSingletonBeanRegistry#addSingletonFactory方法,传入A对象的lamda表达式形式的ObjectFactory对象,将ObjectFactory对象放入三级缓存singletonFactories中,并从2级缓存earlySingletonObjects中移除(虽然这里没有),设置A对象已经开始注册。
- 此处传入的lamda表达式,内部调用了AbstractAutowireCapableBeanFactory#getEarlyBeanReference,此方法用来执行实现了SmartInstantiationAwareBeanPostProcessor的后置处理器,比如实现AOP的AbstractAutoProxyCreator
- 然后开始执行A对象的AbstractAutowireCapableBeanFactory#populateBean,进行属性填充。
- 在进行属性填充时,发现依赖了B对象,执行AbstracBeanFactory.getBean方法,尝试获取B对象。参考上面步骤4~9。
- 执行到B对象的属性填充时,发现依赖了A对象,执行AbstracBeanFactory.getBean方法,尝试获取A对象。
- 在AbstracBeanFactory.doGetBean方法中执行DefaultSingletonBeanRegistry#getSingleton方法,尝试从缓存中获取A对象的单例对象缓存。
- 到一级缓存singletonObjects中找,未找到;
- 到二级缓存earlySingletonObjects中找,未找到;
- 到三级缓存singletonFactories中找,找到了,并调用ObjectFactory的getObject方法获取A对象的引用,ObjectFactory内部调用了AbstractAutowireCapableBeanFactory#getEarlyBeanReference,获取到A的早期对象,将A的早期对象放入二级缓存earlySingletonObjects中,并将三级缓存singletonFactories中A对象移除;
- 这样拿到A的对象之后,B的属性填充完毕,B初始化完成,方法return到DefaultSingletonBeanRegistry#getSingleton的重载方法时,调用DefaultSingletonBeanRegistry#addSingleton方法,将B对象放入一级缓存,并将B从二三级缓存中移除(虽然已经没有了)。
- 这样在return回A的流程,第11步,将A依赖的B属性填充完整,此时A也填充完毕,初始化完成,方法继续return到A流程的DefaultSingletonBeanRegistry#getSingleton的重载方法时,调用DefaultSingletonBeanRegistry#addSingleton方法,将A对象放入一级缓存,并将A从二三级缓存中移除(此时只有二级缓存中有)。
- 这样A和B就初始化完成了。
- 如果A或者B存在AOP,需要返回代理对象,这操作是在第9步的AbstractAutowireCapableBeanFactory#getEarlyBeanReference中完成的,B尝试获取A的时候,触发了这个方法,如果A需要被代理,则是在这个方法中执行的,这个方法最终返回了一个代理对象,并将这个对象以A的名义放入了二级缓存。
- 打完收工。
源码
AbstractApplicationContext#refresh#finishBeanFactoryInitialization 入口类方法 刷新上下文,初始化BeanFactory,完成工厂初始化。
DefaultListableBeanFactory#preInstantiateSingletons 准备实例化单例对象
@Override public void preInstantiateSingletons() throws BeansException { if (logger.isTraceEnabled()) { logger.trace("Pre-instantiating singletons in " + this); } // Iterate over a copy to allow for init methods which in turn register new bean definitions. // While this may not be part of the regular factory bootstrap, it does otherwise work fine. List<String> beanNames = new ArrayList<>(this.beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans... for (String beanName : beanNames) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { if (isFactoryBean(beanName)) { Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); if (bean instanceof FactoryBean) { FactoryBean<?> factory = (FactoryBean<?>) bean; boolean isEagerInit; if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { isEagerInit = AccessController.doPrivileged( (PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit, getAccessControlContext()); } else { isEagerInit = (factory instanceof SmartFactoryBean && ((SmartFactoryBean<?>) factory).isEagerInit()); } if (isEagerInit) { getBean(beanName); } } } else { getBean(beanName); } } } // Trigger post-initialization callback for all applicable beans... for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize") .tag("beanName", beanName); SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; if (System.getSecurityManager() != null) { AccessController.doPrivileged((PrivilegedAction<Object>) () -> { smartSingleton.afterSingletonsInstantiated(); return null; }, getAccessControlContext()); } else { smartSingleton.afterSingletonsInstantiated(); } smartInitialize.end(); } } }
AbstractBeanFactory#getBean(java.lang.String) 获取bean对象
@Override public Object getBean(String name) throws BeansException { return doGetBean(name, null, null, false); }
AbstractBeanFactory#doGetBean 实际执行获取bean对象
/** * Return an instance, which may be shared or independent, of the specified bean. * @param name the name of the bean to retrieve * @param requiredType the required type of the bean to retrieve * @param args arguments to use when creating a bean instance using explicit arguments * (only applied when creating a new instance as opposed to retrieving an existing one) * @param typeCheckOnly whether the instance is obtained for a type check, * not for actual use * @return an instance of the bean * @throws BeansException if the bean could not be created */ @SuppressWarnings("unchecked") protected <T> T doGetBean( String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { String beanName = transformedBeanName(name); Object beanInstance; // Eagerly check singleton cache for manually registered singletons. Object sharedInstance = getSingleton(beanName); if (sharedInstance != null && args == null) { 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 + "'"); } } beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // Fail if we're already creating this bean instance: // We're assumably within a circular reference. if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // Check if bean definition exists in this factory. BeanFactory parentBeanFactory = getParentBeanFactory(); if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // Not found -> check parent. String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); } 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) { markBeanAsCreated(beanName); } StartupStep beanCreation = this.applicationStartup.start("spring.beans.instantiate") .tag("beanName", name); try { if (requiredType != null) { beanCreation.tag("beanType", requiredType::toString); } RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); checkMergedBeanDefinition(mbd, beanName, args); // Guarantee initialization of beans that the current bean depends on. String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } registerDependentBean(dep, beanName); try { getBean(dep); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } } } // Create bean instance. if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { 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; } }); beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { String scopeName = mbd.getScope(); if (!StringUtils.hasLength(scopeName)) { throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'"); } Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new ScopeNotActiveException(beanName, scopeName, ex); } } } catch (BeansException ex) { beanCreation.tag("exception", ex.getClass().toString()); beanCreation.tag("message", String.valueOf(ex.getMessage())); cleanupAfterBeanCreationFailure(beanName); throw ex; } finally { beanCreation.end(); } } return adaptBeanInstance(name, beanInstance, requiredType); }
AbstractAutowireCapableBeanFactory.java createBean、doCreateBean、getEarlyBeanReference
@Override 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. Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } // Prepare method overrides. try { 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. Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex); } try { Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isTraceEnabled()) { logger.trace("Finished creating instance of bean '" + beanName + "'"); } 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); } } protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { // Instantiate the 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; } // Allow post-processors to modify the merged bean definition. 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; } } // Eagerly cache singletons to be able to resolve circular references // even when triggered by lifecycle interfaces like BeanFactoryAware. 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)); } // Initialize the bean instance. 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; } 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; }
DefaultSingletonBeanRegistry.java 三级缓存
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { /** Cache of singleton objects: bean name to bean instance. 一级缓存 */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name to ObjectFactory. 三级缓存 */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. 二级缓存 */ private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); ... /** * Return the (raw) singleton object registered under the given name. * <p>Checks already instantiated singletons and also allows for an early * reference to a currently created singleton (resolving a circular reference). * @param beanName the name of the bean to look for * @param allowEarlyReference whether early references should be created or not * @return the registered singleton object, or {@code null} if none found */ @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // Consistent creation of early reference within full singleton lock singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; } }
到此这篇关于Spring三级缓存解决循环依赖 的文章就介绍到这了,更多相关Spring 循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
SpringMVC之DispatcherServlet配置文件应该放在哪里呢
这篇文章主要介绍了SpringMVC之DispatcherServlet配置文件应该放在哪里的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2023-11-11
最新评论