Spring中ConfigClass配置类是如何解析的

本文详细解读了Spring框架中的ConfigurationClassParser类,重点介绍了如何解析配置类、处理延迟导入、循环导入检测以及各种注解如@ComponentScan、@PropertySource和@Import的处理机制。
摘要由CSDN通过智能技术生成
class ConfigurationClassParser {
    // 解析配置类
    public void parse(Set<BeanDefinitionHolder> configCandidates) {
        // 遍历所有的配置类
        for (BeanDefinitionHolder holder : configCandidates) {
            BeanDefinition bd = holder.getBeanDefinition();
            // 根据不同的参数,封装成ConfigurationClass配置类对象,调用processConfigurationClass进行解析
            if (bd instanceof AnnotatedBeanDefinition) {
                parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()) {
                    // 详见下面解析方法
                    processConfigurationClass(new ConfigurationClass(metadata, beanName));
                }
                continue;
            }
            if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
                parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()) {
                    // 详见下面解析方法
                    processConfigurationClass(new ConfigurationClass(clazz, beanName));
                }
                continue;
            }
            parse(bd.getBeanClassName(), holder.getBeanName()) {
                // 详见下面解析方法
                MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
                processConfigurationClass(new ConfigurationClass(reader, beanName));
            }
        }
        // 上面,所有的ConfigClass都解析完成,现在就要解析延迟导入的类了
        this.deferredImportSelectorHandler.process(){
            // 获取已经保存了需要延迟导入的类
            List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
            // 将deferredImportSelectors置为空,这个要与ConfigurationClassParser.DeferredImportSelectorHandler.handle对比看
            // 因为将deferredImportSelectors置为空,如果调用DeferredImportSelectorHandler.handle才是真正执行导入逻辑
            this.deferredImportSelectors = null;
            try {
                // 在解析的时候,添加到deferredImportSelectors的Import类,还没有处理
                // 如果存在现在来处理
                if (deferredImports != null) {
                    // 创建一个延迟导入的分组处理器,用于分组导入
                    DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
                    // 排序
                    deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
                    // 将导入的类保存到configurationClasses中
                    handler.register(holder) {
                        // 获取导入的分组
                        Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
                        // 保存到map中
                        DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent((group != null ? group : deferredImport), key -> new DeferredImportSelectorGrouping(createGroup(group)));
                        // 将当前导入的类保存到当前分组中
                        grouping.add(deferredImport);
                        // 缓存需要解析的配置类信息
                        this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getConfigurationClass());
                    }
                    // 此时真正处理延迟的Import的类
                    handler.processGroupImports() {
                        // 遍历所有的分组
                        for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
                            // 分组导入
                            grouping.getImports().forEach(entry -> {
                                // 获取缓存的ConfigClass
                                ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
                                // 循环处理Import注解
                                processImports(configurationClass, asSourceClass(configurationClass), asSourceClasses(entry.getImportClassName()), false);
                            });
                        }
                    }
                }
            } finally {
                // 处理完当前延迟的类,需要将deferredImportSelectors重新赋值为默认值
                this.deferredImportSelectors = new ArrayList<>();
            }
        }
    }

    /**
     * 解析配置类
     *
     * @param configClass 配置类的所有信息
     */
    protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
        // 使用条件表达式判断当前配置类是否符合解析条件
        // 就是处理@Conditional注解
        if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
            return;
        }
        // 处理过的配置类需要缓存
        ConfigurationClass existingClass = this.configurationClasses.get(configClass);
        // 如果之前已经处理过该配置类
        if (existingClass != null) {
            // 判断正在解析的配置类是不是被导入的
            // importedBy保存的是当前配置类是被哪个配置类导入的
            // 例如A导入B,在B配置类的importedBy中,存入的是A的配置信息
            if (configClass.isImported() {
                return !this.importedBy.isEmpty();
            }){
                // 上次解析这个类也是被导入的
                if (existingClass.isImported()) {
                    // 将导入信息合并,原因就是A是被B导入的,现在A又被C导入,所以,A最终是被B,C导入的
                    existingClass.mergeImportedBy(configClass) {
                        this.importedBy.addAll(otherConfigClass.importedBy);
                    }
                }
                // 这个判断就是为了mergeImportedBy,只做这一件事
                // 因为这个类已经被解析过了,其他的不需要再处理
                return;
            }
            // 只要不是被导入的,就没必要保存importedBy信息
            // 所以,可以直接将已经解析过的配置删除,重新解析一次
            else{
                this.configurationClasses.remove(configClass);
                this.knownSuperclasses.values().removeIf(configClass::equals);
            }
        }

        // 将配置类封装成SourceClass,SourceClass是描述目标类的信息,内部包含正在解析的类的元数据信息
        // 因为最终要获取到当前配置类的所有父类,所以封装成SourceClass会更方便一些
        SourceClass sourceClass = asSourceClass(configClass)
        do {
            // 正式解析配置类的注解或者方法信息,核心
            sourceClass = doProcessConfigurationClass(configClass, sourceClass);
        }
        // 不停地解析,直到返回空
        while (sourceClass != null);
        // 保存解析后的配置类,内部包含了解析到的所有数据
        this.configurationClasses.put(configClass, configClass);
    }

    /**
     * 解析的核心逻辑
     *
     * @param configClass 正在解析的配置类
     * @param sourceClass 配置类本身的元信息对象
     */
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) {
        // 第一步: 解析配置类中的嵌套的类,内部类
        // 处理配置类中包含了Component注解的情况,@Component和@Configuration都包含
        if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
            // 解析配置类中的嵌套的类,内部类
            processMemberClasses(configClass, sourceClass) {
                // 获取该配置类的所有成员类
                Collection<SourceClass> memberClasses = sourceClass.getMemberClasses() {
                    // 内部调用jdk内置的提供获取成员类的方法
                    Class<?>[] declaredClasses = sourceClass.getDeclaredClasses();
                    for (Class<?> declaredClass : declaredClasses) {
                        members.add(asSourceClass(declaredClass));
                    }
                    // 保存所有的成员类
                    return members;
                }
                // 如果存在成员类
                if (!memberClasses.isEmpty()) {
                    // 要判断一下成员类是不是符合配置类的条件
                    // 如果符合配置类的条件,也需要作为配置类进行解析
                    List<SourceClass> candidates = new ArrayList<>(memberClasses.size());
                    for (SourceClass memberClass : memberClasses) {
                        // 是否符合配置类的条件,上面有相吸解析
                        if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) {
                            // 如果此类是接口,不处理
                            if (metadata.isInterface()) {
                                return false;
                            }

                            // static {
                            // 	candidateIndicators.add(Component.class.getName());
                            // 	candidateIndicators.add(ComponentScan.class.getName());
                            // 	candidateIndicators.add(Import.class.getName());
                            // 	candidateIndicators.add(ImportResource.class.getName());
                            // }
                            // 如果是类上存在上面这几个注解,表示也属于配置Bean,也算作是一个配置类
                            for (String indicator : candidateIndicators) {
                                if (metadata.isAnnotated(indicator)) {
                                    return true;
                                }
                            }

                            // 上面几种都没,判断当前类中是否存在@Bean标注的方法,如果存在,也表示是一个配置类
                            return metadata.hasAnnotatedMethods(Bean.class.getName());
                        }
                        // 当前成员类的类名不能与当前解析的配置类名称一样
                        &&!memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())){
                            // 表示当前类是一个配置类
                            candidates.add(memberClass);
                        }
                    }
                    // 对嵌套的配置类进行排序
                    OrderComparator.sort(candidates);
                    // 遍历这些嵌套的配置类
                    for (SourceClass candidate : candidates) {
                        // 这里是处理循环导入的问题
                        // 当正在导入的时候,当前配置类存在导入的栈中,导入完成才会被importStack出栈
                        // 所以所,如果当前类已经在导入栈中,说明它还未导入完
                        // 这说明什么,内部类是通过导入完成的,类似于@Import的方式来处理的
                        if (this.importStack.contains(configClass)) {
                            this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
                            continue;
                        }
                        // 如果没有被导入,保存到导入栈中
                        this.importStack.push(configClass);
                        try {
                            // 解析当前内部类配置类,递归处理
                            processConfigurationClass(candidate.asConfigClass(configClass));
                        } finally {
                            // 解析完成出栈
                            this.importStack.pop();
                        }
                    }
                }
            }
        }
        // 第二步: 处理@PropertySource注解
        for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class, PropertySource.class)) {
            // 解析@PropertySources的配置文件属性
            if (this.environment instanceof ConfigurableEnvironment) {
                // 解析配置文件
                processPropertySource(propertySource) {
                    // 获取文件路径
                    String[] locations = propertySource.getStringArray("value");
                    // 创建解析配置类的工厂
                    DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory();
                    PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ? DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
                    // 获取解析后的路径名,因为路径可以出现占位符
                    // @PropertySource(value = {"classpath:${user.name}.properties"})
                    // 对应系统变量中,user.name对应值.properties文件
                    String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
                    // 获取该文件的资源对象
                    Resource resource = this.resourceLoader.getResource(resolvedLocation);
                    // 添加
                    addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)) {
                        // 使用工厂factory创建一个ResourcePropertySource对象
                        return (name != null ? new ResourcePropertySource(name, resource) {
                            // 使用工具类PropertiesLoaderUtils加载配置文件
                            super(name,PropertiesLoaderUtils.loadProperties(resource))
                        } : new ResourcePropertySource(resource));
                    }){
                        // 判断当前配置文件是否被加载过
                        // 一个配置文件对应这一个propertySource对象
                        if (this.propertySourceNames.isEmpty()) {
                            // 如果没有被加载,保存到环境的所有的资源列表中
                            propertySources.addLast(propertySource);
                        }
                        // 保存已经解析过的配置文件名称
                        this.propertySourceNames.add(name);
                    }
                }
            }
        }

        // 第三步: 解析@ComponentScan注解
        Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
        // 存在componentScans注解,并且如果标注了@Condition注解且满足情况
        if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
            // 遍历所有的注解
            for (AnnotationAttributes componentScan : componentScans) {
                // 使用componentScanParser去解析这种类,详见ComponentScanParser解析@ComponentScan注解
                // this.componentScanParser = new ComponentScanAnnotationParser(environment, resourceLoader, componentScanBeanNameGenerator, registry);
                Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // 遍历所有扫描的Bean
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                    // 获取原始的BD,因为可能当前BD设置了需要被代理
                    BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                    // 如果bdCand没有,表示没有被代理,则直接获取BD
                    if (bdCand == null) {
                        bdCand = holder.getBeanDefinition();
                    }
                    // 如果它是一个配置类,上面有这个方法的解析
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                        // 调用解析配置类ConfigurationClassParser进行配置类解析
                        // 这里又是递归
                        ConfigurationClassParser.parse(bdCand.getBeanClassName(), holder.getBeanName());
                    }
                }
            }
        }

        // 第四步: 处理@Import注解
        processImports(configClass, sourceClass, getImports(sourceClass) {
            // 所有需要导入的类
            Set<SourceClass> imports = new LinkedHashSet<>();
            // 已经被处理过的类,这个SourceClass可能是类,也可能是注解
            Set<SourceClass> visited = new LinkedHashSet<>();
            // 收集@Import的类
            collectImports(sourceClass, imports, visited) {
                // 保存当前正在处理的SourceClass,表示当前SourceClass被处理过
                if (visited.add(sourceClass)) {
                    // 获取SourceClass的所有注解信息
                    for (SourceClass annotation : sourceClass.getAnnotations()) {
                        // 获取注解名
                        String annName = annotation.getMetadata().getClassName();
                        // 如果注解名不是@Import
                        if (!annName.equals(Import.class.getName())) {
                            // 递归收集注解中的嵌套注解信息
                            collectImports(annotation, imports, visited);
                        }
                    }
                    // 获取@Import注解中导入的类,如果不存在@Import注解,或者没有导入的类,则添加的是一个空集合
                    // 如果存在@Import注解,添加的则是导入的类
                    imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
                }
            }
            // 保存收集的@Import类
            return imports;
        },true){       // 不存在@Import注解
            if (importCandidates.isEmpty()) {
                return;
            }
            // 处理@Import循环导入问题
            if (checkForCircularImports && isChainedImportOnStack(configClass)) {
                this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
                this.importStack.pop();
                return;
            }
            // 将当前配置存入栈中,表示当前类正在导入
            this.importStack.push(configClass);
            // 遍历所有标注@Import的类
            for (SourceClass candidate : importCandidates) {
                // 第一种情况,如果当前类是实现了ImportSelector接口
                if (candidate.isAssignable(ImportSelector.class)) {
                    // 加载当前类
                    Class<?> candidateClass = candidate.loadClass();
                    // 实例化当前类,并且执行生命周期中的Aware接口
                    ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry);
                    // 如果ImportSelector是DeferredImportSelector这种类型,表示是延迟导入
                    if (selector instanceof DeferredImportSelector) {
                        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector) {
                            // 使用延迟导入处理器处理
                            this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector) {
                                // 将当前配置类和importSelector封装到DeferredImportSelectorHolder中
                                DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
                                // deferredImportSelectors=new ArrayList();
                                // 正常情况下,deferredImportSelectors赋了默认值,不可能为空,只有到真正处理Import类的时候会将它置空
                                // 所以,这里都不会进,只会走else逻辑,先保存配置信息,暂时不解析
                                // 要与DeferredImportSelectorHandler.process对比了看就知道
                                if (this.deferredImportSelectors == null) {
                                    // 创建一个延迟导入的分组处理器,用于分组导入
                                    DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
                                    // 将导入的类保存到configurationClasses中,等待处理
                                    handler.register(holder) {
                                        // 获取导入的分组
                                        Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
                                        // 保存到map中
                                        DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent((group != null ? group : deferredImport), key -> new DeferredImportSelectorGrouping(createGroup(group)));
                                        // 将当前导入的类保存到当前分组中
                                        grouping.add(deferredImport);
                                        // 缓存需要解析的配置类信息
                                        this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getConfigurationClass());
                                    }
                                    // 真正处理延迟的Import的类
                                    handler.processGroupImports() {
                                        // 遍历所有的分组
                                        for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
                                            // 分组导入
                                            grouping.getImports().forEach(entry -> {
                                                ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
                                                // 循环处理Import注解
                                                processImports(configurationClass, asSourceClass(configurationClass), asSourceClasses(entry.getImportClassName()), false);
                                            });
                                        }
                                    }
                                } else {
                                    // 先保存需要延迟处理的类
                                    this.deferredImportSelectors.add(holder);
                                }
                            }
                        }
                    } else {
                        // 其他类型的ImportSelector,直接回调Selector的selectImports方法,返回需要注册为Bean的类名
                        String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                        // 一一将类名封装成SourceClass
                        Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                        // 递归处理@Import注解,可能@import导入的类又导入其他类
                        processImports(configClass, currentSourceClass, importSourceClasses, false);
                    }
                }
                // 如果是第二种情况,实现了ImportBeanDefinitionRegistrar接口
                else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                    // 加载当前类
                    Class<?> candidateClass = candidate.loadClass();
                    // 实例化当前类,并且执行生命周期中的Aware接口
                    ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, this.environment, this.resourceLoader, this.registry);
                    // 将registrar保存到configClass中
                    configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()) {
                        // 将导入的ImportBeanDefinitionRegistrar保存到ConfigClass的importBeanDefinitionRegistrars变量中
                        // 因为ImportBeanDefinitionRegistrar就是单纯的注入Bean,没有像@Import这样,可能还有其他导入注解的逻辑
                        this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata);
                    }
                }
                // 其他情况,导入一个普通类
                else {
                    // 把它当做一个导入的@Configuration来处理,压入导入栈中
                    this.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                    // 递归处理配置类逻辑
                    processConfigurationClass(candidate.asConfigClass(configClass));
                }
            }
            // 处理完了导入的类需要出栈
            this.importStack.pop();
        }
    }

    // 第五步: 处理@ImportResource
    AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    // 存在@ImportResource注解
    if(importResource !=null){
        // 获取导入的配置文件路径
        String[] resources = importResource.getStringArray("locations");
        // 获取指定的BeanDefinitionReader
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        // 遍历所有的xml文件路径
        for (String resource : resources) {
            // 解析占位符,和PropertySource类似
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            // 保存到ConfigClass的importedResources变量中,记录了所有要导入的xml配置文件
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }

    // 第六步: 解析@Bean方法
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass){
        // 获取当前配置类的元数据
        AnnotationMetadata original = sourceClass.getMetadata();
        // 找类中所有的@Bean方法
        Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());
        //包含@Bean的方法有多个,这个时候需要处理一下@Bean方法的顺序
        if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
            // 获取元数据解析工厂
            AnnotationMetadata asm = this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
            // 使用asm方式获取@Bean方法
            Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
            // 这样做的目的是asm的方法是按照声明顺序返回的,而直接获取的方法是随机顺序返回的
            if (asmMethods.size() >= beanMethods.size()) {
                // 最终排序好的BeanMethod
                Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
                // 遍历ASM找的Bean方法,因为它是按照声明顺序的
                for (MethodMetadata asmMethod : asmMethods) {
                    // 遍历类中所有的BeanMethod,这个是无序的
                    for (MethodMetadata beanMethod : beanMethods) {
                        // 如果是同一个方法
                        if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
                            // 则保存下来,这个使用selectedMethods中的方法就会按照asm中的顺序来保存了
                            selectedMethods.add(beanMethod);
                            break;
                        }
                    }
                }
                // 如果两种获取的方法数量是一致的,则将排好序的beanMethods返回
                // 如果两个方法个数不一致,那么排序也排的没有用
                if (selectedMethods.size() == beanMethods.size()) {
                    beanMethods = selectedMethods;
                }
            }
        }
        // 只有一个方法,就不需要排序,或者已经排序好的
        return beanMethods;
    }
    // 遍历所有的方法
    for (MethodMetadata methodMetadata : beanMethods) {
        // 将方法封装成BeanMethod对象,保存到ConfigClass中
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // 处理Bean中,实现的接口中的默认方法中含有的@Bean方法
    // 注意: 接口中的默认方法存在@Bean,则实现类也会有这个,到时会出现相同BeanMethod
    processInterfaces(configClass, sourceClass){
        // 遍历当前配置类实现的所有接口
        for (SourceClass ifc : sourceClass.getInterfaces()) {
            // 解析接口中存在的@Bean注解
            Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(ifc);
            // 遍历所有的方法
            for (MethodMetadata methodMetadata : beanMethods) {
                // 方法不能是抽象的
                if (!methodMetadata.isAbstract()) {
                // 将方法封装成BeanMethod对象,保存到ConfigClass中
                    configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
                }
            }
            // 递归解析接口中实现的接口
            processInterfaces(configClass, ifc);
        }
    }

    // 最后,解析当前配置类所有的父类
    if(sourceClass.getMetadata().hasSuperClass()){
        // 获取父类名称
        String superclass = sourceClass.getMetadata().getSuperClassName();
        // 如果存在父类,并且父类不是java的,同时这个父类没有被处理
        if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) {
            // 保存该父类
            this.knownSuperclasses.put(superclass, configClass);
            // 获取父类进行下一次解析
            return sourceClass.getSuperClass();
        }
    }
    // 没有父类 -> 处理完成
    return null;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值