Spring源码详解,面试必问(一)


为了以后不会忘记所学内容,特以记之,有疏忽错误的地方,还望指教


1st. IoC容器与bean

首先贴上一段专业的描述:

IoC容器:容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。

bean:在 Spring 中,构成应用程序主干并由Spring IoC容器管理的对象称为bean,bean是一个由Spring IoC容器实例化、组装和管理的对象。

下面我来举个例子。IoC容器,就像搅拌机一样,而我们所说的bean,就是装在里面的食物。在使用时,我们首先要倒进去食物,再经过处理后得到搅拌机搅拌过的混合物,所以为了方便使用,我们就以map的数据结构来存储,并且给bean取了名字,在map中的key是beanName,而value是bean实例对象。这样我们才能找出想要吃的特定的那一种类美味的食物。

那我们要怎么拿到食物(bean)呢?比如通过context.getBean(class)之类的方式,可是想要获取容器里的食物(bean),我们肯定需要先倒进去食物,也就是创建对象

  • 使用new关键字
  • 使用工厂
  • 使用反射

那么在创建对象之前,我们需要对bean的属性信息进行定义,再将定义好的信息放入xml、json等格式的配置文件里,通过一种规范了配置文件格式的抽象接口传递给IoC容器中,这种抽象接口叫做BeanDefinitionReader,它的作用是读取 Spring 配置文件中的内容,将其转换为 IoC 容器内部的数据结构:BeanDefinition

我们可以想象为将原料(bean的属性信息)用胶囊包起来(xml等配置文件)通过加工机器(BeanDefinitionReader)加工后后,胶囊变成了加工食品(BeanDefinition)被装入搅拌机(IOC容器)里。搅拌机再经过搅拌,制作出美味的饮品(bean对象)。

这里作为提醒,想要拓展一点相关知识,关于实例化初始化的区分。

  • 实例化:是指创建一个对象的过程。这个过程会在堆中开辟内存,将一些非静态的方法、变量存放在里面。在程序执行的过程中,可以创建多个对象,即多次实例化。属性是默认值。每次实例化都会开辟一块新的内存。

  • 初始化:是完成程序执行前的准备工作。在这个阶段,**静态的(变量,方法,代码块)会被执行,同时会开辟一块存储空间用来存放静态的数据。**初始化只在类加载的时候执行一次。

现在,我们将BeanDefinition存入IoC容器中,就该创建对象了。在创建对象时,需要对bean先进行实例化,而后初始化,得到一个完整的bean对象。

2nd. 三个重要接口

在我们进行后续流程前,我们需要先探讨一下非常重要的三个接口:
BeanFactory接口、PostProcessor接口和Environment接口。

首先我们聊一聊spring中最熟悉的接口——BeanFactory接口,给出的介绍是进入spring bean容器的根接口。

The root interface for accessing a Spring bean container

当然咯,我们可以在BeanFactory中对对象进行创建。

先获取对象:

Class a = Class.forName(“完全限定名”);

Class a = 类.class;

Class a = 对象.getClass();

再实例化对象:

Object obj=a.newInstance();//适用于无参构造方法

Object obj=a.getDeclaredConstructor().newInstance();//适用于无参和有参构造方法

我们再来介绍PostProcessor接口,它分为BeanFactoryPostProcessor(Bean 工厂后置处理器)和BeanPostProcessor(Bean 后置处理器)

BeanFactoryPostProcessor:

作为增强器,我们可以通过BeanFactoryPostProcessor对原来的BeanDefinition也就是bean的定义(配置元数据)进行拓展。

也就是说,Spring IoC容器允许BeanFactoryPostProcessor在容器实际实例化任何其他的bean之前读取配置元数据,并有可能修改它。

要记住,BeanFactoryPostProcessor的作用域范围是容器级的,只和我们所使用的容器有关,如果我们在容器中定义一个BeanFactoryPostProcessor,它仅仅对容器中的bean进行后置处理,而不会对定义在另一个容器中的bean进行后置处理,即使两个容器都是在同一层次滴。
在这里插入图片描述
上图为BeanFactoryPostProcessor接口内的方法
在这里插入图片描述
上图为BeanFactoryPostProcessor示例

BeanPostProcessor:
在这里插入图片描述
在这里插入图片描述
上两张图为BeanPostProcessor接口定义的两种方法

如果我们想要改编实际的bean实例(例如从配置元数据创建的对象),那么我们就要使用BeanPostProcessor啦。

我们在bean实例化后使用,可以在初始化bean前后调用方法添加我们需要的功能逻辑(其中Before是实例化bean后、初始化bean前使用,而after是在实例化bean后、初始化bean后使用)
在这里插入图片描述
(图片来自马士兵教育)

第三个接口是Environment接口,主要管理应用程序的两个方面内容:profile和properties

profile相当于管理常见的环境,例如开发(dev)、测试(stg)等,在容器创建时会提前将系统的相关属性加载到StandardEnvironment对象中,方便后续使用。

而properties,是提供给用户方便的服务接口、以便于修改配置、解析配置。

3rd. xml在IoC容器中创建bean对象的流程


锵锵——
终于!我们可以开始介绍创建bean对象的流程啦,下面请跟紧我的步伐,先来简单看一下具体流程,详细细节我们会以后再聊~

首先我们假设已经获取到了xml路径,之后就可以根据路径做配置文件的解析及其他功能的实现。那么想要了解这一整个流程,我们最需要了解的就是refresh函数。

敲黑板!一定要对refresh函数很熟悉哦!

public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      //1.准备刷新的上下文环境
      prepareRefresh();

      //Tell the subclass to refresh the internal bean factory.
      //2. 初始化BeanFactory,并进行xml文件读取
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      //Prepare the bean factory for use in this context.
      //3. 进行各种功能填充
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         //4.子类覆盖方法做额外的处理
         postProcessBeanFactory(beanFactory);

         // Invoke factory processors registered as beans in the context.
         //5.激活各种BeanFactory处理器
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         //6.注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用是在getBean时候
         registerBeanPostProcessors(beanFactory);

         //Initialize message source for this context.
         //7. 为上下文初始化Message源,即不同语言的消息体,国际化处理
         initMessageSource();

         //Initialize event multicaster for this context.
         //8. 初始化应用消息广播器,并放入"applicationEventMulticaster"bean中
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         //9.留给子类来初始化其他的Bean
         onRefresh();

         // Check for listener beans and register them.
         //10.在所有注册的bean中查找Listener bean,注册到消息广播器中
         registerListeners();

         //Instantiate all remaining (non-lazy-init) singletons.
         //11. 初始化剩下的单实例(非懒加载)
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         //12.完成刷新过程,通知生命周期处理器 lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         //Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

(1)初始化前的准备工作,例如对系统属性或环境变量进行准备与验证

(2)初始化BeanFactory,并读取xml文件。

​ 如果我们不看获取xml路径的步骤,那么第一步就是初始化BeanFactory,譬如创业,首先需要有个公司,否则无法进行后续流程。

(3)对BeanFactory进行功能属性填充。

(4)子类覆盖方法做额外的处理。

(5)激活各种BeanFactory处理器。

(6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean的时候。

(7)为上下文初始化Message源,即对不同语言的消息体进行国际化处理。

(8)初始化应用消息广播器,并放入“applicationEventMulticaster”bean中。

(9)留给子类来初始化其他的bean。

(10)在所有注册的bean中查找listener bean,注册到消息广播器中。

(11)初始化剩下的单例(非惰性的)。

(12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。

一、环境准备

首先我们来看refresh中的第一个方法prepareRefresh:

// Prepare this context for refreshing.
//1.准备刷新的上下文环境
prepareRefresh();

prepareRefresh函数主要做准备刷新的工作,例如对系统属性及环境变量的初始化及验证。

下面我们进入到prepareRefresh函数中:
在这里插入图片描述
如上图,在prepareRefresh函数中,我们首先要设置容器启动时间、设置容器关闭为false、激活为true。

第一个方法是initPropertySources,用来初始化属性资源。

// Initialize any placeholder property sources in the context environment.
initPropertySources();

点进initPropertySources方法后会发现是空的,为了留给子类覆盖:
在这里插入图片描述
第二个方法是getEnvironment,用来验证需要的属性文件是否都已经放入环境中。

// Validate that all properties marked as required are resolvable:
// see ConfigurablePropertyResolver#setRequiredProperties
getEnvironment().validateRequiredProperties();

在这里插入图片描述

点进去getEnvironment方法后发现确实是验证创建环境。

我们来举个例子使用这两个方法。

假如有一个需求,工程在运行过程中需要某个设置(“pro”)是从系统环境变量中取得的,而如果用户没有再系统环境变量中配置这个参数,那么工程可能不会工作。

那么我们就可以重写initPropertySources方法,在其中添加getEnvironment().validateRequiredProperties(“pro”),这样在验证的时候也就是程序走到getEnvironment().validateRequiredProperties()代码时,如果系统没有检测到pro的环境变量,就会抛出异常。

二、加载BeanFactory

refresh的第二个方法obtainFreshBeanFactory,直译为获取BeanFactory,也就意味着,我们开始创建工厂啦!

//Tell the subclass to refresh the internal bean factory.
//2. 初始化BeanFactory,并进行xml文件读取
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

进入obtainFreshBeanFactory函数:

/**
 * Tell the subclass to refresh the internal bean factory.
 * @return the fresh BeanFactory instance
 * @see #refreshBeanFactory()
 * @see #getBeanFactory()
 */
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
   refreshBeanFactory();
   return getBeanFactory();
}

第一个方法是refreshBeanFactory,作用是初始化BeanFactory,并进行xml文件读取,将得到的BeanFactory记录在当前实体的属性中。再返回当前实体的BeanFactory属性。

进入refreshBeanFactory方法。

/**
 * This implementation performs an actual refresh of this context's underlying
 * bean factory, shutting down the previous bean factory (if any) and
 * initializing a fresh bean factory for the next phase of the context's lifecycle.
 */
@Override
protected final void refreshBeanFactory() throws BeansException {
	//(1)
   if (hasBeanFactory()) {
      destroyBeans();
      closeBeanFactory();
   }
   try {
  	  //(2)
      DefaultListableBeanFactory beanFactory = createBeanFactory();
      //(3)
      beanFactory.setSerializationId(getId());
      //(4)
      customizeBeanFactory(beanFactory);
      //(5)
      loadBeanDefinitions(beanFactory);
      synchronized (this.beanFactoryMonitor) {
         this.beanFactory = beanFactory;
      }
   }
   catch (IOException ex) {
      throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
   }
}

那么我们来详细分析以上步骤,下述序号与上图代码注释中的序号等同:

(1)我们首先判断有没有BeanFactory,有的话就销毁。假设我们这次是初建工厂,所以进行下一步~

(2)创建DefaultListableBeanFactory,这个可是容器的基础,一定要记住哦。

(3)在创建完工厂,我们需要设置工厂的序列化id,这样子其他人才能找到我们的工厂,也就是将之前xml文件读取到工厂中。

(4)这一步我们要定制beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖,比如设置@Autowired和@Qualifier注解解析器QualifierAnnotationAutowireCandidateResolver。

(5)初始化DodumentReader,进行xml文件读取及解析。加载BeanDefinition。

三、功能扩展

refresh的第三个方法prepareBeanFactory:

//3. 进行各种功能填充
prepareBeanFactory(beanFactory);

进入prepareBeanFactory方法后,如下图,我们发现是对功能的补充。

在这里插入图片描述

refresh的第四个方法postProcessBeanFactory:

// Allows post-processing of the bean factory in context subclasses.
//4.子类覆盖方法做额外的处理
postProcessBeanFactory(beanFactory);

我们进入该方法,会发现里面没有内容,也就是说,postProcessBeanFactory是模板方法,需要的时候我们可以进行扩展实现。
在这里插入图片描述

四、BeanFactory的后处理

refresh的第五个方法invokeBeanFactoryPostProcessors:

// Invoke factory processors registered as beans in the context.
//5.激活各种BeanFactory处理器
invokeBeanFactoryPostProcessors(beanFactory);

这个方法的作用就是调用激活所有注册的BeanFactoryPostProcessors。

在这里插入图片描述

refresh的第六个方法registerBeanPostProcessors:

// Register bean processors that intercept bean creation.
//6.注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用是在getBean时候
registerBeanPostProcessors(beanFactory);

这个方法的作用是注册BeanPostProcessors,记住不是调用而是注册,跟上一个方法可不一样哦,本方法是现在不执行,等到实例化时再执行。

注意,Spring中大部分功能都是通过后处理器的方式进行扩展的,但是在BeanFactory中其实并没有实现后处理器的自动注册,所以在调用的时候如果没有进行手动注册其实是不能使用的。

在这里插入图片描述

refresh的第七个方法initMessageSource:

//Initialize message source for this context.
//7. 为上下文初始化Message源,即不同语言的消息体,国际化处理
initMessageSource();

这个方法的作用是作国际化处理。

在这里插入图片描述

refresh的第八个方法initApplicationEventMulticaster:

//Initialize event multicaster for this context.
//8. 初始化应用消息广播器,并放入"applicationEventMulticaster"bean中
initApplicationEventMulticaster();

这个方法的作用是初始化ApplicationEventMulticaster广播器。

这种方式比较简单,也就是考虑两种情况:

  • 如果用户自定义了事件广播器,那么使用用户自定义的事件广播器。
  • 如果用户没有自定义事件广播器,那么使用默认的ApplicationEventMulticaster。
refresh的第九个方法onRefresh:

// Initialize other special beans in specific context subclasses.
//9.留给子类来初始化其他的Bean
onRefresh();

该方法中是空的,留给子类。

在这里插入图片描述

refresh的第十个方法registerListeners:

// Check for listener beans and register them.
//10.在所有注册的bean中查找Listener bean,注册到消息广播器中
registerListeners();

我们进入registerListeners方法中:

(1)硬编码方式注册的监听器处理

(2)配置文件注册的监听器处理

protected void registerListeners() {
   // Register statically specified listeners first.
   //(1)
   for (ApplicationListener<?> listener : getApplicationListeners()) {
      getApplicationEventMulticaster().addApplicationListener(listener);
   }

   // Do not initialize FactoryBeans here: We need to leave all regular beans
   // uninitialized to let post-processors apply to them!
   //(2
   String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
   for (String listenerBeanName : listenerBeanNames) {
      getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
   }

五、初始化非懒加载单例

refresh的第十一个方法finishBeanFactoryInitialization:

//Instantiate all remaining (non-lazy-init) singletons.
//11. 初始化剩下的单实例(非懒加载)
finishBeanFactoryInitialization(beanFactory);

首先单例模式指的是,确保在任何时候,该类只有唯一一个实例。

单例的创建有两种方式:

  1. 非懒加载(非延迟加载):不管什么时候使用,都要先提前创建实例。
  2. 懒加载(延迟加载):等到真正要使用的时候才去创建实例,不用时不要去创建。

我们进入finishBeanFactoryInitialization方法中,其中有两个方法我们来解释一下。

第一个方法是beanFactory.freezeConfiguration():

// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();

作用是冻结所有的bean定义,说明注册的bean定义将不被修改或进行任何进一步的处理。

第二个方法是beanFactory.preInstantiateSingletons():

// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();

作用是实例化所有剩下的非懒加载的单例。

ApplicationContext实现的默认行为就是在启动时将所有单例的bean都提前实例化。这意味着作为初始化过程中的一部分,ApplicationContext实例会创建并配置所有单例bean,这样在配置中的任何错误就会立刻发现。而这个实例化的过程就是在finishBeanFactoryInitialization中完成的。

我们进入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.
   //(1)
   List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
   // Trigger initialization of all non-lazy singleton beans...
   for (String beanName : beanNames) {
   //(2)
      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
      if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
      //(3)
         if (isFactoryBean(beanName)) {
            Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
            if (bean instanceof FactoryBean) {
               final 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 {
          //(4)
            getBean(beanName);
         }
      }
   }

   // Trigger post-initialization callback for all applicable beans...
   for (String beanName : beanNames) {
      Object singletonInstance = getSingleton(beanName);
      if (singletonInstance instanceof SmartInitializingSingleton) {
         final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
         if (System.getSecurityManager() != null) {
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
               smartSingleton.afterSingletonsInstantiated();
               return null;
            }, getAccessControlContext());
         }
         else {
            smartSingleton.afterSingletonsInstantiated();
         }
      }
   }
}

(1)我们将bean定义里的名字放入list集合中,从头循环

(2)我们获取bean定义信息内容,判断bean是否是抽象、单例、懒加载的

此处插一嘴,关于BeanFactory和FactoryBean的区分:

BeanFactory是一个bean工厂,FactoryBean是一个java bean。

BeanFactory是Ioc容器的根接口,Spring不允许我们直接操作BeanFactory bean工厂,就为我们提供了ApplicationContext这个接口,继承自BeanFactory接口,ApplicationContext包含BeanFactory的所有功能。

而FactoryBean是spirng提供的⼯⼚bean的⼀个接⼝,FactoryBean接⼝提供三个⽅法,⽤来创建对象

(3)判断name的bean对象是否属于FactoryBean

(4)创建bean之前我们要先获取bean:

进入后看到下图doGetBean方法
在这里插入图片描述
我们再深入进这个方法:

由于代码太多,我就不贴在这里啦,我们主要看比较重要的一些方法就好。

(4.1)获取单例对象,因为我们是第一次创建,所以这个地方为null。

// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);

(4.2)获取依赖属性

// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();

(4.3)我们开始创建bean

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;
      }
   });
   bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

我们再深入进createBean(beanName, mbd, args)中。

(4.3.1)找到doCreateBean方法。

try {
   Object beanInstance = doCreateBean(beanName, mbdToUse, args);
   if (logger.isTraceEnabled()) {
      logger.trace("Finished creating instance of bean '" + beanName + "'");
   }
   return beanInstance;
}

进入doCreateBean(beanName, mbdToUse, args)方法。

(4.3.1.1)

发现createBeanInstance方法,终于开始实例化bean!我们继续进入这个方法。

if (instanceWrapper == null) {
   instanceWrapper = createBeanInstance(beanName, mbd, args);

(4.3.1.1.1)

看到这一行代码,我们需要先获取构造器,才能实例化,于是就要验证这个构造器是否是我们需要的那个。

// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);

(4.3.1)我们回到doCreateBean方法中去,找到下面populateBean方法,把xml配置文件中的属性填充给bean。

// Initialize the bean instance.
Object exposedObject = bean;
try {
   populateBean(beanName, mbd, instanceWrapper);
   exposedObject = initializeBean(beanName, exposedObject, mbd);
}

我们再执行aware方法。

invokeAwareMethods(beanName, bean);

然后就可以执行applyBeanPostProcessorsBeforeInitialization方法。

继续invokeInitMethods方法初始化bean。

最后执行applyBeanPostProcessorsAfterInitialization方法。

返回当前bean对象。

		Object wrappedBean = bean;
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		try {
			invokeInitMethods(beanName, wrappedBean, mbd);
		}
		catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}

		return wrappedBean;

至此,我们在IoC容器里创建bean对象的过程就结束啦。

最后执行applyBeanPostProcessorsAfterInitialization方法。

返回当前bean对象。

		Object wrappedBean = bean;
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		try {
			invokeInitMethods(beanName, wrappedBean, mbd);
		}
		catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}

		return wrappedBean;

至此,我们在IoC容器里创建bean对象的过程就结束啦。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值