SpringBoot自动配置原理梳理

SpringBoot自动配置原理梳理

近期想梳理一下SpringBoot的自动配置原理,于是决定从SpringBoot相关的配置注解开始梳理,因为这些注解在自动配置中都会用到,所以一步步梳理。
如发现那里有不对的地方希望大佬们指出,我这个小菜鸡继续学习qql。

原创不易,给位需要转载的话可以附上原文链接!!!

1.SpringBoot的特点

  • 依赖管理

    • 父项目作为依赖

      依赖管理    
      <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>2.3.4.RELEASE</version>
      </parent>
      
      他的父项目
       <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-dependencies</artifactId>
          <version>2.3.4.RELEASE</version>
        </parent>
      
      几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
      
    • 开发导入starter场景启动器

      1、见到很多 spring-boot-starter- :就某种场景
      2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
      3、SpringBoot所有支持的场景
      4、见到的  *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
      5、所有场景启动器最底层的依赖
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.5.2.RELEASE</version>
        <scope>compile</scope>
      </dependency>
      
    • 无需关注版本号,自动版本仲裁

      1、引入依赖默认都可以不写版本
      2、引入非版本仲裁的jar,要写版本号。
      
    • 可以修改默认版本号

      1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
      2、在当前项目里面重写配置
          <properties>
              <mysql.version>5.1.43</mysql.version>
          </properties>
      
      
  • 自动配置

    • 例如导入了springboot的web的stater的模块里面就会自动配置好tomcat和webmvc的相关模块

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0mCrMOb-1631089588588)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908111751313.png)]在这里插入图片描述

    • 自动配好SpringMVC

      • 引入SpringMVC全套组件
      • 自动配好SpringMVC常用组件(功能)
    • 自动配好Web常见功能,如:字符编码问题

      • SpringBoot帮我们配置好了所有web开发的常见场景
    • 默认的包结构

      • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
      • 无需以前的包扫描配置
      • 想要改变扫描路径,@SpringBootApplication(scanBasePackages=“com.kele”)
      • 或者@ComponentScan 指定扫描路径
    @SpringBootApplication
    等同于
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan("com.kele")
    
    • 各种配置拥有默认值

      • 默认配置最终都是映射到某个类上,如:MultipartProperties
      • 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
    • 按需加载所有自动配置项

      • 非常多的starter
      • 引入了哪些场景这个场景的自动配置才会开启
      • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面

2.SpringBoot的配置注解

  • Configuration+Bean

    @Configuration 告诉Spring这是一个配置类 并且也将这个配置类作为组件注册到了容器中
    @bean 给容器中配置组件,并且是单实例的
    

    在启动类中可以测试输出一下

    MyConfig代码

    @Configuration
    public class MyConfig {
    
      
        @Bean//bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数
        public User user01(){
            return new User("kele",22,animal());
        }
    
        @Bean
        public Pet animal(){
    
            return new Pet("dog");
        }
    
    /**
     * 查看容器中有没有组件
     *
     */
    String[] names = run.getBeanDefinitionNames();
    
    
    MyConfig myConfig = run.getBean(MyConfig.class);
    System.out.println(myConfig);
    
    for (String name : names) {
        System.out.println("==========="+":"+name);
    }
    //看我们注册的组件有没有输出
    myConfig  user01  animal
    
    
    • Configuration里面有需要注意的地方

      @Configuration(proxyBeanMethods = true)
      proxyBeanMethods:代理bean的方法
            Full(全量级的配置意思就是说,若容器中存在要调用的对象那么永远都会调用容器里面的) 只要容器中有那么就是一个对象
            lite(轻量级的配置意思就是说,不会保存容器中的对象,每次调用都是一个新的) 会自己再创建
            
       如果是全量级别配置,那么User中调用的Pet的依赖就是组件中的就是一个对象
       如果是全量级别配置,那么User中调用的Pet就不是容器中的组件了,而是新new了一个
      配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
      配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
      
      
    • 使用场景

      • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
      • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
  • Import注解也可以配置组件

    import注解里面声明了一个Class<?>[] value数组,意思就是说可以注册多个类型的组件

    @Import({User.class, DBHelper.class})
    
    代表着注入了一个User类型的组件和一个DBHelper类型的组件
        
    
  • Conditional

    代表着满足条件就注入

    例如

    @ConditionalOnMissingBean 如果容器中没有某个bean就注入
    相反
    @ConditionalOnBean如果容器中有某个bean就注入
    
    @ConditionalOnMissingBean (name = "animal")
        @Bean //bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数
        public User user01(){
    
            /**
             * 如果是全量级别配置,那么User中嗲用的Pet的依赖就是组件中的就是一个对象
             * 如果是全量级别配置,那么User中调用的Pet就不是容器中的组件了,而是新new了一个
             */
            return new User("kele",22,animal());
        }
    
    //    @Bean
        public Pet animal(){
    
            return new Pet("dog");
        }
    
  • ImportResource

    代表着导入某个资源文件,在项目中之前配置bean使用的是xml的方式现在转为注解的方式很复杂,就在可以使用这个注解,指定资源路径就可以导入配置文件注册组件到容器中

3.配置绑定

  • @ConfigurationProperties

    Car.java

    使用@Component+@ConfigurationProperties可以将配置文件中的内容和JavaBean进行注册绑定起来
    
    @Component
    @ConfigurationProperties(prefix = "mycar")
    @Data
    public class Car {
    
        private String name;
        private Double price;
    }
    

    application.properties

    mycar.name=Audi
    mycar.price=100000
    
    
  • @EnableConfigurationProperties

    @ConfigurationProperties(prefix = “mycar”)+@EnableConfigurationProperties(Car.class)使用这两个注解和JavaBean+配置类已可以做到参数的绑定

    Car.java

    @ConfigurationProperties(prefix = "mycar")
    @Data
    public class Car {
    
        private String name;
        private Double price;
    }
    

    application.properties

    mycar.name=Audi
    mycar.price=100000
    
    

    MyConfig.java

    @Configuration
    @EnableConfigurationProperties(Car.class)
    //@EnableConfigurationProperties(Car.class) 有两个作用:1.开启Car的配置绑定 2.
    public class MyConfig {
    
        @Bean //bean的 名字就是方法名,类型就是返回值类型,参数就是返回类型中的参数
        public User user01(){
            return new User("kele",22,animal());
        }
    
        @Bean
        public Pet animal(){
    
            return new Pet("dog");
        }
    
    }
    

4.自动配置原理

4.1启动类入口
//从启动类入口进去我们可以可以看到
//    @SpringBootApplication这个注解其实就是下面三个注解的结合
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
4.1.1@SpringBootConfiguration

点进去可以看到这个注解里面也就是@Configuration。代表当前是一个配置类

4.1.2@ComponentScan

指定扫描哪些,Spring注解;

4.1.3@EnableAutoConfiguration
//点进去看他也是一个合成注解

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
  • @AutoConfigurationPackage
//点进去可以看到
@Import({Registrar.class})
这个Import就是给容器注入组件
//他不是诸如一个他是利用Registrar给容器批量注入组件
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }      
//点进去可以看到registerBeanDefinitions这个方法的参数,第一个参数代表@AutoConfigurationPackage这注解打在哪里,由于是一个集成注解所以最后这个@AutoConfigurationPackage打在了启动类上所以new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]),这一行代码就是拿到这个注解所打在的类上的包信息,将该包下所有的组件注册到容器里面来

  • @Import({AutoConfigurationImportSelector.class})

    已断点的方式看这个AutoConfigurationImportSelector.class怎么走的

    点进去在

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9U3EORjK-1631089588590)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908143703416.png)]在这里插入图片描述

    看到走到了configurations = this.removeDuplicates(configurations);

    可以看到它这个集合的长度是131

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrVm0DEL-1631089588591)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908143942101.png)]在这里插入图片描述

    这个131就代表着,SpringBoot启动就要加载的组件个数

    1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
    2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
        步入这个方法可以看到走了这一行代码
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    我们再点进去SpringFactoriesLoader
        看到public final class SpringFactoriesLoader {
        public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
            主要是这里:"META-INF/spring.factories" 意思就是说会去这个位置去加载文件
    3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
    4、从META-INF/spring.factories位置来加载一个文件。
    	默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
      spring-boot-autoconfigure-2.5.4.jar包里面也有META-INF/spring.factories
        
    

    其实这个131个的组件是在spring-boot-autoconfigure-2.5.4.jar这个springboot自动配置jar包中META-INF/spring.factories写死了spring boot已启动需要自动加载的所有配置类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-05lsyGYC-1631089588592)(C:\Users\tssh\AppData\Roaming\Typora\typora-user-images\image-20210908144930618.png)]在这里插入图片描述

    它肯定不会全部加载所以就有了SpringBoot按需配置

    按需开启自动配置项

4.1.4按需开启自动配置项

这里就要用到之前的@Conditional这个注解

虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。
    

举例说明

  • 去spring-boot-autoconfigure-2.5.4这个SpringBoot自动配置类中举例说明里面有很多xxxAutoConfiguration

  • 举例aop AopAutoConfiguration

    首先在类上我们可以看到

    • @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
    • @ConditionalOnProperty( prefix = “spring.aop”,name = {“auto”}, havingValue = “true”,matchIfMissing = true) 这个条件匹配的注解就是看配置文件的参数有没有==“spring.aop”==等,matchIfMissing = true这个参数代表就算没有我也认为你有,相当于默认开启

    如果类上的判断条件没有过那么类里面的就可以不用看了

    在类里面的东西,例如这个ClassProxyingConfiguration内部类

    • @Configuration(proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
    • @ConditionalOnMissingClass({“org.aspectj.weaver.Advice”})这个条件匹配的意思就是就是看有没有org.aspectj.weaver.Advice这个类,如果没有就生效
    • @ConditionalOnProperty(prefix = “spring.aop”,name = {“proxy-target-class”},havingValue = “true”,matchIfMissing = true )这个条件匹配的注解就是看配置文件的参数有没有==“spring.aop”==等,matchIfMissing = true这个参数代表就算没有我也认为你有,相当于默认开启
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnProperty(
        prefix = "spring.aop",
        name = {"auto"},
        havingValue = "true",
        matchIfMissing = true
    )
    public class AopAutoConfiguration {
        public AopAutoConfiguration() {
        }
    
        @Configuration(
            proxyBeanMethods = false
        )
        @ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
        @ConditionalOnProperty(
            prefix = "spring.aop",
            name = {"proxy-target-class"},
            havingValue = "true",
            matchIfMissing = true
        )
        static class ClassProxyingConfiguration {
            ClassProxyingConfiguration() {
            }
    
            @Bean
            static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
                return (beanFactory) -> {
                    if (beanFactory instanceof BeanDefinitionRegistry) {
                        BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory;
                        AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                        AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                    }
    
                };
            }
        }
    
        @Configuration(
            proxyBeanMethods = false
        )
        @ConditionalOnClass({Advice.class})
        static class AspectJAutoProxyingConfiguration {
            AspectJAutoProxyingConfiguration() {
            }
    
            @Configuration(
                proxyBeanMethods = false
            )
            @EnableAspectJAutoProxy(
                proxyTargetClass = true
            )
            @ConditionalOnProperty(
                prefix = "spring.aop",
                name = {"proxy-target-class"},
                havingValue = "true",
                matchIfMissing = true
            )
            static class CglibAutoProxyConfiguration {
                CglibAutoProxyConfiguration() {
                }
            }
    
            @Configuration(
                proxyBeanMethods = false
            )
            @EnableAspectJAutoProxy(
                proxyTargetClass = false
            )
            @ConditionalOnProperty(
                prefix = "spring.aop",
                name = {"proxy-target-class"},
                havingValue = "false"
            )
            static class JdkDynamicAutoProxyConfiguration {
                JdkDynamicAutoProxyConfiguration() {
                }
            }
        }
    }
    
  • 举例web模块 DispatcherServletAutoConfiguration

    一样的首先看类

    • @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
    • @ConditionalOnWebApplication(type = Type.SERVLET)这个注解是说web容器是什么类型,spring5.2支持了一个新的技术叫响应式编程也就是WebFlux,我们导入的原生的Web模块并且这里使用的springMVC所以肯定是原生的servlet容器
    • @ConditionalOnClass({DispatcherServlet.class})这是条件是有没有DispatcherServlet,既然引入了web依赖那么肯定会有的,因为springboot-web已经集成了webMVC
    • 对于@AutoConfigureAfter:在什么之后,@AutoConfigureOrder:配置顺序,这个不做条件

    再看内部 DispatcherServletConfiguration

    • @Configuration(proxyBeanMethods = false) 代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
    • @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class})这个条件就是看里面有没有这个类
    • @ConditionalOnClass({ServletRegistration.class})同上
    • @EnableConfigurationProperties({WebMvcProperties.class})这个注解就是上面提到的,配置文件绑定

    再看这个内部类DispatcherServletConfiguration的内部

    • @Bean( name = {“dispatcherServlet”})这个就是注册一个名字叫dispatcherServlet的组件

    我们看这个方法他做了什么,它就是new了一个DispatcherServlet对象,并给它的属性进行了初始化值,最后将这个对象返回,这也就证明了,为什么我们没有写springMVC的配置文件但是springMVC是可以用的。

    @AutoConfigureOrder(-2147483648)
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @ConditionalOnClass({DispatcherServlet.class})
    @AutoConfigureAfter({ServletWebServerFactoryAutoConfiguration.class})
    public class DispatcherServletAutoConfiguration {
        public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
        public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
    
        public DispatcherServletAutoConfiguration() {
        }
    
        @Configuration(
            proxyBeanMethods = false
        )
        @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class})
        @ConditionalOnClass({ServletRegistration.class})
        @EnableConfigurationProperties({WebMvcProperties.class})
        protected static class DispatcherServletConfiguration {
            protected DispatcherServletConfiguration() {
            }
            @Bean(
                name = {"dispatcherServlet"}
            )
            public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
                DispatcherServlet dispatcherServlet = new DispatcherServlet();
                dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
                dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
                dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
                dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
                dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
                return dispatcherServlet;
            }
            @Bean
            @ConditionalOnBean({MultipartResolver.class})
            @ConditionalOnMissingBean(
                name = {"multipartResolver"}
            )
            public MultipartResolver multipartResolver(MultipartResolver resolver) {
                return resolver;
            }
        }
    }
    
    
4.1.5修改默认配置

以web模块中DispatcherServlet中的MultipartResolver文件上传解析器为例,这个东西非常有趣

        @Bean
		@ConditionalOnBean(MultipartResolver.class)  //容器中有这个类型组件
		@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
		public MultipartResolver multipartResolver(MultipartResolver resolver) {
            //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
            //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
			// Detect if the user has created a MultipartResolver but named it incorrectly
			return resolver;
		}
给容器中加入了文件上传解析器;

5.SpringBoot配置文件生效的原理

我们在springboot中经常去application.properties中配置一些属性,那么这些配置文件是怎么生效的呢,我现在以字符编码解析器为例

  • HttpEncodingAutoConfiguration 以这个自动配置类分析

    这个只需要看一些关键点就行

    同样的先分析类上的注解

    • @Configuration( proxyBeanMethods = false)代表它是一个配置类,proxyBeanMethods = false一开始有说过这是轻量配置
    • @ConditionalOnWebApplication(type = Type.SERVLET)这个注解是说web容器是什么类型,spring5.2支持了一个新的技术叫响应式编程也就是WebFlux,我们导入的原生的Web模块并且这里使用的springMVC所以肯定是原生的servlet容器
    • @EnableConfigurationProperties({ServerProperties.class})这个注解可以将配置文件中的内容和JavaBean进行注册绑定起来@ConditionalOnClass({CharacterEncodingFilter.class})这个条件判断就是看有没有CharacterEncodingFilter这个类
    • @ConditionalOnProperty(prefix = “server.servlet.encoding”, value = {“enabled”},matchIfMissing = true)

    看到ServerProperties.class可以看到配置文件跟这个类绑定了,证明这个类的属性就是我们在配置文件设置的属性,具体头部是怎么开始的呢

    我们看到这里prefix = “server.servlet.encoding”,证明我们在配置文件中以这个开始,设置属性就是ServerProperties.class中的属性名称

    这样就做到了我们修改的配置文件,springboot是如何生效的

    • 还有个注意点

      @ConditionalOnMissingBean这个条件如果没有配置组件,那么SpringBoot才会给你配置

      所以应证了一句话:SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先

      //SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
          @Bean
          @ConditionalOnMissingBean
          public CharacterEncodingFilter characterEncodingFilter() {}
      
    @Configuration(
        proxyBeanMethods = false
    )
    @EnableConfigurationProperties({ServerProperties.class})
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @ConditionalOnClass({CharacterEncodingFilter.class})
    @ConditionalOnProperty(
        prefix = "server.servlet.encoding",
        value = {"enabled"},
        matchIfMissing = true
    )
    public class HttpEncodingAutoConfiguration {
        private final Encoding properties;
    
        public HttpEncodingAutoConfiguration(ServerProperties properties) {
            this.properties = properties.getServlet().getEncoding();
        }
    
        @Bean
        @ConditionalOnMissingBean
        public CharacterEncodingFilter characterEncodingFilter() {
            CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
            filter.setEncoding(this.properties.getCharset().name());
            filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
            filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
            return filter;
        }
    

总结

这里总结我感觉我总结的不是很好,于是引用了雷丰阳老师的总结,我觉得总结非常到位

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值