SpringBoot源码解析(十八)自动配置

前几篇文章聊完了prepareContext方法,本文继续SpringApplication#run方法的下一行:refreshContext
在这里插入图片描述
这是Spring容器启动过程最重要的一个步骤,所有的扫描、解析、注入等等都在这里面完成的,当然本文依然是把焦点聚集在SpringBoot上,它的自动配置,也是在容器refresh的过程中执行的

作为SpringBoot项目,启动类会加一个注解@SpringBootApplication,我们从这个注解开始入手,看它都做了哪些初始化,怎么整合到容器的启动流程并完成自动配置

首先看下这个注解的内部结构

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

其中组合了一个@EnableAutoConfiguration注解,顾名思义,它会用来完成自动配置,再看下它的定义

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {

这个注解通过@Import导入了一个类AutoConfigurationImportSelector,我们看下这个类的继承结构,它实现了DeferredImportSelector接口,这个接口扩展自ImportSelector
在这里插入图片描述
再简单看下DeferredImportSelector的定义,它有一个getImportGroup方法,并且有一个内部接口Group,
这个Group用于对Selector分组,不同的Group可以实现不同的配置方式,SpringBoot导入的AutoConfigurationImportSelector内部也会定义一个自己的Group,通过其getImportGroup方法返回,我们后续会看到

public interface DeferredImportSelector extends ImportSelector {
    @Nullable
    default Class<? extends DeferredImportSelector.Group> getImportGroup() {
        return null;
    }

    public interface Group {
		......
		......
	}
}

Spring对于@Import导入类的处理,一般有三种类型,实现ImportSelector接口、实现ImportBeanDefinitionRegistrar接口,以及普通类,这个算是Spring源码的范畴,对每种类型的大致处理方式可以参考下图,这里不再详细展开
在这里插入图片描述
而这里的DeferredImportSelector类型虽然扩展自ImportSelector,但是它比较特殊,它的执行方式不同于ImportSelector,会延迟执行,等其它@Import全部完成后才会工作,而且调用的方法和导入ImportSelector也不一样

具体处理@Import的位置是ConfigurationClassParser的parse方法,调用栈就不详细跟了,是从ConfigurationClassPostProcessor的postProcessorBeanDefinitionRegistry方法中触发的,这个类是Spirng初始化流程非常关键的一个类,我们前面的文章也提到过几次,看下这个parse方法

    public void parse(Set<BeanDefinitionHolder> configCandidates) {
        Iterator var2 = configCandidates.iterator();

        while(var2.hasNext()) {
            BeanDefinitionHolder holder = (BeanDefinitionHolder)var2.next();
            BeanDefinition bd = holder.getBeanDefinition();

            try {
                if (bd instanceof AnnotatedBeanDefinition) {
                    this.parse(((AnnotatedBeanDefinition)bd).getMetadata(), holder.getBeanName());
                } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition)bd).hasBeanClass()) {
                    this.parse(((AbstractBeanDefinition)bd).getBeanClass(), holder.getBeanName());
                } else {
                    this.parse(bd.getBeanClassName(), holder.getBeanName());
                }
            } catch (BeanDefinitionStoreException var6) {
                throw var6;
            } catch (Throwable var7) {
                throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd.getBeanClassName() + "]", var7);
            }
        }

        this.deferredImportSelectorHandler.process();
    }

在try模块中,我们通过注解@Import导入的类,会走第一个分支,调用this.parse开始解析@Import的内容,完成相关类的导入
在方法的最后,又调用了this.deferredImportSelectorHandler.process(),看名字它肯定跟我们上面导入的DeferredImportSelector有关

实际上在调用this.parse的时候,如果发现当前类实现了DeferredImportSelector接口,并不会真正处理,而是把它存储到一个集合中,最后等其它类执行完毕后,再通过这个集合,处理DeferredImportSelector的导入

具体看下这个过程,parse最终会调用当前类的processImports方法

    private void processImports(ConfigurationClass configClass, ConfigurationClassParser.SourceClass currentSourceClass, Collection<ConfigurationClassParser.SourceClass> importCandidates, boolean checkForCircularImports) {
        ......
        ......
        if (candidate.isAssignable(ImportSelector.class)) {
              candidateClass = candidate.loadClass();
              ImportSelector selector = (ImportSelector)BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
              ParserStrategyUtils.invokeAwareMethods(selector, this.environment, this.resourceLoader, this.registry);
              if (selector instanceof DeferredImportSelector) {
                  this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector)selector);
              } else {
                  String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                  Collection<ConfigurationClassParser.SourceClass> importSourceClasses = this.asSourceClasses(importClassNames);
                  this.processImports(configClass, currentSourceClass, importSourceClasses, false);
              }
         } 
         ......
         ......
    }

在处理ImportSelector的分支中,如果发现导入的类是DeferredImportSelector类型,则调用 this.deferredImportSelectorHandler.handle方法,将其封装成一个Holder,存储到List中

    private class DeferredImportSelectorHandler {
        @Nullable
        private List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImportSelectors;

        private DeferredImportSelectorHandler() {
            this.deferredImportSelectors = new ArrayList();
        }

        public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
            ConfigurationClassParser.DeferredImportSelectorHolder holder = new ConfigurationClassParser.DeferredImportSelectorHolder(configClass, importSelector);
            ......
            this.deferredImportSelectors.add(holder);
            ......
        }
        
        public void process() {
            List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
            this.deferredImportSelectors = null;

            try {
                if (deferredImports != null) {
                    ConfigurationClassParser.DeferredImportSelectorGroupingHandler handler = ConfigurationClassParser.this.new DeferredImportSelectorGroupingHandler();
                    deferredImports.sort(ConfigurationClassParser.DEFERRED_IMPORT_COMPARATOR);
                    deferredImports.forEach(handler::register);
                    handler.processGroupImports();
                }
            } finally {
                this.deferredImportSelectors = new ArrayList();
            }

        }
    }    

DeferredImportSelectorHandler 内部相当于存储了一个Holder列表,每个Hodler中存储了DeferredImportSelector以及它所属的配置类
在这里插入图片描述

回到parse方法,其它的导入类执行完毕后,调用deferredImportSelectorHandler.process方法

	......
	......
    public void process() {
        List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
        this.deferredImportSelectors = null;

        try {
            if (deferredImports != null) {
                ConfigurationClassParser.DeferredImportSelectorGroupingHandler handler = ConfigurationClassParser.this.new DeferredImportSelectorGroupingHandler();
                deferredImports.sort(ConfigurationClassParser.DEFERRED_IMPORT_COMPARATOR);
                deferredImports.forEach(handler::register);
                handler.processGroupImports();
            }
        } finally {
            this.deferredImportSelectors = new ArrayList();
        }

    }
    ......
    ......

在这个方法中,先创建了一个DeferredImportSelectorGroupingHandler,然后循环所有的Holder,传递给它的register方法

    public void register(ConfigurationClassParser.DeferredImportSelectorHolder deferredImport) {
        Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
        ConfigurationClassParser.DeferredImportSelectorGrouping grouping = (ConfigurationClassParser.DeferredImportSelectorGrouping)this.groupings.computeIfAbsent(group != null ? group : deferredImport, (key) -> {
            return new ConfigurationClassParser.DeferredImportSelectorGrouping(this.createGroup(group));
        });
        grouping.add(deferredImport);
        this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getConfigurationClass());
    }

register方法主要做了几件事情

  1. 获取DeferredImportSelector的Group,每个DeferredImportSelector会实现一个getImportGroup方法,返回自己所属的Group
  2. 将Group添加到groupingHandler的Map中,value为封装后 的Grouping,包含Group和对应的Hodler列表
  3. 将配置类元信息添加到GroupingHandler中(key为配置类的metaData,value为配置类)

DeferredImportSelectorGroupingHandler存储结构
在这里插入图片描述

全部注册完毕后,调用handler.processGroupImports

    public void processGroupImports() {
        Iterator var1 = this.groupings.values().iterator();

        while(var1.hasNext()) {
            ConfigurationClassParser.DeferredImportSelectorGrouping grouping = (ConfigurationClassParser.DeferredImportSelectorGrouping)var1.next();
            grouping.getImports().forEach((entry) -> {
                ConfigurationClass configurationClass = (ConfigurationClass)this.configurationClasses.get(entry.getMetadata());

                try {
                    ConfigurationClassParser.this.processImports(configurationClass, ConfigurationClassParser.this.asSourceClass(configurationClass), ConfigurationClassParser.this.asSourceClasses(entry.getImportClassName()), false);
                } catch (BeanDefinitionStoreException var4) {
                    throw var4;
                } catch (Throwable var5) {
                    throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" + configurationClass.getMetadata().getClassName() + "]", var5);
                }
            });
        }

    }

首先循环内部的grouping,调用getImports方法,对返回的结果再进行forEach循环处理,先看getImports方法

    public Iterable<Entry> getImports() {
        Iterator var1 = this.deferredImports.iterator();

        while(var1.hasNext()) {
            ConfigurationClassParser.DeferredImportSelectorHolder deferredImport = (ConfigurationClassParser.DeferredImportSelectorHolder)var1.next();
            this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());
        }
        return this.group.selectImports();
    }

这个方法又会循环内部的Holder列表,调用this.group.process方法,这里Group的具体实现是AutoConfigurationGroup,它是SpringBoot导入的AutoConfigurationImportSelector的内部类,看其process方法

    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
        Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {
            return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());
        });
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(this.getAutoConfigurationMetadata(), annotationMetadata);
        this.autoConfigurationEntries.add(autoConfigurationEntry);
        Iterator var4 = autoConfigurationEntry.getConfigurations().iterator();

        while(var4.hasNext()) {
            String importClassName = (String)var4.next();
            this.entries.putIfAbsent(importClassName, annotationMetadata);
        }

    }

这个方法会寻找所有需要自动配置的类,进入getAutoConfigurationEntry方法

    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

首先判断是否开启了自动配置,如果spring.boot.enableautoconfiguration属性设置为false,就返回一个空结果,默认是开启的

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }

然后进入自动配置流程,首先调用getCandidateConfigurations方法到META-INF/spring.factories中加载需要自动配置的列表

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

调用了SpringFactoriesLoader.loadFactoryNames方法,这里它要找的类型,通过方法this.getSpringFactoriesLoaderFactoryClass()返回,即EnableAutoConfiguration类型

    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }

找到自动配置的列表后,需要做一个过滤,只留下符合当前环境,真正需要完成自动配置的类
过滤流程分两个步骤,第一步先筛除我们手动配置的过滤项,第二步再根据系统配置的过滤器来过滤,先看第一步,调用getExclusions方法

    protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        Set<String> excluded = new LinkedHashSet();
        excluded.addAll(this.asList(attributes, "exclude"));
        excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
        excluded.addAll(this.getExcludeAutoConfigurationsProperty());
        return excluded;
    }

首先根据配置类上的exclude和excludeName来过滤,然后调用getExcludeAutoConfigurationsProperty方法,看我们是否通过配置属性spring.autoconfigure.exclude来指定过滤项

    private List<String> getExcludeAutoConfigurationsProperty() {
        if (this.getEnvironment() instanceof ConfigurableEnvironment) {
            Binder binder = Binder.get(this.getEnvironment());
            return (List)binder.bind("spring.autoconfigure.exclude", String[].class).map(Arrays::asList).orElse(Collections.emptyList());
        } else {
            String[] excludes = (String[])this.getEnvironment().getProperty("spring.autoconfigure.exclude", String[].class);
            return excludes != null ? Arrays.asList(excludes) : Collections.emptyList();
        }
    }

处理完手动配置的过滤项后,回到getAutoConfigurationEntry方法,继续调用filter方法完成系统过滤

private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
        long startTime = System.nanoTime();
        String[] candidates = StringUtils.toStringArray(configurations);
        boolean[] skip = new boolean[candidates.length];
        boolean skipped = false;
        Iterator var8 = this.getAutoConfigurationImportFilters().iterator();

        while(var8.hasNext()) {
            AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var8.next();
            this.invokeAwareMethods(filter);
            boolean[] match = filter.match(candidates, autoConfigurationMetadata);

            for(int i = 0; i < match.length; ++i) {
                if (!match[i]) {
                    skip[i] = true;
                    candidates[i] = null;
                    skipped = true;
                }
            }
        }

        if (!skipped) {
            return configurations;
        } else {
            List<String> result = new ArrayList(candidates.length);

            int numberFiltered;
            for(numberFiltered = 0; numberFiltered < candidates.length; ++numberFiltered) {
                if (!skip[numberFiltered]) {
                    result.add(candidates[numberFiltered]);
                }
            }

            if (logger.isTraceEnabled()) {
                numberFiltered = configurations.size() - result.size();
                logger.trace("Filtered " + numberFiltered + " auto configuration class in " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
            }

            return new ArrayList(result);
        }
    }

该方法先通过getAutoConfigurationImportFilters加载系统配置的过滤器,也就是到META-INF/spring.factories中找AutoConfigurationImportFilter

    protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
        return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
    }

最终在spring-boot-autoconfigure包下找到3个,这几个名字我们应该很熟悉了,在自动配置的类中,会添加一些条件注解,比如@ConditionalOnBean 等,根据当前容器中是否存在某个Bean,类决定当前类是否装配到容器中

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

最后通过这几个Filter,筛选出最终需要装配到容器中的类

这个遍历的过程结束后,回到getImports方法,调用最后一行this.group.selectImports()

        public Iterable<Entry> getImports() {
            Iterator var1 = this.deferredImports.iterator();

            while(var1.hasNext()) {
                ConfigurationClassParser.DeferredImportSelectorHolder deferredImport = (ConfigurationClassParser.DeferredImportSelectorHolder)var1.next();
                this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());
            }

            return this.group.selectImports();
        }

具体实现同样在AutoConfigurationImportSelector中,它会将需要自动配置的类封装成一个Entry返回

    public Iterable<Entry> selectImports() {
        if (this.autoConfigurationEntries.isEmpty()) {
            return Collections.emptyList();
        } else {
            Set<String> allExclusions = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
            Set<String> processedConfigurations = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
            processedConfigurations.removeAll(allExclusions);
            return (Iterable)this.sortAutoConfigurations(processedConfigurations, this.getAutoConfigurationMetadata()).stream().map((importClassName) -> {
                return new Entry((AnnotationMetadata)this.entries.get(importClassName), importClassName);
            }).collect(Collectors.toList());
         }
    }

回到processGroupImports方法,遍历上一步封装的Entry,调用processImports方法,完成类的导入,这个过程跟Spring处理@Import是完全一样的

    public void processGroupImports() {
        Iterator var1 = this.groupings.values().iterator();

        while(var1.hasNext()) {
            ConfigurationClassParser.DeferredImportSelectorGrouping grouping = (ConfigurationClassParser.DeferredImportSelectorGrouping)var1.next();
            grouping.getImports().forEach((entry) -> {
                ConfigurationClass configurationClass = (ConfigurationClass)this.configurationClasses.get(entry.getMetadata());

                try {
                    ConfigurationClassParser.this.processImports(configurationClass, ConfigurationClassParser.this.asSourceClass(configurationClass), ConfigurationClassParser.this.asSourceClasses(entry.getImportClassName()), false);
                } catch (BeanDefinitionStoreException var4) {
                    throw var4;
                } catch (Throwable var5) {
                    throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" + configurationClass.getMetadata().getClassName() + "]", var5);
                }
            });
        }
    }

至此,SpringBoot就完成了自动配置,我们常见的各种starter,其实就是在其META-INF/spring.factories中配置自定义的EnableAutoConfiguration,在对应的类中添加条件注解,然后通过上面的一些列流程,加载到了Spring容器中

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Springboot源码解析PDF是一本深入解析Springboot框架的技术书籍,涵盖了Springboot的背景、原理、设计思路、运行机制、开发实践等方面。全书主要分为三部分:第一部分介绍Springboot的基础知识,包括Spring框架的常用注解、Springboot配置自动配置原理等;第二部分深入探讨Springboot的主要功能,如数据访问、Web开发、缓存、消息、安全等;第三部分着重介绍开发Springboot应用的最佳实践,包括Springboot与其他框架的结合使用、集成测试、监控与诊断等。 阅读Springboot源码解析PDF可以让开发者更深入理解Springboot的设计理念、技术实现以及应用场景,在实际项目开发中更加灵活、高效地使用Springboot。该书对于有一定JavaSpring框架基础的开发者来说是一本非常优秀的参考资料,也是Java开发者必不可少的技术读物。同时,该书也是借助源码解析的方式,让读者更加系统化地学习Springboot技术,具有很高的实用性和参考价值。总之,阅读Springboot源码解析PDF有助于开发者更好地掌握Springboot技术,提高应用开发效率和代码质量。 ### 回答2: Spring Boot源码解析pdf是一本介绍Spring Boot框架的开源书籍,该书的目的是帮助开发者深入了解Spring Boot的内部工作原理和实现细节。 该书首先从Spring Boot框架的源码结构和核心模块入手,详细介绍了Spring Boot的MVC、ORM、缓存、安全等核心功能的实现原理。同时,该书还介绍了Spring Boot对微服务的支持和整合Spring Cloud的方式,让开发者更深入了解Spring Boot在分布式架构中的应用。 在讲解源码实现原理的同时,该书还指出了一些常见的开发问题和易错点,并提供了相应的解决方案。此外,该书还通过一系列的案例,全面展示了Spring Boot的实际开发应用场景,帮助开发者更好地应用Spring Boot框架。 总的来说,Spring Boot源码解析pdf是一本非常实用的书籍,能够帮助开发者快速掌握Spring Boot框架的内部实现原理,提高开发效率和代码质量。同时,该书还可以作为学习Spring Boot的参考资料,对于想要深入学习和研究Spring Boot的开发者来说是非常有用的。 ### 回答3: Spring Boot 是一个很受欢迎的 Java 框架,它简化了开发者的工作,允许他们更快速地构建和部署应用程序。Spring Boot 的优点包括简洁的配置、内嵌的 Web 容器和现成的插件,让开发者可以更专注于业务逻辑。Spring Boot源码解析是学习它的关键。 Spring Boot源码解析Java初学者来说可能会比较复杂,但是它对于学习框架和原理是非常有益的。一个好的 Spring Boot 项目需要基于很好的基础,这就要求开发者了解其源码源码解析可以帮助开发者了解各种设计模式和组件的原理,有助于解决实际问题。 在 Spring Boot源码解析中,我们将会找到很多有用的信息。例如,我们可以看到 Spring Boot 如何通过注解处理器创建并配置 bean,这可以帮助我们更好地理解依赖注入和 IoC 容器。此外,我们还可以了解 Spring Boot 如何处理 HTTP 请求、创建模板、处理安全性等。这些知识可以帮助我们更好地理解 Spring Boot 内部的工作机制。 总之,Spring Boot源码解析是必不可少的一部分,它可以帮助开发者更好地了解和使用该框架,掌握在实践中所需的知识和技能。如果您是一名 Spring 开发者,那么深入了解 Spring Boot源码将会是一个很好的学习过程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值