Spring源码之依赖注入

1.前言

        依赖注入(Dependency Injection,简称DI)是控制反转(Inversion of Control,简称IoC)一种具体实现技术,作为Spring核心功能,解耦了对象的创建与使用,即我们可以不用显示的创建对象的依赖,而是通过外部容器在运行时完成对象的注入。本文基于基于Spring5.3.37版本对其在Spring中的具体实现做分析。

控制反转(Inversion of Control,简称IoC)是一种设计原则,其核心思想是将程序的控制流的控制权从代码本身转移到一个容器或框架上。在传统的编程模式下,对象直接创建和管理其依赖对象,而在IoC模式下,这种控制权被反转,由一个外部实体(通常是框架或容器)负责创建和管理对象及其依赖关系。

2.使用示例

        Spring支持通过xml配置文件和注解两种方式进行依赖注入,其中注解方式支持@Autowired、@Resource、@Inject、@Value注解,以下为常用的@Autowired注解示例。

1.构造器注入

@Component
public class AutowiredClass {
    private DependenceClass dependedObj;
    
    @Autowired
    public AutowiredClass(DependenceClass dependedObj) {
        this.dependedObj = dependedObj;
    }
}

2.属性注入

@Component
public class AutowiredClass {
    private DependenceClass dependedObj;
    
    @Autowired
    public void setDependedObj(DependenceClass dependedObj) {
        this.dependedObj = dependedObj;
    }
}

3.字段注入

@Component
public class AutowiredClass {
    @Autowired
    private DependenceClass dependedObj;
}

4.方法参数注入

@Component
public class AutowiredClass {
    private DependenceClass dependedObj;
    
    public void setDependedObj(@Autowired DependenceClass dependedObj) {
        this.dependedObj = dependedObj;
    }
}

3.核心流程

        在Spring中,依赖注入(属性填充)对应的核心步骤核心步骤为寻找注入点、进行值注入两步,其流程图如下所示:

以上步骤在AutowiredAnnotationBeanPostProcessor#postProcessProperties()、CommonAnnotationBeanPostProcessor#postProcessProperties()完成的,其中

  • AutowiredAnnotationBeanPostProcessor#postProcessProperties()负责@Autowired、@Value、@Inject注解的注入点解析和值注入
  • CommonAnnotationBeanPostProcessor#postProcessProperties()负责@Resource注解的注入点解析和值注入

接下来将从源码角度进行分析。

4.源码分析

        在Spring中,依赖注入(属性填充)核心方法为AbstractAutowireCapableBeanFactory#populateBean(),其执行流程如下:

1.进行实例化后处理,对应调用方法为:InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation();

2.获取AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE中对应的注入信息(注入点、注入值),这两个属性主要用在@Bean注解的autowire属性(已废弃)、XML定义的Bean中autowire属性;

3.进行@Autowired、@Value、@Inject、@Resource注解的注入点的解析及值注入,对应调用方法为:InstantiationAwareBeanPostProcessor#postProcessProperties(),其实现为AutowiredAnnotationBeanPostProcessor#postProcessProperties()、CommonAnnotationBeanPostProcessor#postProcessProperties();

4.执行自定义的属性值处理方法,对应方法为:InstantiationAwareBeanPostProcessor#postProcessPropertyValues();

5.对AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE对应注入信息进行值注入。

其核心源码如下所示:

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    // 省略部分代码...

    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
       for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
           // 执行实例化后操作
          if (!bp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
             return;
          }
       }
    }

    PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

    int resolvedAutowireMode = mbd.getResolvedAutowireMode();
    // 对AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE对应的注入点信息进行解析
    if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
       MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
       // Add property values based on autowire by name if applicable.
       if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
          autowireByName(beanName, mbd, bw, newPvs);
       }
       // Add property values based on autowire by type if applicable.
       if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
          autowireByType(beanName, mbd, bw, newPvs);
       }
       pvs = newPvs;
    }

    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

    PropertyDescriptor[] filteredPds = null;
    if (hasInstAwareBpps) {
       if (pvs == null) {
          pvs = mbd.getPropertyValues();
       }
       for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
           // 进行@Autowired、@Value、@Inject@Resource注解的注入点的解析及值注入
          PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
          if (pvsToUse == null) {
             if (filteredPds == null) {
                filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
             }
             // 执行自定义的属性值处理方法
             pvsToUse = bp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
             if (pvsToUse == null) {
                return;
             }
          }
          pvs = pvsToUse;
       }
    }
    // 省略部分代码...

    if (pvs != null) {
        // 对AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE对应注入信息进行值注入
       applyPropertyValues(beanName, mbd, bw, pvs);
    }
}

接下来对注解标记的注入点解析核心方法进行分析。

4.1 AutowiredAnnotationBeanPostProcessor#postProcessProperties()

        本方法主要干了两件事情:

1.寻找@Autowired、@Value、@Inject标记的注入点;

2.进行值注入。

其核心源码如下:

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 寻找@Autowired、@Value、@Inject标记的注入点
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 进行值注入
       metadata.inject(bean, beanName, pvs);
    }
    // 省略部分代码...
    return pvs;
}

4.1.1 寻找注入点

        此步骤对应方法为AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata(),内部对应注入点查找的方法为AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata(),其内部核心步骤为:

1.寻找类属性上的注入点集合

①.属性上有注解@Autowired、@Value、@Inject、@Resource

②.static修饰的属性排除

③.根据注解的required判断是否必填

2.寻找类方法的注入点集合

①.查找桥接方法,若不存在则是方法自身

②.方法上有注解@Autowired、@Value、@Inject、@Resource

③.static修饰的方法排除

④.无参的方法排除

⑤.根据注解的required判断是否必填

3.获取当前类对应的父类,继续寻找父类上的注入点

其核心源码如下:

private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
    if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
       return InjectionMetadata.EMPTY;
    }

    final List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
       final List<InjectionMetadata.InjectedElement> fieldElements = new ArrayList<>();
       // 寻找为注入点的属性
       ReflectionUtils.doWithLocalFields(targetClass, field -> {
          MergedAnnotation<?> ann = findAutowiredAnnotation(field);
          if (ann != null) {
              // 如果是static修饰的属性,则忽略
             if (Modifier.isStatic(field.getModifiers())) {
                // 省略部分代码...
                return;
             }
             // 根据注解的required判断是否必填
             boolean required = determineRequiredStatus(ann);
             fieldElements.add(new AutowiredFieldElement(field, required));
          }
       });

       final List<InjectionMetadata.InjectedElement> methodElements = new ArrayList<>();
       // 寻找为注入点的方法
       ReflectionUtils.doWithLocalMethods(targetClass, method -> {
          Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
          if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
             return;
          }
          MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
          if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
              // 如果是static修饰的方法,则忽略
             if (Modifier.isStatic(method.getModifiers())) {
                // 省略部分代码...
                return;
             }
             // 无参的方法排除
             if (method.getParameterCount() == 0) {
                // 省略部分代码...
             }
             // 根据注解的required判断是否必填
             boolean required = determineRequiredStatus(ann);
             PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
             methodElements.add(new AutowiredMethodElement(method, required, pd));
          }
       });

        // 将寻找到的注入点添加到集合中,其中由于是往集合头部追加,因此将先执行属性注入,后执行方法注入
       elements.addAll(0, sortMethodElements(methodElements, targetClass));
       elements.addAll(0, fieldElements);
       // 获取父类
       targetClass = targetClass.getSuperclass();
    }
    // 存在父类且父类不为Object类,继续寻找注入点
    while (targetClass != null && targetClass != Object.class);

    return InjectionMetadata.forElements(elements, clazz);
}

4.1.2 进行值注入

        此步骤对应的方法为InjectionMetadata#inject(),此方法通过遍历前面步骤获取的注入点,执行其注入方法进行值注入。

其核心源码如下:

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Collection<InjectedElement> checkedElements = this.checkedElements;
    Collection<InjectedElement> elementsToIterate =
          (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
       for (InjectedElement element : elementsToIterate) {
           // 执行注入方法
          element.inject(target, beanName, pvs);
       }
    }
}

其核心为InjectedElement#inject(),AutowiredAnnotationBeanPostProcessor存在两个内部类AutowiredFieldElement、AutowiredMethodElement继承自InjectedElement,对其做了扩展,此处实际执行的是AutowiredFieldElement#inject()、AutowiredMethodElement#inject()方法。

4.1.2.1 AutowiredFieldElement#inject()

此方法对应的核心步骤为:

1.解析注入点属性值

2.使用反射进行属性值设置

其核心源码如下:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Field field = (Field) this.member;
    Object value;
    // 已缓存则从缓存里取值
    if (this.cached) {
       try {
          value = resolveCachedArgument(beanName, this.cachedFieldValue);
       }
       catch (BeansException ex) {
          // 省略部分代码...
          // 解析获得注入值
          value = resolveFieldValue(field, bean, beanName);
       }
    }
    else {
       // 解析获得注入值        
       value = resolveFieldValue(field, bean, beanName);
    }
    if (value != null) {
       ReflectionUtils.makeAccessible(field);
       // 利用反射进行值注入
       field.set(bean, value);
    }
}

对应核心方法为resolveFieldValue(),其核心源码如下:

private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {
       // 省略部分代码...
       try {
           // 解析获得依赖的值
          value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
       }
       // 省略部分代码...
       synchronized (this) {
          if (!this.cached) {
             if (value != null || this.required) {
                Object cachedFieldValue = desc;
                // 设置依赖关系
                registerDependentBeans(beanName, autowiredBeanNames);
                if (value != null && autowiredBeanNames.size() == 1) {
                   String autowiredBeanName = autowiredBeanNames.iterator().next();
                   if (beanFactory.containsBean(autowiredBeanName) &&
                         beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                      cachedFieldValue = new ShortcutDependencyDescriptor(desc, autowiredBeanName);
                   }
                }
                // 将解析获得的注入值添加到缓存属性中
                this.cachedFieldValue = cachedFieldValue;
                this.cached = true;
             }
             else {
                this.cachedFieldValue = null;
                // cached flag remains false
             }
          }
       }
       return value;
    }
}

4.1.2.2 AutowiredMethodElement#inject()

        此方法对应的核心步骤为:

1.解析方法参数值

2.使用反射执行注入方法

其核心源码如下:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    if (checkPropertySkipping(pvs)) {
       return;
    }
    Method method = (Method) this.member;
    Object[] arguments;
    // 若存在缓存则走缓存
    if (this.cached) {
       try {
           // 获取缓存中的注入参数值
          arguments = resolveCachedArguments(beanName, this.cachedMethodArguments);
       }
       catch (BeansException ex) {
          // Unexpected target bean mismatch for cached argument -> re-resolve
          this.cached = false;
          logger.debug("Failed to resolve cached argument", ex);
          // 解析获得注入参数值
          arguments = resolveMethodArguments(method, bean, beanName);
       }
    }
    else {
        // 解析获得注入参数值        
       arguments = resolveMethodArguments(method, bean, beanName);
    }
    if (arguments != null) {
       try {
          ReflectionUtils.makeAccessible(method);
          // 使用反射调用方法进行值注入
          method.invoke(bean, arguments);
       }
       catch (InvocationTargetException ex) {
          throw ex.getTargetException();
       }
    }
}

对应核心方法为resolveMethodArguments(),其核心源码如下:

private Object[] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) {
       int argumentCount = method.getParameterCount();
       Object[] arguments = new Object[argumentCount];
       DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];
       Set<String> autowiredBeanNames = new LinkedHashSet<>(argumentCount * 2);
       Assert.state(beanFactory != null, "No BeanFactory available");
       TypeConverter typeConverter = beanFactory.getTypeConverter();
       // 循环方法参数,获得对应参数值
       for (int i = 0; i < arguments.length; i++) {
          MethodParameter methodParam = new MethodParameter(method, i);
          DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
          currDesc.setContainingClass(bean.getClass());
          descriptors[i] = currDesc;
          try {
              // 解析获得依赖的值
             Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeanNames, typeConverter);
             if (arg == null && !this.required) {
                arguments = null;
                break;
             }
             arguments[i] = arg;
          }
          catch (BeansException ex) {
             throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
          }
       }
       synchronized (this) {
          if (!this.cached) {
             if (arguments != null) {
                DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, argumentCount);
                // 设置依赖关系
                registerDependentBeans(beanName, autowiredBeanNames);
                
                // 省略部分缓存设置相关的代码...
          }
       }
       return arguments;
    }
}

4.1.2.3 DefaultListableBeanFactory#resolveDependency()

        在AutowiredFieldElement#inject()、AutowiredMethodElement#inject()方法中均调用了AutowireCapableBeanFactory#resolveDependency()获取依赖的值,该方法对应实现为DefaultListableBeanFactory#resolveDependency(),此方法用于获取给定Bean名称对应的Bean。其内部核心方法为DefaultListableBeanFactory#doResolveDependency()。

其核心步骤如下:

1.如果设置了默认值,则获取设置的默认值,默认情况下是Null;

2.如果参数类型是集合类型,获取集合泛型中指定的参数类型,根据参数类型获取对应的Bean集合,其中针对Map类型的注入点属性,如果Key的类型不是String,则不处理;

3.不满足以上两种情况,则根据Bean类型获取到所有候选者;

4. 如果注入候选者有多个,则需确定唯一的候选者

①首选获取@Primary注解标记的Bean

②如果不存在@Primary注解标记的Bean,则根据Bean上@Priority设置的值进行排序,优先级最小的为最终候选者,其中相同类型的Bean优先级的值不能设置成相同的

③如果以上情况均不满足,则将Bean名称和参数名一样的Bean作为最终候选者

5.取得唯一的候选者,如果类型是Class类型,则进行实例化;

6.如果是NullBean,而当前注入点要求必填则抛出异常则抛出异常,否则返回此Bean。

其核心源码如下:

public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
       @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    try {
       Object shortcut = descriptor.resolveShortcut(this);
       if (shortcut != null) {
          return shortcut;
       }

       Class<?> type = descriptor.getDependencyType();
       // 获取设置的默认值,默认情况下为Null
       Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
       if (value != null) {
          if (value instanceof String) {
             String strVal = resolveEmbeddedValue((String) value);
             BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                   getMergedBeanDefinition(beanName) : null);
             value = evaluateBeanDefinitionString(strVal, bd);
          }
          TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
          try {
             return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
          }
          catch (UnsupportedOperationException ex) {
             // A custom TypeConverter which does not support TypeDescriptor resolution...
             return (descriptor.getField() != null ?
                   converter.convertIfNecessary(value, type, descriptor.getField()) :
                   converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
          }
       }
       // 如果参数类型是集合类型,获取集合泛型中指定的参数类型,根据参数类型获取对应的Bean集合
       // 其中针对Map类型的注入点属性,如果Key的类型不是String,则不处理
       Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
       if (multipleBeans != null) {
          return multipleBeans;
       }
       // 查找注入候选者集合
       Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
       if (matchingBeans.isEmpty()) {
          if (isRequired(descriptor)) {
             raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
          }
          return null;
       }

       String autowiredBeanName;
       Object instanceCandidate;

       if (matchingBeans.size() > 1) {
           // 如果注入候选者有多个,则需确定唯一的候选者
           // 首选获取@Primary注解标记的Bean
           // 如果不存在@Primary注解标记的Bean,则根据Bean上@Priority设置的值进行排序,优先级最小的为最终候选者,其中相同类型的Bean优先级的值不能设置成相同的
           // 如果以上情况均不满足,则将Bean名称和参数名一样的Bean作为最终候选者
          autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
          if (autowiredBeanName == null) {
             if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
                return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
             }
             else {
                // In case of an optional Collection/Map, silently ignore a non-unique case:
                // possibly it was meant to be an empty collection of multiple regular beans
                // (before 4.3 in particular when we didn't even look for collection beans).
                return null;
             }
          }
          instanceCandidate = matchingBeans.get(autowiredBeanName);
       }
       else {
          // We have exactly one match.
          Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
          autowiredBeanName = entry.getKey();
          instanceCandidate = entry.getValue();
       }

       if (autowiredBeanNames != null) {
          autowiredBeanNames.add(autowiredBeanName);
       }
       // 如果取得的候选者是Clas类型,即未实例化,则进行实例化
       if (instanceCandidate instanceof Class) {
          instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
       }
       Object result = instanceCandidate;
       // 如果是一个Null,Spring会给包装成NullBean,此处如果找到的候选者是个Null的Bean,而当前注入点要求必填则抛出异常
       if (result instanceof NullBean) {
          if (isRequired(descriptor)) {
             raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
          }
          result = null;
       }
       if (!ClassUtils.isAssignableValue(type, result)) {
          throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
       }
       return result;
    }
    finally {
       ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    }
}

4.1.2.4 DefaultListableBeanFactory#findAutowireCandidates()

        此方法用于获取给定类型的注入候选者,其核心步骤如下:

1.获取当前Context中与给定类型匹配的所有Bean名称;

2.检查已解析的依赖项,将与给定类型匹配的Bean添加到候选集合中;

3.将步骤1中得到的Bean名称取得对应Bean,将符合条件的Bean添加到候选集合中;

4.如果候选者集合为空,则尝试使用回退匹配策略,将不是引用的自身且符合条件的Bean添加到候选者集合中;

5.如果使用回退匹配策略仍未获取到符合条件的Bean,且要注入的是自身且符合条件,则将其添加到候选者集合中

4.1.2.5 DefaultListableBeanFactory#isAutowireCandidate()

        此方法用于判断传入的Bean名称对应Bean是否为候选者。其内部调用的核心方法为DefaultListableBeanFactory#isAutowireCandidate(String beanName, RootBeanDefinition mbd,DependencyDescriptor descriptor, AutowireCandidateResolver resolver),其通过给定的Bean名称取得对应的BeanDefinition,传入AutowireCandidateResolver#isAutowireCandidate()方法判断是否为候选者。

AutowireCandidateResolver#isAutowireCandidate()的实现者有以下三个:

  • QualifierAnnotationAutowireCandidateResolver:用于解析@Qualifier注解,判断是否与该注解指定的Bean一致
  • GenericTypeAwareAutowireCandidateResolver:用于解析泛型对应实际的实现的类型,判断是否为候选者
  • SimpleAutowireCandidateResolver:通过BeanDefinition的属性autowireCandidate判断是否为候选者,为true的为候选者

对应UML类图如图所示:

这三个实现类是继承的关系,在执行isAutowireCandidate()方法时,会先调用父类的方法判断是否满足条件、如果不满足,则直接返回false,即不满足,只有条件均满足的情况下才是候选者。

5.总结

        在Spring中,依赖注入的通过寻找注入点(@Autowired、@Resource、@Inject、@Value注解标记的属性、方法),其中AutowiredAnnotationBeanPostProcessor#postProcessProperties()负责@Autowired、@Value、@Inject注解的注入点解析和值注入,CommonAnnotationBeanPostProcessor#postProcessProperties()负责@Resource注解的注入点解析和值注入;然后使用DefaultListableBeanFactory#resolveDependency()方法获取注入值,通过反射的方式将值进行注入。

        在获取注入值的过程中,先通过DefaultListableBeanFactory#findAutowireCandidates()找到所有候选者Bean;通过递进的三个条件:是否存在@Primary注解、@Priority值最小、属性/参数名称与候选者Bean名称一致获得用于注入的Bean,这三个条件满足其中一个即可。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值