深入理解@Autowired注解以及Spring加载Bean的机制

@Autowired注解在平时开发中用的非常的多,即自动装配,这些天碰到了一个与之相关的报错,所以打算深入理解其原理。

首先看看它的定义如下:这里提一下它上面有三个元注解:

1、@Target表示这个注解是作用在什么上,方法上还是类上还是参数上等等。

2、@Rentention 可以理解为这个注解的生命周期,RententionPolicy.SOURCE、RententionPolicy.CLASS(默认值)、RententionPolicy.RUNTIME分别表示:Java源文件—》class文件—》内存中的字节码。编译或者运行时,都有可能会取消注解。Rentention的3种取值意味让注解保留到哪个阶段

3、@Document   Documented注释的作用及其javadoc文档生成工具的使用

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

 读者最好结合源码看,看完这个注解的定义后,再介绍一个相当重要的类,那就是  AutowiredAnnotationBeanPostProcessor 这个类,我们的Autowired注解都是通过这个类来先预解析的。这个类里面最关键的方法就是下面列出的这个。

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        InjectionMetadata metadata = this.findAutowiringMetadata(beanName, beanType, (PropertyValues)null);
        metadata.checkConfigMembers(beanDefinition);
    }

为什么这个预解析的类里面会有这个 postProcessMergedBeanDefinition 这个方法呢,原因就是我们可以发现这个类实现了这么一个接口MergedBeanDefinitionPostProcessor,实现了这个方法,那么这个Autowired的注解解析类又为什么要实现这么一个接口呢,接下来就又得提到Spring加载一个bean的过程了。

实例化bean的调用链如下  :

  1. DefaultListbleBeanFactory.getBean() 
  2. ——》AbstractBeanFactory.doGetBean()
  3. ————》DefaultSingletonBeanRegistry.getSingleton()
  4. ——————》AbstractAutowireCapableBeanFactory.createBean()
  5. ————————》AbstractAutowireCapableBeanFactory.doCreateBean()
  6. ——————————》AbstractAutowireCapableBeanFactory.createBeanInstance()
  7. ————————————》AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors()
  8. 都在这个类里面完成的,省略了。。。。

所以一个Bean的实例化初始化填充属性等等什么的都是在AbstractAutowireCapableBeanFactory这个类中完成的,为什么要大费口舌说这个类呢,我们从createBean这个方法中一直跟下去,就能得到答案。接下来看看createBean这个方法。

    protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
        if(this.logger.isDebugEnabled()) {
            this.logger.debug("Creating instance of bean \'" + beanName + "\'");
        }

        RootBeanDefinition mbdToUse = mbd;
       
        //确认并加载bean的class
        Class resolvedClass = this.resolveBeanClass(mbd, beanName, new Class[0]);
        if(resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
            mbdToUse = new RootBeanDefinition(mbd);
            mbdToUse.setBeanClass(resolvedClass);
        }

        try {
            mbdToUse.prepareMethodOverrides();
        } catch (BeanDefinitionValidationException var9) {
            throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", var9);
        }

        Object ex;
        try {
            ex = this.resolveBeforeInstantiation(beanName, mbdToUse);
            if(ex != null) {
                return ex;
            }
        } catch (Throwable var10) {
            throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", var10);
        }

        try {
            //真正的创建bean
            ex = this.doCreateBean(beanName, mbdToUse, args);
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("Finished creating instance of bean \'" + beanName + "\'");
            }

            return ex;
        } catch (ImplicitlyAppearedSingletonException | BeanCreationException var7) {
            throw var7;
        } catch (Throwable var8) {
            throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", var8);
        }
    }

这段代码真正做的事情就是下面这几步:

  1. 根据设置的class属性或者className来解析、加载class
  2. 对override属性进行标记和验证(bean XML配置中的lookup-method和replace-method属性)
  3. 应用初始化前的后处理器,如果处理器中返回了AOP的代理对象,则直接返回该单例对象,不需要继续创建
  4. 创建bean

 

    protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
        BeanWrapper instanceWrapper = null;
        if(mbd.isSingleton()) {
            //先尝试从缓存中取出
            instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
        }
            //如果缓存没有命中则根据对应的策略创建实例
        if(instanceWrapper == null) {
            instanceWrapper = this.createBeanInstance(beanName, mbd, args);
        }

        Object bean = instanceWrapper.getWrappedInstance();
        Class beanType = instanceWrapper.getWrappedClass();
        if(beanType != NullBean.class) {
            mbd.resolvedTargetType = beanType;
        }

        Object earlySingletonExposure = mbd.postProcessingLock;
        synchronized(mbd.postProcessingLock) {
            //调用MergedBeanDefinitionPostProcessor 后处理器,合并bean的定义信息
            //Autowire等注解信息就是在这一步完成预解析,并且将注解需要的信息放入缓存
            if(!mbd.postProcessed) {
                try {
                    //主要关注这个方法,后面会讲到
                    this.applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
                } catch (Throwable var17) {
                    throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", var17);
                }

                mbd.postProcessed = true;
            }
        }
        //
        boolean var20 = mbd.isSingleton() && this.allowCircularReferences && this.isSingletonCurrentlyInCreation(beanName);
        if(var20) {
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("Eagerly caching bean \'" + beanName + "\' to allow for resolving potential circular references");
            }

            this.addSingletonFactory(beanName, () -> {
                return this.getEarlyBeanReference(beanName, mbd, bean);
            });
        }

        Object exposedObject = bean;

        try {
        //重点关注方法,后面会讲到
        // 对bean属性进行填充,注入bean中的属性,会递归初始化依赖的bean
            this.populateBean(beanName, mbd, instanceWrapper);
        //调用初始化方法,比如init-method、注入Aware对象
            exposedObject = this.initializeBean(beanName, exposedObject, mbd);
        } catch (Throwable var18) {
            if(var18 instanceof BeanCreationException && beanName.equals(((BeanCreationException)var18).getBeanName())) {
                throw (BeanCreationException)var18;
            }

            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", var18);
        }

        if(var20) {
            Object ex = this.getSingleton(beanName, false);
            if(ex != null) {
                if(exposedObject == bean) {
                    exposedObject = ex;
                } else if(!this.allowRawInjectionDespiteWrapping && this.hasDependentBean(beanName)) {
                    String[] dependentBeans = this.getDependentBeans(beanName);
                    LinkedHashSet actualDependentBeans = new LinkedHashSet(dependentBeans.length);
                    String[] var12 = dependentBeans;
                    int var13 = dependentBeans.length;

                    for(int var14 = 0; var14 < var13; ++var14) {
                        String dependentBean = var12[var14];
                        if(!this.removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                            actualDependentBeans.add(dependentBean);
                        }
                    }

                    if(!actualDependentBeans.isEmpty()) {
                        throw new BeanCurrentlyInCreationException(beanName, "Bean with name \'" + beanName + "\' has been injected into other beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using \'getBeanNamesOfType\' with the \'allowEagerInit\' flag turned off, for example.");
                    }
                }
            }
        }

        try {
            this.registerDisposableBeanIfNecessary(beanName, bean, mbd);
            return exposedObject;
        } catch (BeanDefinitionValidationException var16) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", var16);
        }
    }

 这里所做的事有很多:

  1. 如果是单例则首先清除缓存
  2. 实例化bean,并使用BeanWarpper包装
  3. 如果存在工厂方法,则使用工厂方法实例化
  4. 如果有多个构造函数,则根据传入的参数确定构造函数进行初始化使用默认的构造函数初始化
  5. 调用MergedBeanDefinitionPostProcessor,Autowired注解就是在这样完成的预解析工作
  6. 依赖处理。如果A和B存在循环依赖,那么Spring在创建B的时候,需要自动注入A时,并不会直接创建再次创建A,而是通过放入缓存中A的ObjectFactory来创建实例,这样就解决了循环依赖的问题
  7. 属性填充。所有需要的属性都在这一步注入到bean
  8. 循环依赖检查
  9. 注册DisposableBean。如果配置了destroy-method,这里需要注册,以便在销毁时调用
  10. 完成创建并返回

因为我们这里只关心的是@Autowired注解的相关动作,到这里先打住,这里我们就看到了@Autowired注解的预解析工作,重点关注以下这个applyMergedBeanDefinitionPostProcessors方法。

    protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
        Iterator var4 = this.getBeanPostProcessors().iterator();

        while(var4.hasNext()) {
            BeanPostProcessor bp = (BeanPostProcessor)var4.next();
            if(bp instanceof MergedBeanDefinitionPostProcessor) {
                MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor)bp;
                bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
            }
        }

    }

我们发现,这里面就循环调用了所有的实现了MergedBeanDefinitionPostProcessor接口的类的postProcessMergedBeanDefinition方法,这时我们就明白了为什么开始上面说AutowiredAnnotationBeanPostProcessor这个类的这个下面方法是核心方法了。也就是说Spring预解析注解的入口就是这个方法,下面关注这个方法。

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        InjectionMetadata metadata = this.findAutowiringMetadata(beanName, beanType, (PropertyValues)null);
        metadata.checkConfigMembers(beanDefinition);
    }

继续跟下去,这个方法又调用了两个方法,首先看看这个findAutowiringMetadata方法

    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        String cacheKey = StringUtils.hasLength(beanName)?beanName:clazz.getName();
        InjectionMetadata metadata = (InjectionMetadata)this.injectionMetadataCache.get(cacheKey);
        if(InjectionMetadata.needsRefresh(metadata, clazz)) {
            Map var6 = this.injectionMetadataCache;
            synchronized(this.injectionMetadataCache) {
                metadata = (InjectionMetadata)this.injectionMetadataCache.get(cacheKey);
                if(InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if(metadata != null) {
                        metadata.clear(pvs);
                    }

                    metadata = this.buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }

        return metadata;
    }

顾名思义这个方法就是找到Autowired元数据,这个方法其实没有做什么,首先有一个injectionMetadataCache的Map对象,它是用来缓存beanName与InjectionMetadata的。重要的是InjectionMetadata对象通过buildAutowiringMetadata()方法来创建的。所以继续跟进buildAutowiringMetadata方法,最后调用完之后放入缓存中。

    private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
        ArrayList elements = new ArrayList();
        Class targetClass = clazz;

        do {
            ArrayList currElements = new ArrayList();
            //遍历这个类的所有的field去寻找又Autowired修饰的field
            ReflectionUtils.doWithLocalFields(targetClass, (field) -> {
                AnnotationAttributes ann = this.findAutowiredAnnotation(field);
                if(ann != null) {
                    //如果这个field是静态static的就警告并停止
                    if(Modifier.isStatic(field.getModifiers())) {
                        if(this.logger.isWarnEnabled()) {
                            this.logger.warn("Autowired annotation is not supported on static fields: " + field);
                        }

                        return;
                    }
                    //这里就取了Autowired的一个required属性,这个属性的作用是
                    //如果这个是false就表明在自动装配的时候没有发现又对应的实例
                    //就跳过去,如果是true没有发现有与之匹配的就会抛出个异常,仅此而已
                    boolean required = this.determineRequiredStatus(ann);
                    currElements.add(new AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement(field, required));
                }

            });
            //接着是遍历这个类里面的方法
            ReflectionUtils.doWithLocalMethods(targetClass, (method) -> {
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if(BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    AnnotationAttributes ann = this.findAutowiredAnnotation(bridgedMethod);
                    //同样的静态方法不能自动装配
                    if(ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                        if(Modifier.isStatic(method.getModifiers())) {
                            if(this.logger.isWarnEnabled()) {
                                this.logger.warn("Autowired annotation is not supported on static methods: " + method);
                            }

                            return;
                        }

                        if(method.getParameterCount() == 0 && this.logger.isWarnEnabled()) {
                            this.logger.warn("Autowired annotation should only be used on methods with parameters: " + method);
                        }

                        boolean required = this.determineRequiredStatus(ann);
                        PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                        currElements.add(new AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement(method, required, pd));
                    }

                }
            });
            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        } while(targetClass != null && targetClass != Object.class);
        return new InjectionMetadata(clazz, elements);
    }

所以说这个buildAutowiringMetadata()方法才是主角,首先Spring尝试调用findAutowiringMetadata方法获取该bean的InjectionMetadata实例(也就是有哪些属性需要被自动装配,也就是查找被@Autowired注解标记的元素)。怎么获取呢?首先去缓存里面找,找不到就遍历bean的和父类的字段域和方法,如果别标记为@Autowired并且不是静态的就添加到InjectionMetadata中,它所作的事情就分为对字段方法的@Autowired注解处理。

1、将标记@Autowired的字段封装为AutowiredFieldElement对象。

2、将标记@Autowired的方法并且此方法是字段的getter或setter方法封装到AutowiredMethodElement对象

然后都加入到InjectionMetadata中,此过程是一个循环过程此类处理完会沿着父类继续向上处理。AutowiredFieldElement与AutowiredMethodElement都是InjectionMetadata.InjectedElement的子类,都覆盖了父类的inject()方法这个下面会介绍。
 

 这时这个方法跟到底了事情也做完了,无非就是产生了一个InjectionMetadata的实例,然后返回上面的postProcessMergedBeanDefinition,发现还有一个动作就是调用InjectionMetadata实例checkConfigMembers方法

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        InjectionMetadata metadata = this.findAutowiringMetadata(beanName, beanType, (PropertyValues)null);
        metadata.checkConfigMembers(beanDefinition);
    }
    public void checkConfigMembers(RootBeanDefinition beanDefinition) {
        LinkedHashSet checkedElements = new LinkedHashSet(this.injectedElements.size());
        //injectedElements这个集合就是类里面所有被Autowired修饰过的field或method
        Iterator var3 = this.injectedElements.iterator();
        //遍历这个集合
        //把method或者field放入externallyManagedConfigMembers缓存中
        while(var3.hasNext()) {
            InjectionMetadata.InjectedElement element = (InjectionMetadata.InjectedElement)var3.next();
            Member member = element.getMember();
            if(!beanDefinition.isExternallyManagedConfigMember(member)) {
                beanDefinition.registerExternallyManagedConfigMember(member);
                checkedElements.add(element);
                if(logger.isDebugEnabled()) {
                    logger.debug("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
                }
            }
        }

        this.checkedElements = checkedElements;
    }

到此预解析的动作进行完毕了,那么预解析完成之后又是在哪里注入的呢,我们回到刚开始的doCreateBean这个方法中,我们发现后面有这么一个动作this.populateBean(beanName, mbd, instanceWrapper);跟进去看看

    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
       //如果实例为空但是还有属性,抛出异常
            if(bw == null) {
            if(mbd.hasPropertyValues()) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
            }
        } else {
            boolean continueWithPropertyPopulation = true;
            //这边主要是根据InstantiationAwareBeanPostProcessor 判断是否继续给该bean配置属性
            if(!mbd.isSynthetic() && this.hasInstantiationAwareBeanPostProcessors()) {
                Iterator pvs = this.getBeanPostProcessors().iterator();

                while(pvs.hasNext()) {
                    BeanPostProcessor hasInstAwareBpps = (BeanPostProcessor)pvs.next();
                    if(hasInstAwareBpps instanceof InstantiationAwareBeanPostProcessor) {
                        InstantiationAwareBeanPostProcessor needsDepCheck = (InstantiationAwareBeanPostProcessor)hasInstAwareBpps;
                        if(!needsDepCheck.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                            continueWithPropertyPopulation = false;
                            break;
                        }
                    }
                }
            }

            if(continueWithPropertyPopulation) {
                Object pvs1 = mbd.hasPropertyValues()?mbd.getPropertyValues():null;
                //判断如果是按照名字或者类型注入的就进入这条分支
                if(mbd.getResolvedAutowireMode() == 1 || mbd.getResolvedAutowireMode() == 2) {
                    MutablePropertyValues hasInstAwareBpps1 = new MutablePropertyValues((PropertyValues)pvs1);
                    if(mbd.getResolvedAutowireMode() == 1) {
                        this.autowireByName(beanName, mbd, bw, hasInstAwareBpps1);
                    }

                    if(mbd.getResolvedAutowireMode() == 2) {
                        this.autowireByType(beanName, mbd, bw, hasInstAwareBpps1);
                    }

                    pvs1 = hasInstAwareBpps1;
                }

                boolean hasInstAwareBpps2 = this.hasInstantiationAwareBeanPostProcessors();
                boolean needsDepCheck1 = mbd.getDependencyCheck() != 0;
                if(hasInstAwareBpps2 || needsDepCheck1) {
                    if(pvs1 == null) {
                        pvs1 = mbd.getPropertyValues();
                    }

                    PropertyDescriptor[] filteredPds = this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
                    if(hasInstAwareBpps2) {
                        Iterator var9 = this.getBeanPostProcessors().iterator();

                        while(var9.hasNext()) {
                            BeanPostProcessor bp = (BeanPostProcessor)var9.next();
//这里循环调用了InstantiationAwareBeanPostProcessor实现类的postProcessPropertyValues
//方法,我们Autowire注解解析类AutowiredAnnotationBeanPostProcessor也是该接口的实现类
                            if(bp instanceof InstantiationAwareBeanPostProcessor) {
                                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor)bp;
                                pvs1 = ibp.postProcessPropertyValues((PropertyValues)pvs1, filteredPds, bw.getWrappedInstance(), beanName);
                                if(pvs1 == null) {
                                    return;
                                }
                            }
                        }
                    }

                    if(needsDepCheck1) {
                        this.checkDependencies(beanName, mbd, filteredPds, (PropertyValues)pvs1);
                    }
                }

                if(pvs1 != null) {
                    this.applyPropertyValues(beanName, mbd, bw, (PropertyValues)pvs1);
                }

            }
        }
    }

这里关键的我觉得就两点

  1. autowireByName方法与autowireByType方法

  2. 循环遍历调用的postProcessPropertyValues方法

所以接下来主要讲一下这两个,首先看一下autowireByName与AutowireByType

    protected void autowireByName(String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
        //这个unsatisfiedNonSimpleProperties方法,byName byType都用到了
        //主要作用就是遍历bean的所有的属性将满足条件的属性筛选出来
        //必须有set方法,必须不能是忽略的类型或者接口,spring配置的properties中没有改属性
        //该属性不能是 a primitive, an enum, a String or other CharSequence, a Number, a                         
        //Date,a URI, a URL, a Locale or a Class.
        //有兴趣的可以跟进去看看
        String[] propertyNames = this.unsatisfiedNonSimpleProperties(mbd, bw);
        String[] var6 = propertyNames;
        int var7 = propertyNames.length;

        for(int var8 = 0; var8 < var7; ++var8) {
            String propertyName = var6[var8];
            if(this.containsBean(propertyName)) {
                Object bean = this.getBean(propertyName);
                pvs.add(propertyName, bean);
                this.registerDependentBean(propertyName, beanName);
                if(this.logger.isDebugEnabled()) {
                    this.logger.debug("Added autowiring by name from bean name \'" + beanName + "\' via property \'" + propertyName + "\' to bean named \'" + propertyName + "\'");
                }
            } else if(this.logger.isTraceEnabled()) {
                this.logger.trace("Not autowiring property \'" + propertyName + "\' of bean \'" + beanName + "\' by name: no matching bean found");
            }
        }

    }

    protected void autowireByType(String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
        Object converter = this.getCustomTypeConverter();
        if(converter == null) {
            converter = bw;
        }

        LinkedHashSet autowiredBeanNames = new LinkedHashSet(4);
        String[] propertyNames = this.unsatisfiedNonSimpleProperties(mbd, bw);
        String[] var8 = propertyNames;
        int var9 = propertyNames.length;

        for(int var10 = 0; var10 < var9; ++var10) {
            String propertyName = var8[var10];
            try {
                PropertyDescriptor ex = bw.getPropertyDescriptor(propertyName);
                if(Object.class != ex.getPropertyType()) {
                    MethodParameter methodParam = BeanUtils.getWriteMethodParameter(ex);
                    boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance());
                    AbstractAutowireCapableBeanFactory.AutowireByTypeDependencyDescriptor desc = new AbstractAutowireCapableBeanFactory.AutowireByTypeDependencyDescriptor(methodParam, eager);
                    Object autowiredArgument = this.resolveDependency(desc, beanName, autowiredBeanNames, (TypeConverter)converter);
                    if(autowiredArgument != null) {
                        pvs.add(propertyName, autowiredArgument);
                    }
                    Iterator var17 = autowiredBeanNames.iterator();
                    while(var17.hasNext()) {
                        String autowiredBeanName = (String)var17.next();
                        this.registerDependentBean(autowiredBeanName, beanName);
                    }
                    autowiredBeanNames.clear();
                }
            } 
        }

    }

 

  • 32
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值