Spring之推断构造方法

1.前言

在之前的博文《Spring之BeanDefinition》讲述了BeanDefinition的用法,以及如何定制化属性,使bean按照我们期望的方式实例化。

通过总结,实例化方式大概分为三种:

  • supplier
  • factoryBeanName + factoryMethodName (beanClass + factoryMethodName)
  • 构造器

今天我们主要讲解Spring是如何推断出一个唯一的构造器方法,来实例化对象

2.源码分析

相关源码在AbstractAutowireCapableBeanFactory#createBeanInstance

分成三个优先级

  1. 第一优先级通过supplier实例化bean对象
  2. 第二优先级通过factoryMethod实例化bean对象
  3. 第三优先级才是我们今天讨论的主角,通过推断构造方法实例化bean对象

2.1 determineConstructorsFromBeanPostProcessors方法解析

这个方法主要循环执行BeanPostProcessor的determineCandidateConstructors方法,我们需要关注其子类实现AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors

相关源码注释

删除了处理@Lookup注解的相关源码,有兴趣的可以看我之前的博文 《Spring之@Lookup

@Override
    @Nullable
    public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) throws BeanCreationException {
        // Quick check on the concurrent map first, with minimal locking.
        Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
        if (candidateConstructors == null) {
            // Fully synchronized resolution now...
            synchronized (this.candidateConstructorsCache) {
                candidateConstructors = this.candidateConstructorsCache.get(beanClass);
                if (candidateConstructors == null) {
                    // 获取所有构造方法
                    Constructor<?>[] rawCandidates;
                    try {
                        rawCandidates = beanClass.getDeclaredConstructors();
                    } catch (Throwable ex) {
                        throw new BeanCreationException(beanName,
                                "Resolution of declared constructors on bean Class [" + beanClass.getName() +
                                        "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
                    }
                    List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                    Constructor<?> requiredConstructor = null;
                    Constructor<?> defaultConstructor = null;

                    // 查看是不是Kotlin相关的类 查找primaryConstructor
                    Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);

                    // 记录有多少个非合成的构造方法,主要针对两种情况
                    // case1:只有1个非合成的构造方法(primaryConstructor)
                    // case2:存在2个非合成的构造方法(primaryConstructor + defaultConstructor)
                    int nonSyntheticConstructors = 0;

                    for (Constructor<?> candidate : rawCandidates) {
                        // 判断是不是非合成的构造方法,不是合成的构造方法参数nonSyntheticConstructors自增1
                        if (!candidate.isSynthetic()) {
                            nonSyntheticConstructors++;
                        }

                        // Kotlin是不是存在合成构造方法的概念?
                        // 如果存在,针对上面的case1已经可以找到候选的构造方法了,所以不倾向使用这些合成的构造方法
                        else if (primaryConstructor != null) {
                            continue;
                        }

                        // 查看构造器是否含有@Autowired注解
                        MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
                        if (ann == null) {
                            // 如果构造器没有@Autowired注解,则查看类名是否包含$$字符
                            // 如果存在此特殊字符,表明此类是cglib动态代理产生的子类
                            // 这时候需要从父类的构造器查看是否含有@Autowired注解
                            Class<?> userClass = ClassUtils.getUserClass(beanClass);
                            if (userClass != beanClass) {
                                try {
                                    Constructor<?> superCtor =
                                            userClass.getDeclaredConstructor(candidate.getParameterTypes());
                                    ann = findAutowiredAnnotation(superCtor);
                                } catch (NoSuchMethodException ex) {
                                    // Simply proceed, no equivalent superclass constructor found...
                                }
                            }
                        }

                        // 能进入此条件表明查询到包含@Autowired注解的构造器
                        if (ann != null) {
                            // 1.如果存在一个@Autowired(required = true)注解标注的构造方法
                            // 则不能再出现@Autowired注解标注的构造方法(无论required等于true或false都报错)
                            // 2.可以有多个@Autowired(required = false)注解标注的构造方法

                            // 表明已经解析了一个@Autowired(required = true)注解标注的构造方法
                            // 不能再出现@Autowired注解标注的构造方法
                            // TODO 1
                            if (requiredConstructor != null) {
                                throw new BeanCreationException(beanName,
                                        "Invalid autowire-marked constructor: " + candidate +
                                                ". Found constructor with 'required' Autowired annotation already: " +
                                                requiredConstructor);
                            }

                            boolean required = determineRequiredStatus(ann);
                            if (required) {
                                // 如果条件成立,表明至少解析了一个@Autowired(required = false)注解标注的构造方法
                                // PS:如果同时存在@Autowired(required = true)注解标注的构造方法和@Autowired(required = false)注解标注的构造方法
                                // 如果先解析@Autowired(required = true)注解标注的构造方法,则在TODO 1处抛出异常
                                // 如果先解析@Autowired(required = false)注解标注的构造方法,则在TODO 2处抛出异常
                                // TODO 2
                                if (!candidates.isEmpty()) {
                                    throw new BeanCreationException(beanName,
                                            "Invalid autowire-marked constructors: " + candidates +
                                                    ". Found constructor with 'required' Autowired annotation: " +
                                                    candidate);
                                }

                                // 表明是第一次解析@Autowired(required = true)注解标注的构造方法
                                requiredConstructor = candidate;
                            }
                            // 这个候选者可以是第一次解析@Autowired(required = true)注解标注的构造方法
                            // 或者前面解析了n个@Autowired(required = false)注解标注的构造方法,这次解析的同样是
                            // @Autowired(required = false)注解标注的构造方法
                            candidates.add(candidate);
                        }
                        // 如果没有@Autowired注解标注的构造方法,则将无参构造方法设为defaultConstructor
                        else if (candidate.getParameterCount() == 0) {
                            defaultConstructor = candidate;
                        }
                    }

                    // 第一优先级:存在候选构造方法
                    if (!candidates.isEmpty()) {
                        // Add default constructor to list of optional constructors, as fallback.
                        if (requiredConstructor == null) {
                            // 解析了n个@Autowired(required = false)注解标注的构造方法
                            // 并且将默认构造方法也当成候选构造方法返回
                            if (defaultConstructor != null) {
                                candidates.add(defaultConstructor);
                            }

                            // 这里可能存在两种情况:
                            // 第一种情况:只有一个无参构造方法,并且标记了@Autowired(required = false)注解
                            // 第二种情况:只有一个有参构造方法,并且标记了@Autowired(required = false)注解
                            else if (candidates.size() == 1 && logger.isInfoEnabled()) {
                                logger.info("Inconsistent constructor declaration on bean with name '" + beanName +
                                        "': single autowire-marked constructor flagged as optional - " +
                                        "this constructor is effectively required since there is no " +
                                        "default constructor to fall back to: " + candidates.get(0));
                            }
                        }
                        candidateConstructors = candidates.toArray(new Constructor<?>[0]);
                    }
                    // 第二优先级:只有一个构造方法 且参数大于0
                    // PS:不存在@Autowired注解标注的构造方法,不存在无参构造方法
                    else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
                        candidateConstructors = new Constructor<?>[]{rawCandidates[0]};
                    }
                    // 第三优先级:如果有两个非合成的构造方法,且一个对应primaryConstructor 一个对应defaultConstructor
                    // 主要针对Kotlin相关的类
                    else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
                            defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
                        candidateConstructors = new Constructor<?>[]{primaryConstructor, defaultConstructor};
                    }
                    // 第四优先级:有且只有一个非合成的构造方法,且对应primaryConstructor
                    // 主要针对Kotlin相关的类
                    else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
                        candidateConstructors = new Constructor<?>[]{primaryConstructor};
                    }
                    // 返回兜底的空构造方法数组
                    else {
                        candidateConstructors = new Constructor<?>[0];
                    }
                    this.candidateConstructorsCache.put(beanClass, candidateConstructors);
                }
            }
        }
        return (candidateConstructors.length > 0 ? candidateConstructors : null);
    }
determineConstructorsFromBeanPostProcessors方法总结
1.可以推断出构造方法的情况(不考虑Kotlin相关的类)
  1. 有且仅有一个@Autowired(required = true)注解标注的构造方法,不能出现其他有@Autowired注解标注的构造方法
  2. 有多个@Autowired(required = false)注解标注的构造方法,没有@Autowired(required = true)注解标注的构造方法(如果存在defaultConstructor,则将defaultConstructor也加入候选构造方法)
  3. 不存在@Autowired注解标注的构造方法,有唯一一个有参构造方法
2.注意点
  • 如果存在@Autowired注解标注的构造方法,要么推断出构造方法,要么抛出异常
3.个人理解

我们可以把@Autowired(required = true)注解标注的构造方法当成一个写锁,把@Autowired(required = false)注解标注的构造方法当成一个读锁。在并发情况下,只能一个写锁或多个读锁加锁成功 (即推断出了候选构造方法)

举例演示上述各种情况
代码准备

创建实体类

package com.test.ctor.component;


import org.springframework.stereotype.Component;

@Component
public class Elephant{

}
package com.test.ctor.component;

import org.springframework.stereotype.Component;

@Component
public class Monkey {
}

创建配置类

package com.test.ctor.config;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("com.test.ctor")
public class AppConfig {

}

 创建启动类

package com.test.ctor;

import com.test.ctor.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

    }
}
case1 : 有且仅有一个@Autowired(required = true)注解标注的构造方法
package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo1 {

    private Elephant elephant;

    @Autowired(required = true)
    public Zoo1(Elephant elephant) {
        this.elephant = elephant;
    }
}

debug源码,查看推断结果 

 case2 : 存在多个@Autowired(required = false)注解标注的构造方法 + 无参构造方法
package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo2 {

    private Elephant elephant;

    private Monkey monkey;

    public Zoo2() {
    }

    @Autowired(required = false)
    public Zoo2(Elephant elephant) {
        this.elephant = elephant;
    }

    @Autowired(required = false)
    public Zoo2(Monkey monkey) {
        this.monkey = monkey;
    }
}

debug源码,查看推断结果 

推断出了@Autowired(required = false)注解标注的构造方法,和无参构造方法

case3 : 不存在@Autowired注解标注的构造方法,有唯一一个有参构造方法
package com.test.ctor.component;

import org.springframework.stereotype.Component;

@Component
public class Zoo3 {

    private Elephant elephant;

    public Zoo3(Elephant elephant) {
        this.elephant = elephant;
    }
}

debug源码,查看推断结果 

case4 : 同时存在@Autowired(required = true)注解标注的构造方法和@Autowired(required = false)注解标注的构造方法
package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo4 {

    private Elephant elephant;

    private Monkey monkey;

    @Autowired(required = true)
    public Zoo4(Elephant elephant) {
        this.elephant = elephant;
    }

    @Autowired(required = false)
    public Zoo4(Monkey monkey) {
        this.monkey = monkey;
    }
}

 查看结果 

直接抛出在上面源码注释中,TODO 1处的异常

2.2 autowireConstructor

进入autowireConstructor方法最少满足下列条件之一

  1. ctors != null
  2. mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR
  3. mbd.hasConstructorArgumentValues()
  4. !ObjectUtils.isEmpty(args)

先简单阐述一下如何满足这四个条件,后面会举例演示其他三种条件用法

  1. determineConstructorsFromBeanPostProcessors方法推断出候选构造方法
  2. 自定义BeanFactoryPostProcessor修改了BeanDefinition的autowireMode属性
  3. 自定义BeanFactoryPostProcessor给BeanDefinition的constructorArgumentValues属性赋值
  4. 通过beanFactory获取一个懒加载的bean(或者原型bean)的时候,添加参数
分阶段解析
阶段一 处理constructorToUse和argsToUse

  • case1 如果方法传入了指定参数,则将这指定参数作为argsToUse
  • case2 比如针对原型bean,第一次获取bean的时候,缓存了constructorToUse和argsToUse,第二次则取缓存的constructorToUse和argsToUse。如果出现case1情况,即使缓存了constructorToUse和argsToUse也不会生效
阶段二 获取待处理构造方法

如果determineCandidateConstructors方法推断出候选构造方法,则将这些候选构造方法作为待处理构造方法,否则根据mbd参数判断是将所有构造方法作为待处理构造方法还是将public修饰的构造方法作为待处理构造方法

如果只有一个待处理构造方法,没有特殊条件,且是无参构造方法则直接实例化bean对象

阶段三 确定构造方法最小参数个数

在上文中阐述了进入autowireConstructor方法的四个条件

  1. ctors != null
  2. mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR
  3. mbd.hasConstructorArgumentValues()
  4. !ObjectUtils.isEmpty(args)

条件1、2、3最小参数个数:beanDefinition的constructorArgumentValues属性的属性的元素之和(indexedArgumentValues + genericArgumentValues)

条件4最小参数个数 : 传入参数数组(explicitArgs)的长度

条件1、2与条件3的不同点 :

  • 条件3只能从属性constructorArgumentValues中找相匹配的ConstructorArgumentValues.ValueHolder,如果找不到则抛出异常
  • 条件1、2 先从属性constructorArgumentValues中找相匹配的ConstructorArgumentValues.ValueHolder(一般不存在,所以条件1、2的最小参数个数一般为0),然后再从beanFactory中查找相关依赖(如果找不到相关依赖也会报错)。
阶段四 给待处理的构造方法排序

虽然只有一行代码,但是这个排序很重要,它最终决定了,以什么构造方法实例化bean对象,如果优先级靠后,就很难成为最后被选中的构造方法。

排序规则
  1. public > 其他修饰符修饰的构造方法
  2. 构造方法参数个数从大到小
  3. code的书写顺序(隐藏规则)

举例演示上述几种情况

demo1

创建实体类Zoo5

package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo5 {

    private Elephant elephant;

    private Monkey monkey;

    @Autowired(required = false)
    public Zoo5(Monkey monkey) {
        this.monkey = monkey;
    }

    @Autowired(required = false)
    Zoo5(Elephant elephant, Monkey monkey) {
        this.elephant = elephant;
        this.monkey = monkey;
    }
}

运行main方法,查看运行结果

使用的是第一个public描述的构造方法(利用第一优先级规则)

demo2

创建实体类Zoo6

package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo6 {

    private Elephant elephant;

    private Monkey monkey;

    @Autowired(required = false)
    public Zoo6(Elephant elephant) {
        this.elephant = elephant;
    }

    @Autowired(required = false)
    public Zoo6(Monkey monkey, Elephant elephant) {
        this.monkey = monkey;
        this.elephant = elephant;
    }
}

运行main方法,查看运行结果

同修饰符,使用的参数个数多的(利用第二优先级规则)

demo3

创建实体类Zoo7

package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo7 {

    private Elephant elephant;

    private Monkey monkey;

    @Autowired(required = false)
    public Zoo7(Monkey monkey) {
        this.monkey = monkey;
    }

    @Autowired(required = false)
    public Zoo7(Elephant elephant) {
        this.elephant = elephant;
    }
}

运行main方法,查看运行结果

使用的是code书写顺序的第一个

调换两个构造方法位置

package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo7 {

    private Elephant elephant;

    private Monkey monkey;

    @Autowired(required = false)
    public Zoo7(Elephant elephant) {
        this.elephant = elephant;
    }

    @Autowired(required = false)
    public Zoo7(Monkey monkey) {
        this.monkey = monkey;
    }
}

再次运行main方法,查看运行结果

同修饰符同参数个数,按住code书写顺序选择第一个(利用第三优先级规则)

阶段五 直接跳出循环或者过滤一些不符合要求构造方法

结合阶段四的分析,说明一下跳出循环的条件

// 通过上一阶段的分析,我们知道了构造器优先级顺序,我们来思考一下什么情况下符合下面的条件
// case1:同优先级,已经成功解析了一个参数更多的构造方法
// case2:不同优先级,已经成功解析的优先级更高的构造方法,拥有的参数个数更多
// PS:Spring可能觉得参数更多的构造方法更有价值,就不再处理那些优先级更低,参数个数更少的构造方法了
if (constructorToUse != null && argsToUse != null && argsToUse.length > parameterCount) {
    // Already found greedy constructor that can be satisfied ->
    // do not look any further, there are only less greedy constructors left.
    break;
}
// 参数个数必须满足的阈值
if (parameterCount < minNrOfArgs) {
    continue;
}
阶段六 一些解析工作

主要将ConstructorArgumentValues.ValueHolder对象转换成ArgumentsHolder对象,有兴趣的小伙伴可以看一下细节 

阶段七 计算类型差异权重

这个阶段也很重要,我们在阶段四讨论构造方法优先级的时候说过,如果优先级较低,最后基本很难成为constructorToUse,这个阶段也是唯一一个优先级较低的情况下可能成为constructorToUse的情况。

我们主要说明一下getTypeDifferenceWeight方法

  • 如果构造方法的参数是一个接口,最后Spring找到一个接口的实现类注入进去,权重+1
  • 如果构造方法的参数是一个基类,最后Spring找到一个基类的子类注入进去,权重+2
  • 如果构造方法的参数是一个基类,最后Spring找到一个基类的孙子类注入进去,权重+4 (以此类推)

我们具体说明第一种情况,其他情况类似,就不重复举例

创建接口Bear 实现类WhiteBear 以及实体类Zoo8

package com.test.ctor.component;

public interface Bear {
}
package com.test.ctor.component;

import org.springframework.stereotype.Component;

@Component
public class WhiteBear implements Bear {
}
package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo8 {

    private Bear bear;

    private Monkey monkey;

    @Autowired(required = false)
    public Zoo8(Bear bear) {
        this.bear = bear;
    }

    @Autowired(required = false)
    private Zoo8(Monkey monkey) {
        this.monkey = monkey;
    }
}

运行main方法,查看运行结果

根据第一优先级规则,应该推断出第一个public修饰的构造方法。但是因为构造方法的参数类型和实际注入进去的类型存在差异,权重值高于另外一个构造方法,所以最后Spring选择了第二个构造方法

如果权重值,优先级和参数个数都一致,根据第三优先级规则,则选择code书写顺序在前面的那个,另外一个放入ambiguousConstructors。如果BeanDefinition的属性值lenientConstructorResolution为false则抛出异常

3.举例演示另外几种条件推断构造方法

mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR

创建实体类Zoo9

package com.test.ctor.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Zoo9 {

    private Elephant elephant;

    private Monkey monkey;

    public Zoo9(Elephant elephant) {
        this.elephant = elephant;
    }

    public Zoo9(Monkey monkey, Elephant elephant) {
        this.monkey = monkey;
        this.elephant = elephant;
    }
}

 创建bfpp

package com.test.ctor.bfpp;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class FirstBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)registry.getBeanDefinition("zoo9");
        beanDefinition.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

运行main方法,查看运行结果

优先选取参数个数多的(第一优先级一致,根据第二优先级)

mbd.hasConstructorArgumentValues()

创建实体类Swan Zoo10

package com.test.ctor.component;

public class Swan {

    private String sign;

    public Swan() {
    }

    public Swan(String sign) {
        this.sign = sign;
    }
}
package com.test.ctor.component;

import org.springframework.stereotype.Component;

@Component
public class Zoo10 {

    private Elephant elephant;

    private Swan swan;

    public Zoo10(Elephant elephant) {
        this.elephant = elephant;
    }

    public Zoo10(Swan swan) {
        this.swan = swan;
    }
}

创建bfpp

package com.test.ctor.bfpp;

import com.test.ctor.component.Swan;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class SecondBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) registry.getBeanDefinition("zoo10");
        beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, new Swan("666"));
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

运行main方法,查看运行结果

最终根据我们设置的值,推断构造方法

!ObjectUtils.isEmpty(args)

创建实例类Zoo11

package com.test.ctor.component;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
@Lazy
public class Zoo11 {

    private Elephant elephant;

    private Swan swan;

    public Zoo11(Elephant elephant) {
        this.elephant = elephant;
    }

    public Zoo11(Swan swan) {
        this.swan = swan;
    }
}

运行main方法,查看运行结果 

 跟上一种类似,都是根据我们的传入值推断的构造方法(注意Zoo11为懒加载的,Spring启动就创建的话args为null)

总结

1.如果存在@Autowired注解标注的构造方法,要么推断出构造方法,要么抛出异常

2.如果满足条件进入autowireConstructor方法,则必须推断出唯一构造方法来实例化bean对象

3.注意三大优先级规则

4.理解类型差异权重的计算

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值