Spring 5 AbstractBeanFactory -- getBean() 源码分析(一)

58 篇文章 1 订阅
56 篇文章 3 订阅

Spring 5 AbstractBeanFactory – getBean() 源码分析(一)
Spring 5 AbstractBeanFactory – getBean() 源码分析(二)

相关源码注释

ApplicationContext

Spring 5 DefaultResourceLoader 源码注释
Spring 5 AbstractApplicationContext 源码注释

BeanFactory

Spring 5 SimpleAliasRegistry 源码注释
Spring 5 DefaultSingletonBeanRegistry 源码注释
Spring 5 FactoryBeanRegistrySupport 源码注释
Spring 5 AbstractBeanFactory 源码注释
Spring 5 AbstractAutowireCapableBeanFactory 源码注释
Spring 5 DefaultLisbaleBeanFactory 源码注释

getBean

AbstractBeanFactory#getBean() 方法实现是交给 doGetBean() 方法具体实现的

@Override
	public Object getBean(String name) throws BeansException {
		return doGetBean(name, null, null, false);
	}

	@Override
	public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
		return doGetBean(name, requiredType, null, false);
	}

	@Override
	public Object getBean(String name, Object... args) throws BeansException {
		return doGetBean(name, null, args, false);
	}

	/**
	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>返回一个实例,该实例可以指定bean的共享或独立</p>
	 * @param name the name of the bean to retrieve
	 *              -- 要检索的Bean名
	 * @param requiredType the required type of the bean to retrieve
	 *                     -- 检查所需的Bean类型
	 * @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)
	 *             -- 使用显示参数创建Bean实例时要使用的参数(仅在创建新实例而不是检索现有实例时才应用)
	 * @return an instance of the bean -- Bean的一个实例
	 * @throws BeansException if the bean could not be created -- 如果无法创建Bean
	 */
	public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
			throws BeansException {
		//返回一个实例,该实例可以指定bean的共享或独立
		return doGetBean(name, requiredType, args, false);
	}

doGetBean

 	/*
 	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>返回一个实例,该实例可以指定bean的共享或独立</p>
	 * @param name the name of the bean to retrieve - 要检索的bean名称
	 * @param requiredType the required type of the bean to retrieve - 检索所需的bean类型
	 * @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)
	 *             -- 使用显示参数创建bean实例时要使用的参数(仅在创建新实例而不是检索现有实例时适用)
	 * @param typeCheckOnly whether the instance is obtained for a type check,
	 * not for actual use - 是否获取实例以进行类型检查,并非用于实际用途
	 * @return an instance of the bean - Bean的一个实例
	 * @throws BeansException if the bean could not be created - 如果无法创建该bean
	 */
	@SuppressWarnings("unchecked")
	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
		//获取name对应的规范名称【全类名】,包括以'&'开头的name
		final String beanName = transformedBeanName(name);
		Object bean;

		// Eagerly check singleton cache for manually registered singletons.
		// 译:认真检查单例缓存中是否有手动注册的单例。
		// 获取beanName的单例对象,并允许创建早期引用。
		// 	其最终实现:DefaultSingletonBeanRegistry#getSingleton(beanName,true);
		Object sharedInstance = getSingleton(beanName);
		//如果bean的单例对象找到了,且 没有创建bean实例时要使用的参数
		if (sharedInstance != null && args == null) {
			//打印跟踪日志
			if (logger.isTraceEnabled()) {
				//如果当前beanName正在创建(在整个工厂内)
				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 + "'");
				}
			}
			//获取给定Bean实例对象,如果是FactoryBean,则可以是bean实例本身或 创建的对象
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}

		else {
			// Fail if we're already creating this bean instance:
			// We're assumably within a circular reference.
			// 译:如果我们已经创建此bean实例,则失败:我们大概在循环引用之内
			// 如果当前线程内,beanName正在创建中
			if (isPrototypeCurrentlyInCreation(beanName)) {
				//抛出异常,表明该beanName在创建时抛出异常。
				throw new BeanCurrentlyInCreationException(beanName);
			}

			// Check if bean definition exists in this factory.
			// 译:检查该工厂中是否存在bean定义
			//获取当前父工厂
			BeanFactory parentBeanFactory = getParentBeanFactory();
			//如果父工厂存在,且 beanNeae对的定义不存在
			if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
				// Not found -> check parent. 找不到 -> 检查父工厂
				//获取name对应的规范名称【全类名】,如果name前面有'&',则会返回'&'+规范名称【全类名】
				String nameToLookup = originalBeanName(name);
				//如果父工厂是AbstractBeanFactory的实例
				if (parentBeanFactory instanceof AbstractBeanFactory) {
					//调用父工厂的doGetBean方法,就是该方法。【递归】
					return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
							nameToLookup, requiredType, args, typeCheckOnly);
				}
				else if (args != null) {//如果有创建bean实例时要使用的参数
					// Delegation to parent with explicit args. 使用显示参数委派给父工厂
					// 使用父工厂获取该bean对象,通bean全类名和创建bean实例时要使用的参数
					return (T) parentBeanFactory.getBean(nameToLookup, args);
				}
				else if (requiredType != null) {
					// No args -> delegate to standard getBean method.
					//没有创建bean实例时要使用的参数 -> 委托给标准的getBean方法。
					//使用父工厂获取该bean对象,通bean全类名和所需的bean类型
					return parentBeanFactory.getBean(nameToLookup, requiredType);
				}
				else {
					//使用父工厂获取bean,通过bean全类名
					return (T) parentBeanFactory.getBean(nameToLookup);
				}
			}

			//如果不需要检索所需的bean类型
			if (!typeCheckOnly) {
				//为beanName标记为已经创建(或将要创建)
				markBeanAsCreated(beanName);
			}

			try {
				//获取beanName合并后的RootBeanDefinition对象
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
				//检查mbd的合法性,不合格会引发验证异常
				checkMergedBeanDefinition(mbd, beanName, args);

				// Guarantee initialization of beans that the current bean depends on.
				// 确保当前bean依赖的bean的初始化
				//获取mbd所配置的bean的所需依赖的bean名
				String[] dependsOn = mbd.getDependsOn();
				//如果有依赖的bean名
				if (dependsOn != null) {
					//遍历依赖的bean名
					for (String dep : dependsOn) {
						//如果beanName已注册依赖于dependentBeanName的关系
						if (isDependent(beanName, dep)) {
							// 抛出创建Bean异常:循环依赖关系在beanName和dep之间
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
						}
						//注册dep与beanName的依赖关系
						registerDependentBean(dep, beanName);
						try {
							//递归 优先实例化被依赖的Bean
							getBean(dep);
						}
						//捕捉为找到BeanDefinition异常:'beanName'依赖于缺少的bean'dep'
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
						}
					}
				}

				// Create bean instance.
				// 创建bean实例
				if (mbd.isSingleton()) {
					//返回以beanName的(原始)单例对象,如果尚未注册,则使用singletonFactory创建并注册一个对象:
					sharedInstance = getSingleton(beanName, () -> {
						try {
							//为给定的合并后BeanDefinition(和参数)创建一个bean实例
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {//捕捉Bean异常
							// 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.
							// 显示地从单例缓存中删除实例:它可能是由创建过程急切地放在那里,以允许循环引用解析。还要删除
							// 接收到该Bean临时引用的任何Bean
							//销毁给定的bean。如果找到相应的一次性Bean实例,则委托给destoryBean
							destroySingleton(beanName);
							//重新抛出ex
							throw ex;
						}
					});
					//从 beanInstannce 中获取公开的Bean对象,主要处理beanInstance是FactoryBean对象的情况,如果不是
					// 	FactoryBean会直接 返回beanInstance实例
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}

				//如果mbd指定了ProtoType作用域
				else if (mbd.isPrototype()) {
					// It's a prototype -> create a new instance.
					// 它是一个原型 -> 创建一个新实例
					//定义 proptype实例
					Object prototypeInstance = null;
					try {
						//创建ProtoPype对象前的准备工作,默认实现 将beanName添加到 prototypesCurrentlyInCreation 中
						beforePrototypeCreation(beanName);
						//为mbd(和参数)创建一个bean实例
						prototypeInstance = createBean(beanName, mbd, args);
					}
					finally {
						//创建完prototype实例后的回调,默认是将beanName从 prototypesCurrentlyInCreation 移除
						afterPrototypeCreation(beanName);
					}
					//从 beanInstannce 中获取公开的Bean对象,主要处理beanInstance是FactoryBean对象的情况,如果不是
					// FactoryBean会直接 返回beanInstance实例
					bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				}

				else {
					//获取mbd指定的Scope
					String scopeName = mbd.getScope();
					//从scopes中获取scopeName对于的Scope对象
					final Scope scope = this.scopes.get(scopeName);
					//如果scope为null
					if (scope == null) {
						//抛出非法状态异常:没有名为'scopeName'的scope注册
						throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
					}
					try {
						//从scope中获取beanName对应的实例对象
						Object scopedInstance = scope.get(beanName, () -> {
							//创建ProtoPype对象前的准备工作,默认实现 将beanName添加到 prototypesCurrentlyInCreation 中
							beforePrototypeCreation(beanName);
							try {
								//为mbd(和参数)创建一个bean实例
								return createBean(beanName, mbd, args);
							}
							finally {
								//创建完prototype实例后的回调,默认是将beanName从 prototypesCurrentlyInCreation 移除
								afterPrototypeCreation(beanName);
							}
						});
						//从 beanInstannce 中获取公开的Bean对象,主要处理beanInstance是FactoryBean对象的情况,如果不是
						// 	FactoryBean会直接 返回beanInstance实例
						bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
					}
					catch (IllegalStateException ex) {//捕捉非法状态异常
						//抛出Bean创建异常:作用域 'scopeName' 对于当前线程是不活动的;如果您打算从单个实例引用它,请考虑为此
						// beanDefinition一个作用域代理
						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对象抛出的Bean异常
				//在Bean创建失败后,对缓存的元数据执行适当的清理
				cleanupAfterBeanCreationFailure(beanName);
				//重新抛出ex
				throw ex;
			}
		}

		// Check if required type matches the type of the actual bean instance.
		// 检查 requiredType 是否与实际Bean实例的类型匹配
		// 如果requiredType不为null && bean不是requiredType的实例
		if (requiredType != null && !requiredType.isInstance(bean)) {
			try {
				//获取此BeanFactory使用的类型转换器,将bean转换为requiredType
				T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
				//如果 convertedBean 为 null
				if (convertedBean == null) {
					//抛出 Bean不是必要类型的异常
					throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
				}
				//返回convertedBean
				return convertedBean;
			}
			catch (TypeMismatchException ex) {//捕捉 尝试设置Bean属性时,在类型不匹配上引发异常
				//如果当前日志级别是追踪级别
				if (logger.isTraceEnabled()) {
					//打印追踪日志:转换bean'name'到要求的类型失败
					logger.trace("Failed to convert bean '" + name + "' to required type '" +
							ClassUtils.getQualifiedName(requiredType) + "'", ex);
				}
				//抛出 Bean不是必要类型的异常
				throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
			}
		}
		//将bean返回出去
		return (T) bean;
	}

transformedBeanName(name)

/**
	 * Return the bean name, stripping out the factory dereference prefix if necessary,
	 * and resolving aliases to canonical names.
	 * <p>返回Bean名称,如果必要,去除工厂取消前缀,并将别名解析为规范名称</p>
	 * @param name the user-specified name - 用户指定的名称
	 * @return the transformed bean name - 转换后的bena名称
	 */
	protected String transformedBeanName(String name) {
		//去除开头的'&'字符,返回剩余的字符串得到转换后的Bean名称,然后通过递归形式在
		// aliasMap【别名映射到规范名称集合】中得到最终的规范名称
		return canonicalName(BeanFactoryUtils.transformedBeanName(name));
	}

去除name开头的’&‘字符,获取name最终的规范名称【最终别名或者是全类名】:去除开头的’&'字符,返回剩余的字符串得到转换后的Bean名称, 然后通过递归形式在 aliasMap【别名映射到规范名称集合】中得到最终的规范名称

BeanFactoryUtils.transformedBeanName(name)

去除开头的’&'字符,返回剩余的字符串作为转换后的Bean名称【可能是全类名】

  1. 如果name为null,抛出异常
  2. 如果name不是以’&'开头,就直接返回
  3. 从具有FactoryBean前缀的名称缓存到剥离的名称的ConcurrentHashMap【transformedBenaNameCache】 中获取bean名对应的转换后的名称并返回
  4. 获取不到时就构建一个:从name的开头位置去掉’&’,再重新检查是否还是以’&‘开头,是的话就再截直到没有出现’&'开头为止。所得到 的字符串就是Bean名称【可能是全类名】
/**
	 * Return the actual bean name, stripping out the factory dereference
	 * prefix (if any, also stripping repeated factory prefixes if found).
	 * <p>返回实际的bean名称,删除工厂取消引用前缀(如果有的话,还删除重复的
	 * 工厂前缀(如果找到))</p>
	 * <p>用于获取以'&'开头的name对应的bean名,如果bean不是'&'开头,就直接返回该name</p>
	 * @param name the name of the bean - bean名
	 * @return the transformed name - 转换后的名称
	 * @see BeanFactory#FACTORY_BEAN_PREFIX
	 */
	public static String transformedBeanName(String name) {
		//如果bean名为null,抛出异常
		Assert.notNull(name, "'name' must not be null");
		//如果bean名不是以'&'开头,就直接返回
		if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
			return name;
		}
		//从transformedBenaNameCache中获取bean名对应的转换后的名称
		return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
			//从beanName的开头位置去掉'&',并重新赋值给beanName,再重新检查是还是以'&'开头,是的话就再截
			// 知道开头不是以'&'开头后,加入到transformedBeanNameCache中
			do {
				beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
			}
			while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
			return beanName;
		});
	}

canonicalName(name)

canonicalName(name) 是 org.springframework.core.SimpleAliasRegistry#canonicalName(String) 方法: 获取name的最终别名或者是全类名,通过递归形式在aliasMap【别名映射到规范名称集合】中得到最终的规范名称

/**
	 * Determine the raw name, resolving aliases to canonical names.
	 * <p>确定原始名称,将别名解析为规范名称</p>
	 * @param name the user-specified name - 用户指定的名称
	 * @return the transformed name - 转换后的名称
	 */
	public String canonicalName(String name) {
		//规范名称初始化化传入的name
		String canonicalName = name;
		// Handle aliasing... 处理别名
		String resolvedName;
		//从下面代码可以看出,aliasMap的存储形式是:
		// C是真正的规范名称,A,B都是别名:
		// 		aliasMap={key:A,val:B;key:B,val:C},通过A拿到B,再通过B拿到C
		do {
			//通过 规范名称 从 aliasMap中获取解析后的名称
			resolvedName = this.aliasMap.get(canonicalName);
			//如果找到了解析后的名称
			if (resolvedName != null) {
				//规范名称重新赋值为解析后名称
				canonicalName = resolvedName;
			}
		}
		//只要找到了解析后的名称
		while (resolvedName != null);
		return canonicalName;
	}

aliasMap 是 org.springframework.core.SimpleAliasRegistry 的成员变量

/**
	 * Map from alias to canonical name.
	 * <p>从别名映射到规范名称</p>
	 * */
	private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);

整个Map数据结构应该要抽象理解为一个二维数组,因为在检索别名的时候,是可以通过别名查别名的
举个栗子:

  1. A是B的别名,C是B的别名,存放到aliasMap中的数据结构就是:[{key=B,val=A},{key=C,val=B}]
  2. 当要获取A的所有别名[B,C]时:先获取A的Key->B,则通过递归形式获取B的key->C

getSingleton

获取以beanName注册的(原始)单例对象,该代码的所呈现的三级缓存其实是为解决循环依赖问题,有关循环依赖的资料建议还是看视频,比较好理解,这里不作概述。

  1. 从单例对象的高速缓存【singletonObjects】中获取beanName的单例对象,赋值为【singletonObject】
  2. 如果单例对象没成功获取,并且 baneName 是正在被创建:
    1. 同步,以singletonObjects作为锁
    2. 从早期单例对象的高速缓存【earlySingletonObjects】中获取bean对象,赋值为【singletonObject】
    3. 如果singletonObject为null,且允许创建早期引用:
      1. 从单例工厂的缓存【singletonFactories】中获取beanName的单例工厂对象,赋值给【singletonFactory】
      2. 如果singletonFactory不为null:
        1. 从singletonFactory中获取该beanName的单例对象,作为singletonObject
        2. 添加beanName和singletonObject到 早期单例对象高速缓存【earlySingletonObjects】中
        3. 从单例对象工厂缓存【singletonFactories】中移除beanName的单例对象工
  3. 返回singletonObject
/**
	 * 获取beanName的单例对象,并允许创建早期引用
	 * @param beanName the name of the bean to look for - 要寻找的bean名
	 * @see #getSingleton(String, boolean)
	 */
	@Override
	@Nullable
	public Object getSingleton(String beanName) {
		//获取beanName的单例对象,并允许创建早期引用
		return getSingleton(beanName, true);
	}

	/**
	 * Return the (raw) singleton object registered under the given name.
	 * <p>返回以给定名称注册的(原始)单例对象,如果单例对象没有找到,并且beanName存在
	 * 正在创建的Set集合中</p>
	 * <p>Checks already instantiated singletons and also allows for an early
	 * reference to a currently created singleton (resolving a circular reference).
	 * <p>检查已经实例化的单例,并且还允许对当前的单例的早期引用(解析循环引用)</p>
	 * @param beanName the name of the bean to look for - 要寻找的bean名
	 * @param allowEarlyReference whether early references should be created or not
	 *                            - 是否应创建早期引用
	 * @return the registered singleton object, or {@code null} if none found
	 * 		- 注册的单例对象;如果找不到,则为{@code null}
	 */
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		//从单例对象的高速缓存中获取beanName的单例对象
		Object singletonObject = this.singletonObjects.get(beanName);
		//如果单例对象没有找到,并且 baneName 是正在被创建
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			//同步,以singletonObjects作为锁
			synchronized (this.singletonObjects) {
				//从早期单例对象的高速缓存中获取bean对象
				singletonObject = this.earlySingletonObjects.get(beanName);
				//如果获取不了bean的单例对象,且允许创建早期引用
				if (singletonObject == null && allowEarlyReference) {
					//从单例工厂的缓存中获取beanName的单例工厂对象
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					//如果beanName的单例工厂对象找到了
					if (singletonFactory != null) {
						//从beanName的单例工厂对象中获取该beanName的单例对象
						singletonObject = singletonFactory.getObject();
						//下面的操作主要是为了防止beanName对应的对象重复构建
						//添加beanName和其对应的beanName单例对象到 早期单例对象高速缓存中
						this.earlySingletonObjects.put(beanName, singletonObject);
						//从单例对象工厂缓存中移除beanName的单例对象工厂
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		//返回beanName对应的单例对象
		return singletonObject;
	}

singletonObjects, earlySingletonObjects, singletonFactories 是 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 的成员变量

/**
	 * Cache of singleton objects: bean name to bean instance.
	 * <p>单例对象的高速缓存:beam名称-bean实例,所有bean对象最终都会放到对象中</p>
	 * */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/**
	 * Cache of singleton factories: bean name to ObjectFactory.
	 * <p>单例工厂的缓存:bean名称 - ObjectFactory </p>
	 * */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/**
	 * Cache of early singleton objects: bean name to bean instance.
	 * <p>早期单例对象的高速缓存:bean名称 - bean实例</p>
	 * <p>当从singletonFactories中获取到对应对象后,就会放到这个缓存中</p>
	 * */
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

getObjectForBeanInstance(sharedInstance, name, beanName, null);

从 beanInstannce 中获取公开的Bean对象,主要处理beanInstance是FactoryBean对象的情况,如果不是FactoryBean会直接 返回beanInstance实例

Spring 5 AbstractBeanFactory – getObjectForBeanInstance 源码解析

isPrototypeCurrentlyInCreation(beanName)

返回指定的原型bean是否当前正在创建中(在当前线程内)


/**
	 * Names of beans that are currently in creation.
	 * <p>当前正在创建的bean名称</p>
	 * */
	private final ThreadLocal<Object> prototypesCurrentlyInCreation =
			new NamedThreadLocal<>("Prototype beans currently in creation");


/**
	 * Return whether the specified prototype bean is currently in creation
	 * (within the current thread).
	 * <p>返回指定的原型bean是否当前正在创建中(在当前线程内)</p>
	 * @param beanName the name of the bean - bean名
	 */
	protected boolean isPrototypeCurrentlyInCreation(String beanName) {
		//获取当前正在创建的bean名称【线程本地】
		Object curVal = this.prototypesCurrentlyInCreation.get();
		//如果当前正在创建的bean名称不为null,且 (当前正在创建的bean名称等于beanName 或者
		//	 当前正在创建的bean名称是Set集合,并包含该beanName)
		//就返回true,表示在当前线程内,beanName当前正在创建中。
		return (curVal != null &&
				(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
	}

篇幅过长,请看下一篇

Spring 5 AbstractBeanFactory – getBean() 源码分析(二)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值