【Spring】IOC核心源码学习(二):容器初始化过程

http://singleant.iteye.com/blog/1177358

接上文啃啃老菜: Spring IOC核心源码学习(一),本文将以ClassPathXmlApplicationContext这个容器的实现作为基础,学习容器的初始化过程。

ClassPathXmlApplicationContext类体系结构

以下是ClassPathXmlApplicationContext的类继承体系结构,理解这个结构有助于后面的代码理解。


左边黄色部分是ApplicationContext体系继承结构,右边是BeanFactory的结构体系,两个结构是典型模板方法设计模式的使用。

从该继承体系可以看出:

1.BeanFactory是一个bean工厂的最基本定义,里面包含了一个bean工厂的几个最基本的方法,getBean(…)、containsBean(…)等,是一个很纯粹的bean工厂,不关注资源、资源位置、事件等。ApplicationContext是一个容器的最基本接口定义,它继承了BeanFactory,拥有工厂的基本方法。同时继承了ApplicationEventPublisher、MessageSource、ResourcePatternResolver等接口,使其定义了一些额外的功能,如资源、事件等这些额外的功能。

2.AbstractBeanFactory和AbstractAutowireCapableBeanFactory是两个模板抽象工厂类。AbstractBeanFactory提供了bean工厂的抽象基类,同时提供了ConfigurableBeanFactory的完整实现。AbstractAutowireCapableBeanFactory是继承了AbstractBeanFactory的抽象工厂,里面提供了bean创建的支持,包括bean的创建、依赖注入、检查等等功能,是一个核心的bean工厂基类。

3.ClassPathXmlApplicationContext之所以拥有bean工厂的功能是通过持有一个真正的bean工厂DefaultListableBeanFactory的实例,并通过代理该工厂完成。

4.ClassPathXmlApplicationContext的初始化过程是对本身容器的初始化同时也是对其持有的DefaultListableBeanFactory的初始化。

下面通过源码着重介绍一个容器的初始化过程,并重点理解bean的创建过程。

容器初始化过程

通过上文啃啃老菜: Spring IOC核心源码学习(一)已经可以了解一个容器的大概过程是:


整个过程可以理解为是容器的初始化过程。第一个过程是ApplicationContext的职责范围,第二步是BeanFactory的职责范围。可以看出ApplicationContext是一个运行时的容器需要提供不容资源环境的支持,屏蔽不同环境的差异化。而BeanDifinition是内部关于bean定义的基本结构。Bean的创建就是基于它,回头会介绍一下改结构的定义。下面看一下整个容器的初始化过程。

容器的初始化是通过调用refresh()来实现。该方法是非常重要的一个方法,定义在AbstractApplicationContext接口里。AbstractApplicationContext是容器的最基础的一个抽象父类。也就是说在该里面定义了一个容器初始化的基本流程,流程里的各个方法有些有提供了具体实现,有些是抽象的(因为不同的容器实例不一样),由继承它的每一个具体容器完成定制。看看refresh的基本流程:

Java代码 收藏代码
  1. publicvoidrefresh()throwsBeansException,IllegalStateException{
  2. synchronized(this.startupShutdownMonitor){
  3. //Preparethiscontextforrefreshing.
  4. prepareRefresh();
  5. //Tellthesubclasstorefreshtheinternalbeanfactory.
  6. ConfigurableListableBeanFactorybeanFactory=obtainFreshBeanFactory();
  7. //Preparethebeanfactoryforuseinthiscontext.
  8. prepareBeanFactory(beanFactory);
  9. try{
  10. //Allowspost-processingofthebeanfactoryincontextsubclasses.
  11. postProcessBeanFactory(beanFactory);
  12. //Invokefactoryprocessorsregisteredasbeansinthecontext.
  13. invokeBeanFactoryPostProcessors(beanFactory);
  14. //Registerbeanprocessorsthatinterceptbeancreation.
  15. registerBeanPostProcessors(beanFactory);
  16. //Initializemessagesourceforthiscontext.
  17. initMessageSource();
  18. //Initializeeventmulticasterforthiscontext.
  19. initApplicationEventMulticaster();
  20. //Initializeotherspecialbeansinspecificcontextsubclasses.
  21. onRefresh();
  22. //Checkforlistenerbeansandregisterthem.
  23. registerListeners();
  24. //Instantiateallremaining(non-lazy-init)singletons.
  25. finishBeanFactoryInitialization(beanFactory);
  26. //Laststep:publishcorrespondingevent.
  27. finishRefresh();
  28. }
  29. catch(BeansExceptionex){
  30. //Destroyalreadycreatedsingletonstoavoiddanglingresources.
  31. beanFactory.destroySingletons();
  32. //Reset'active'flag.
  33. cancelRefresh(ex);
  34. //Propagateexceptiontocaller.
  35. throwex;
  36. }
  37. }
  38. }

解释如下:


Bean的创建过程

Bean的创建过程基本是BeanFactory所要完成的事情.

根据以上过程,将会重点带着以下两个个问题来理解核心代码:

1.Bean的创建时机

bean是在什么时候被创建的,有哪些规则。

2.Bean的创建过程

bean是怎么创建的,会选择哪个构造函数?依赖如何注入?InitializingBean的set方法什么时候被调用?实现ApplicationContextAware, BeanFactoryAware,BeanNameAware, ResourceLoaderAware这些接口的bean的set方法何时被调用?

在解释这两个问题前,先看一下BeanDefinition接口的定义。


从该接口定义可以看出,通过bean定义能够得到bean的详细信息,如类名子、工厂类名称、scope、是否单例、是否抽象、是否延迟加载等等。基于此,来看一下以下两个问题:

问题1:Bean的创建时机

bean是在什么时候被创建的,有哪些规则?

容器初始化的时候会预先对单例和非延迟加载的对象进行预先初始化。其他的都是延迟加载是在第一次调用getBean的时候被创建。从DefaultListableBeanFactory的preInstantiateSingletons里可以看到这个规则的实现。

Java代码 收藏代码
  1. publicvoidpreInstantiateSingletons()throwsBeansException{
  2. if(this.logger.isInfoEnabled()){
  3. this.logger.info("Pre-instantiatingsingletonsin"+this);
  4. }
  5. synchronized(this.beanDefinitionMap){
  6. for(Iteratorit=this.beanDefinitionNames.iterator();it.hasNext();){
  7. StringbeanName=(String)it.next();
  8. RootBeanDefinitionbd=getMergedLocalBeanDefinition(beanName);
  9. <spanstyle="color:#ff0000;">if(!bd.isAbstract()&&bd.isSingleton()&&!bd.isLazyInit()){</span>
  10. //对非抽象、单例的和非延迟加载的对象进行实例化。
  11. if(isFactoryBean(beanName)){
  12. FactoryBeanfactory=(FactoryBean)getBean(FACTORY_BEAN_PREFIX+beanName);
  13. if(factoryinstanceofSmartFactoryBean&&((SmartFactoryBean)factory).isEagerInit()){
  14. getBean(beanName);
  15. }
  16. }
  17. else{
  18. getBean(beanName);
  19. }
  20. }
  21. }
  22. }
  23. }


从上面来看对于以下配置,只有singletonBean会被预先创建。

Xml代码 收藏代码
  1. <?xmlversion="1.0"encoding="GB2312"?>
  2. <!DOCTYPEbeansPUBLIC"-//SPRING//DTDBEAN2.0//EN""http://www.springframework.org/dtd/spring-beans-2.0.dtd">
  3. <beansdefault-autowire="byName">
  4. <beanid="otherBean"class="com.test.OtherBean"scope="prototype"/>
  5. <beanid="myBean"class="com.test.MyBean"lazy-init="true"/>
  6. <beanid="singletonBean"class="com.test.SingletonBean"/>
  7. </beans>

问题二:Bean的创建过程

对于bean的创建过程其实都是通过调用工厂的getBean方法来完成的。这里面将会完成对构造函数的选择、依赖注入等。

无论预先创建还是延迟加载都是调用getBean实现,AbstractBeanFactory定义了getBean的过程:

Java代码 收藏代码
  1. protectedObjectdoGetBean(
  2. finalStringname,finalClassrequiredType,finalObject[]args,booleantypeCheckOnly)throwsBeansException{
  3. finalStringbeanName=transformedBeanName(name);
  4. Objectbean=null;
  5. //Eagerlychecksingletoncacheformanuallyregisteredsingletons.
  6. ObjectsharedInstance=getSingleton(beanName);
  7. if(sharedInstance!=null&&args==null){
  8. if(logger.isDebugEnabled()){
  9. if(isSingletonCurrentlyInCreation(beanName)){
  10. logger.debug("Returningeagerlycachedinstanceofsingletonbean'"+beanName+
  11. "'thatisnotfullyinitializedyet-aconsequenceofacircularreference");
  12. }
  13. else{
  14. logger.debug("Returningcachedinstanceofsingletonbean'"+beanName+"'");
  15. }
  16. }
  17. bean=getObjectForBeanInstance(sharedInstance,name,beanName,null);
  18. }
  19. else{
  20. //Failifwe'realreadycreatingthisbeaninstance:
  21. //We'reassumablywithinacircularreference.
  22. if(isPrototypeCurrentlyInCreation(beanName)){
  23. thrownewBeanCurrentlyInCreationException(beanName);
  24. }
  25. //Checkifbeandefinitionexistsinthisfactory.
  26. BeanFactoryparentBeanFactory=getParentBeanFactory();
  27. if(parentBeanFactory!=null&&!containsBeanDefinition(beanName)){
  28. //Notfound->checkparent.
  29. StringnameToLookup=originalBeanName(name);
  30. if(args!=null){
  31. //Delegationtoparentwithexplicitargs.
  32. returnparentBeanFactory.getBean(nameToLookup,args);
  33. }
  34. else{
  35. //Noargs->delegatetostandardgetBeanmethod.
  36. returnparentBeanFactory.getBean(nameToLookup,requiredType);
  37. }
  38. }
  39. if(!typeCheckOnly){
  40. markBeanAsCreated(beanName);
  41. }
  42. finalRootBeanDefinitionmbd=getMergedLocalBeanDefinition(beanName);
  43. checkMergedBeanDefinition(mbd,beanName,args);
  44. //Guaranteeinitializationofbeansthatthecurrentbeandependson.
  45. String[]dependsOn=mbd.getDependsOn();
  46. if(dependsOn!=null){
  47. for(inti=0;i<dependsOn.length;i++){
  48. StringdependsOnBean=dependsOn[i];
  49. getBean(dependsOnBean);
  50. registerDependentBean(dependsOnBean,beanName);
  51. }
  52. }
  53. //Createbeaninstance.
  54. <spanstyle="color:#ff0000;">if(mbd.isSingleton()){//单例对象创建过程,间接通过getSingleton方法来创建,里面会实现将单例对象缓存</span>
  55. sharedInstance=getSingleton(beanName,newObjectFactory(){
  56. publicObjectgetObject()throwsBeansException{
  57. try{
  58. returncreateBean(beanName,mbd,args);
  59. }
  60. catch(BeansExceptionex){
  61. //Explicitlyremoveinstancefromsingletoncache:Itmighthavebeenputthere
  62. //eagerlybythecreationprocess,toallowforcircularreferenceresolution.
  63. //Alsoremoveanybeansthatreceivedatemporaryreferencetothebean.
  64. destroySingleton(beanName);
  65. throwex;
  66. }
  67. }
  68. });
  69. bean=getObjectForBeanInstance(sharedInstance,name,beanName,mbd);
  70. }
  71. <spanstyle="color:#ff0000;">elseif(mbd.isPrototype()){//非单例对象创建</span>
  72. //It'saprototype->createanewinstance.
  73. ObjectprototypeInstance=null;
  74. try{
  75. beforePrototypeCreation(beanName);
  76. prototypeInstance=createBean(beanName,mbd,args);<spanstyle="color:#ff0000;">//直接调用createBean</span>
  77. }
  78. finally{
  79. afterPrototypeCreation(beanName);
  80. }
  81. bean=getObjectForBeanInstance(prototypeInstance,name,beanName,mbd);
  82. }
  83. else{
  84. StringscopeName=mbd.getScope();
  85. finalScopescope=(Scope)this.scopes.get(scopeName);
  86. if(scope==null){
  87. thrownewIllegalStateException("NoScoperegisteredforscope'"+scopeName+"'");
  88. }
  89. try{
  90. ObjectscopedInstance=scope.get(beanName,newObjectFactory(){
  91. publicObjectgetObject()throwsBeansException{
  92. beforePrototypeCreation(beanName);
  93. try{
  94. returncreateBean(beanName,mbd,args);
  95. }
  96. finally{
  97. afterPrototypeCreation(beanName);
  98. }
  99. }
  100. });
  101. bean=getObjectForBeanInstance(scopedInstance,name,beanName,mbd);
  102. }
  103. catch(IllegalStateExceptionex){
  104. thrownewBeanCreationException(beanName,
  105. "Scope'"+scopeName+"'isnotactiveforthecurrentthread;"+
  106. "considerdefiningascopedproxyforthisbeanifyouintendtorefertoitfromasingleton",
  107. ex);
  108. }
  109. }
  110. }
  111. //Checkifrequiredtypematchesthetypeoftheactualbeaninstance.
  112. if(requiredType!=null&&bean!=null&&!requiredType.isAssignableFrom(bean.getClass())){
  113. thrownewBeanNotOfRequiredTypeException(name,requiredType,bean.getClass());
  114. }
  115. returnbean;
  116. }

GetBean的大概过程:

1.先试着从单例缓存对象里获取。

2.从父容器里取定义,有则由父容器创建。

3.如果是单例,则走单例对象的创建过程:在spring容器里单例对象和非单例对象的创建过程是一样的。都会调用父类AbstractAutowireCapableBeanFactory的createBean方法。不同的是单例对象只创建一次并且需要缓存起来。DefaultListableBeanFactory的父类DefaultSingletonBeanRegistry提供了对单例对象缓存等支持工作。所以是单例对象的话会调用DefaultSingletonBeanRegistry的getSingleton方法,它会间接调用AbstractAutowireCapableBeanFactory的createBean方法。

如果是Prototype多例则直接调用父类AbstractAutowireCapableBeanFactory的createBean方法。

bean的创建是由AbstractAutowireCapableBeanFactory来定义:

Java代码 收藏代码
  1. protectedObjectcreateBean(finalStringbeanName,finalRootBeanDefinitionmbd,finalObject[]args)
  2. throwsBeanCreationException{
  3. AccessControlContextacc=AccessController.getContext();
  4. returnAccessController.doPrivileged(newPrivilegedAction(){
  5. publicObjectrun(){
  6. if(logger.isDebugEnabled()){
  7. logger.debug("Creatinginstanceofbean'"+beanName+"'");
  8. }
  9. //Makesurebeanclassisactuallyresolvedatthispoint.
  10. resolveBeanClass(mbd,beanName);
  11. //Preparemethodoverrides.
  12. try{
  13. mbd.prepareMethodOverrides();
  14. }
  15. catch(BeanDefinitionValidationExceptionex){
  16. thrownewBeanDefinitionStoreException(mbd.getResourceDescription(),
  17. beanName,"Validationofmethodoverridesfailed",ex);
  18. }
  19. try{
  20. //GiveBeanPostProcessorsachancetoreturnaproxyinsteadofthetargetbeaninstance.
  21. Objectbean=resolveBeforeInstantiation(beanName,mbd);
  22. if(bean!=null){
  23. returnbean;
  24. }
  25. }
  26. catch(Throwableex){
  27. thrownewBeanCreationException(mbd.getResourceDescription(),beanName,
  28. "BeanPostProcessorbeforeinstantiationofbeanfailed",ex);
  29. }
  30. ObjectbeanInstance=doCreateBean(beanName,mbd,args);
  31. if(logger.isDebugEnabled()){
  32. logger.debug("Finishedcreatinginstanceofbean'"+beanName+"'");
  33. }
  34. returnbeanInstance;
  35. }
  36. },acc);
  37. }

createBean会调用doCreateBean方法:

Java代码 收藏代码
  1. protectedObjectdoCreateBean(finalStringbeanName,finalRootBeanDefinitionmbd,finalObject[]args){
  2. //Instantiatethebean.
  3. BeanWrapperinstanceWrapper=null;
  4. if(mbd.isSingleton()){
  5. instanceWrapper=(BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
  6. }
  7. if(instanceWrapper==null){
  8. instanceWrapper=createBeanInstance(beanName,mbd,args);
  9. }
  10. finalObjectbean=(instanceWrapper!=null?instanceWrapper.getWrappedInstance():null);
  11. ClassbeanType=(instanceWrapper!=null?instanceWrapper.getWrappedClass():null);
  12. //Allowpost-processorstomodifythemergedbeandefinition.
  13. synchronized(mbd.postProcessingLock){
  14. if(!mbd.postProcessed){
  15. applyMergedBeanDefinitionPostProcessors(mbd,beanType,beanName);
  16. mbd.postProcessed=true;
  17. }
  18. }
  19. //Eagerlycachesingletonstobeabletoresolvecircularreferences
  20. //evenwhentriggeredbylifecycleinterfaceslikeBeanFactoryAware.
  21. booleanearlySingletonExposure=(mbd.isSingleton()&&this.allowCircularReferences&&
  22. isSingletonCurrentlyInCreation(beanName));
  23. if(earlySingletonExposure){
  24. if(logger.isDebugEnabled()){
  25. logger.debug("Eagerlycachingbean'"+beanName+
  26. "'toallowforresolvingpotentialcircularreferences");
  27. }
  28. addSingletonFactory(beanName,newObjectFactory(){
  29. publicObjectgetObject()throwsBeansException{
  30. returngetEarlyBeanReference(beanName,mbd,bean);
  31. }
  32. });
  33. }
  34. //Initializethebeaninstance.
  35. ObjectexposedObject=bean;
  36. try{
  37. populateBean(beanName,mbd,instanceWrapper);
  38. exposedObject=initializeBean(beanName,exposedObject,mbd);
  39. }
  40. catch(Throwableex){
  41. if(exinstanceofBeanCreationException&&beanName.equals(((BeanCreationException)ex).getBeanName())){
  42. throw(BeanCreationException)ex;
  43. }
  44. else{
  45. thrownewBeanCreationException(mbd.getResourceDescription(),beanName,"Initializationofbeanfailed",ex);
  46. }
  47. }
  48. if(earlySingletonExposure){
  49. ObjectearlySingletonReference=getSingleton(beanName,false);
  50. if(earlySingletonReference!=null){
  51. if(exposedObject==bean){
  52. exposedObject=earlySingletonReference;
  53. }
  54. elseif(!this.allowRawInjectionDespiteWrapping&&hasDependentBean(beanName)){
  55. String[]dependentBeans=getDependentBeans(beanName);
  56. SetactualDependentBeans=newLinkedHashSet(dependentBeans.length);
  57. for(inti=0;i<dependentBeans.length;i++){
  58. StringdependentBean=dependentBeans[i];
  59. if(!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)){actualDependentBeans.add(dependentBean);
  60. }
  61. }
  62. if(!actualDependentBeans.isEmpty()){
  63. thrownewBeanCurrentlyInCreationException(beanName,
  64. "Beanwithname'"+beanName+"'hasbeeninjectedintootherbeans["+
  65. StringUtils.collectionToCommaDelimitedString(actualDependentBeans)+
  66. "]initsrawversionaspartofacircularreference,buthaseventuallybeen"+
  67. "wrapped.Thismeansthatsaidotherbeansdonotusethefinalversionofthe"+
  68. "bean.Thisisoftentheresultofover-eagertypematching-considerusing"+
  69. "'getBeanNamesOfType'withthe'allowEagerInit'flagturnedoff,forexample.");
  70. }
  71. }
  72. }
  73. }
  74. //Registerbeanasdisposable.
  75. registerDisposableBeanIfNecessary(beanName,bean,mbd);
  76. returnexposedObject;
  77. }

doCreateBean的流程:

1.会创建一个BeanWrapper对象用于存放实例化对象。

2.如果没有指定构造函数,会通过反射拿到一个默认的构造函数对象,并赋予beanDefinition.resolvedConstructorOrFactoryMethod。

3.调用spring的BeanUtils的instantiateClass方法,通过反射创建对象。

4.applyMergedBeanDefinitionPostProcessors

5.populateBean(beanName, mbd, instanceWrapper);根据注入方式进行注入。根据是否有依赖检查进行依赖检查。

执行bean的注入里面会选择注入类型:

Java代码 收藏代码
  1. if(mbd.getResolvedAutowireMode()==RootBeanDefinition.AUTOWIRE_BY_NAME||
  2. mbd.getResolvedAutowireMode()==RootBeanDefinition.AUTOWIRE_BY_TYPE){
  3. MutablePropertyValuesnewPvs=newMutablePropertyValues(pvs);
  4. //Addpropertyvaluesbasedonautowirebynameifapplicable.
  5. if(mbd.getResolvedAutowireMode()==RootBeanDefinition.AUTOWIRE_BY_NAME){
  6. autowireByName(beanName,mbd,bw,newPvs);
  7. }<spanstyle="color:#ff0000;">//根据名字注入</span>
  8. //Addpropertyvaluesbasedonautowirebytypeifapplicable.
  9. if(mbd.getResolvedAutowireMode()==RootBeanDefinition.AUTOWIRE_BY_TYPE){
  10. autowireByType(beanName,mbd,bw,newPvs);
  11. }<spanstyle="color:#ff0000;">//根据类型注入</span>
  12. pvs=newPvs;
  13. }

6.initializeBean(beanName, exposedObject, mbd);

判断是否实现了BeanNameAware、BeanClassLoaderAware等spring提供的接口,如果实现了,进行默认的注入。同时判断是否实现了InitializingBean接口,如果是的话,调用afterPropertySet方法。

Java代码 收藏代码
  1. protectedObjectinitializeBean(StringbeanName,Objectbean,RootBeanDefinitionmbd){
  2. if(beaninstanceofBeanNameAware){
  3. ((BeanNameAware)bean).setBeanName(beanName);
  4. }
  5. if(beaninstanceofBeanClassLoaderAware){
  6. ((BeanClassLoaderAware)bean).setBeanClassLoader(getBeanClassLoader());
  7. }
  8. if(beaninstanceofBeanFactoryAware){
  9. ((BeanFactoryAware)bean).setBeanFactory(this);
  10. }
  11. ObjectwrappedBean=bean;
  12. if(mbd==null||!mbd.isSynthetic()){
  13. wrappedBean=applyBeanPostProcessorsBeforeInitialization(wrappedBean,beanName);
  14. }
  15. <spanstyle="color:#ff0000;">try{
  16. invokeInitMethods(beanName,wrappedBean,mbd);
  17. }</span>
  18. catch(Throwableex){
  19. thrownewBeanCreationException(
  20. (mbd!=null?mbd.getResourceDescription():null),
  21. beanName,"Invocationofinitmethodfailed",ex);
  22. }
  23. if(mbd==null||!mbd.isSynthetic()){
  24. wrappedBean=applyBeanPostProcessorsAfterInitialization(wrappedBean,beanName);
  25. }
  26. returnwrappedBean;
  27. }

其中invokeInitMethods实现如下:

Java代码 收藏代码
  1. protectedvoidinvokeInitMethods(StringbeanName,Objectbean,RootBeanDefinitionmbd)
  2. throwsThrowable{
  3. booleanisInitializingBean=(beaninstanceofInitializingBean);
  4. if(isInitializingBean&&(mbd==null||!mbd.isExternallyManagedInitMethod("afterPropertiesSet"))){
  5. if(logger.isDebugEnabled()){
  6. logger.debug("InvokingafterPropertiesSet()onbeanwithname'"+beanName+"'");
  7. }
  8. ((InitializingBean)bean).afterPropertiesSet();<spanstyle="color:#ff0000;">//调用afterPropertiesSet方法</span>
  9. }
  10. StringinitMethodName=(mbd!=null?mbd.getInitMethodName():null);
  11. if(initMethodName!=null&&!(isInitializingBean&&"afterPropertiesSet".equals(initMethodName))&&
  12. !mbd.isExternallyManagedInitMethod(initMethodName)){
  13. invokeCustomInitMethod(beanName,bean,initMethodName,mbd.isEnforceInitMethod());
  14. }
  15. }

总结

以上基本描述了spring容器的初始化过程和bean的创建过程。主要还是从大处着眼,很多细节没有涉及到。代码阅读总体感觉就是spring的代码抽象很好,结合结构读起来还是蛮顺利的。后面的学习将从细节着手。

下面源码学习预告:

1.Spring的声明式标签如实现

2.Spring aop代理如何实现


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值