【SpringBoot】 — 2 - 配置文件

🔍 配置文件


1. 配置文件

SpringBoot 使用一个全局的配置文件,配置文件名是固定的:

  • application.properties

    语法结构 :key = value

  • application.yml

    语法结构 :key:空格 value

📜 标记语言:

  • 以前的配置文件;大多都使用的是 xxxx.xml 文件;

  • YAML:以数据为中心,比 json、xml 等更适合做配置文件;

💬 比如我们可以在配置文件中修改 Tomcat 默认启动的端口号

YAML:

server:
  port: 8081

properties:

server.port=8081

2. YAML 语法

① 基本语法

k: v:表示一对键值对(空格必须有)

空格的缩进来控制层级关系,只要是左对齐的一列数据,都是同一个层级的

server:
    port: 8081
    path: /hello

属性和值的大小写都是十分敏感的

② 值的写法

Ⅰ 字面量:普通的值(数字,字符串,布尔)

字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;

k: v
  • " ":双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思

    💬 例如:

    name: "zhangsan \n lisi"
    

    输出:

    zhangsan
    lisi
    
  • ' ':单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据

    💬 例如:

    name: ‘zhangsan \n lisi’
    

    输出

    zhangsan \n lisi
    
Ⅱ Map(属性和值)(键值对)

k: v:在下一行来写对象的属性和值的关系,注意缩进

#对象、Map格式
k: 
    v1:
    v2:

💬 例如:

friends:
	lastName: zhangsan
	age: 20

行内写法:

friends: {lastName: zhangsan,age: 18}
Ⅲ 数组(List、Set)

- 值 表示数组中的一个元素

💬 例如:

pets:
  - cat
  - dog
  - pig

行内写法

pets: [cat,dog,pig]

3. @ConfigurationProperties 全局配置文件值注入

① 全局配置文件值注入

📜 需要导入配置文件处理器的依赖,配置文件进行绑定就会有提示

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-configuration-processor</artifactId>
 <optional>true</optional>
</dependency>

注入步骤:

  • 在 springboot 项目中的 resources 目录下新建一个文件 application.yml
  • 编写实体类

配置文件 yaml 格式

person:
    lastName: hello
    age: 18
    boss: false
    birth: 2017/12/12
    maps: {k1: v1,k2: 12}
    lists:
      - lisi
      - zhaoliu
    dog:
      name: 小狗
      age: 12

配置文件 properties 格式

person.last-name=张三
person.age=11
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=14
person.lists=a,b,c
person.dog.name=dog
person.dog.age=15

🚨 properties 配置文件在 IDEA 中默认 utf-8 可能会乱码,如图进行相应设置:

实体类:

/**
 * 将配置文件中配置的每一个属性的值,映射到这个组件中
 */
@Component
@ConfigurationProperties(prefix = "person")
public class Person {

    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birth;

    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
	
    // 有参无参构造、get、set方法、toString()方法  
  • @ConfigurationProperties:告诉 SpringBoot 将本类中的所有属性和配置文件中相关的配置进行绑定;

    只有这个组件是容器中的组件,才能使用容器提供的 @ConfigurationProperties 功能

  • prefix = "person":与配置文件中哪个属性进行一一映射

② @Validated(JSR303 数据校验)

Ⅰ 概述

JSR303数据校验 , 就是我们可以在字段上增加一层过滤器验证 , 保证数据的合法性

使用 @Validated 注解:表示这个 JavaBean 中的属性需要校验

@Component
@ConfigurationProperties(prefix = "person")
@Validated
public class Person {

    //lastName必须是邮箱格式
    @Email
    private String lastName;
    private Integer age;
    private Boolean boss;

    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
Ⅱ 常见参数
@NotNull(message="名字不能为空")
private String userName;

@Max(value=120,message="年龄最大不能查过120")
private int age;

@Email(message="邮箱格式错误")
private String email;

// 空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.
    
// Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  
    
// 长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) string is between min and max included.

// 日期检查
@Past       验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

4. @Value 值注入

@Value 值注入是 Spring 中的方式

① @Value 值注入

可以使用 @Value 注解从配置文件中手动取值或者自由赋值,支持三种方式:

  • ${key} 从环境变量、配置文件中获取值

    @Value("${person.last-name}")
    private String lastName;
    
  • # {SpEL}

    @Value("#{11*2}")
    private Integer age;
    
  • "字面量"

    @Value("true")
    private Boolean boss;
    
@Component
public class Person {
    /**
     * <bean class="Person">
     *      <property name="lastName" value="字面量//"></property>
     * <bean/>
     */
    @Value("${person.last-name}")
    private String lastName;
    @Value("#{11*2}")
    private Integer age;
    @Value("true")
    private Boolean boss;

    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;

② @Value 和 @ConfigurationProperties 注入值比较

@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SpEL不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持

💡 松散绑定:比如 yml 中写的 last-name,这个和 lastName 是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定

无论配置文件是 yml还是 properties他们都能获取到值;

  • 如果只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用 @Value

    @RestController
    public calss HelloController{
        @Value("${person.lastName}")
        private String name;
        
        @RequestMapping("/sayHello")
        public String sayHello(){
            return "Hello" + name;
        }
    }
    
  • 如果我们专门编写了一个 JavaBean 来和配置文件进行映射,就直接使用@ConfigurationProperties

5. @PropertySource 指定配置文件值注入

@ConfigurationProperties(prefix = "person") 默认从全局配置文件中获取值,如果所有的配置全写在全局配置文件中,那么这个文件就太大了。我们可以自定义配置文件使用 @PropertySource 加载指定的配置文件

比如我们在 resources 目录下新建一个 person.properties 文件

@PropertySource(value = {"classpath:person.properties"})
@Component
public class Person {
    private String lastName;
    private Integer age;
    private Boolean boss;

6. @ImportResource / @Bean

@ImportResource:导入 Spring 的配置文件,让配置文件里面的内容生效

(Spring Boot 里面没有 Spring 的配置文件,我们自己编写的配置文件,也不能自动识别)

@ImportResource 标注在一个配置类上可以让 Spring 的配置文件生效并加载进来;

@ImportResource(locations = {"classpath:beans.xml"})
@SpringBootApplication

Spring 的配置文件 beans.xml(示例:我们创建一个 service.HelloService.java 文件)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	
    <bean id="helloService" class="com.smallbeef.springboot.service.HelloService"></bean>
</beans>

🌎 显然,这样给容器中添加组件的方法比较麻烦,SpringBoot 推荐给容器中添加组件的方式:使用全注解的方式

  • 创建一个 config.MyAppConfig.java 配置类

  • ⭐ 使用**@Bean** 给容器中添加组件

    @Configuration 表示此类是 Spring 的配置类(替代原来的 Spring 配置文件)

    /**
     * @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件
     */
    @Configuration
    public class MyAppConfig {
    
        //将方法的返回值添加到容器中;容器中这个组件默认的 id 就是方法名
        @Bean
        public HelloService helloService02(){
            System.out.println("配置类 @Bean 给容器中添加组件了...");
            return new HelloService();
        }
    }
    

##7. 配置文件占位符

① 随机数

配置文件还可以编写占位符生成随机数

  • ${random.value}
  • ${random.int}
  • ${random.long}
  • ${random.int(10)}
  • ${random.int[1024,65536]}

② 占位符获取之前配置的值

person.last-name = 张三${random.uuid}
person.age = ${random.int}
# 在 person.last-name 后面 添加 _dog 字符
person.dog.name=${person.last-name}_dog 

如果没有可以是用 : 指定默认值

person.last-name=张三${random.uuid}
# person.dog.name = hello_dog
person.dog.name=${person.hello:hello}_dog

8. Profile 多环境切换

profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境 👇

① 多配置文件

我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml

比如:application-dev.propertiesapplication-prod.properties

默认使用 application.properties 的配置

② 激活指定环境

  • ⭐ 在配置文件中指定

    spring.profiles.active=dev
    
  • 命令行:

    java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;

    可以直接在测试的时候,配置传入命令行参数

  • 虚拟机参数;

    -Dspring.profiles.active=dev

③ yml 支持多文档块方式

properties配置文件中一样,但是使用 yml去实现不需要创建多个配置文件,更加方便了~

server: 
  port: 8081 # 默认端口
#选择要激活那个环境块
spring:
  profiles:
  	# 激活
    active: prod # 使用 prod 环境,8084端口

---
server:
  port: 8083
spring:
  profiles: dev # 配置环境的名称


---
server:
  port: 8084
spring:
  profiles: prod  # 配置环境的名称

9. 配置文件加载位置

springboot 启动会扫描以下位置的 application.properties 或者 application.yml 文件作为 Spring boot 的默认配置文件

  • file:./config/ 项目路径下的 config 文件夹配置文件

  • file:./ 项目路径下配置文件

  • classpath:/config/ 资源路径下的 config 文件夹配置文件

  • classpath:/ 资源路径下配置文件

优先级由高到底,高优先级的配置会覆盖低优先级的配置;

SpringBoot 会从这四个位置全部加载主配置文件,⭐ 互补配置

📜 我们还可以通过 spring.config.location 来改变默认的配置文件位置

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;

比如我们项目打包后后我们指定配置文件的新位置为 E:/application.properties

java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=E:/application.properties

10. 自动配置原理

配置文件到底能写什么?怎么写?配置文件能配置的属性参照

① 自动配置原理 ⭐

Ⅰ 加载主配置类,开启自动配置功能

@SpringBootApplication(Spring Boot应用):标注在某个类上说明这个类是 SpringBoot 的主程序类,SpringBoot 就应该运行这个类的 main方法来启动 SpringBoot 应用;

📑 该注解源码如下:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

底层由三个注解实现:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

⭐ 自动配置第 1 步:SpringBoot 启动的时候加载主配置类(由 @SpringBootConfiguration 标识),并开启了自动配置功能 @EnableAutoConfiguration

Ⅱ 导入组件(自动配置类)

@EnableAutoConfiguration开启自动配置功能

以前我们需要配置的东西,Spring Boot 帮我们自动配置;

@EnableAutoConfiguration告诉 SpringBoot 开启自动配置功能,这样自动配置才能生效;

📑 该注解的源码如下:

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
  • @AutoConfigurationPackage:自动配置包,将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到 Spring 容器中

    📑 该注解的源码如下:

    @Import({Registrar.class})
    public @interface AutoConfigurationPackage {
    }
    
    • @Import({Registrar.class})@import是 Spring 中的底层注解,给容器中导入一个组件

    • Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到 Spring 容器 ;

  • @Import({AutoConfigurationImportSelector.class}):给容器中导入组件。

    AutoConfigurationImportSelector自动配置导入选择器。将所有需要导入的组件以全类名的方式返回;这些组件就会被添加到容器中。说白了就是导入组件

    那么它会导入哪些组件的选择器呢?📑 我们点击去这个类 AutoConfigurationImportSelector 看源码:

    这个类中有一个这样的方法:

    // 获得候选的配置
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 这里的 getSpringFactoriesLoaderFactoryClass() 方法
        // 返回的就是启动自动导入配置文件的注解类;EnableAutoConfiguration
        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 类的静态方法,我们进入SpringFactoriesLoaderloadFactoryNames() 方法:

    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        // 这里它又调用了 loadSpringFactories 方法
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }
    

    我们继续点击查看 loadSpringFactories方法:

    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        // 获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                //去获取一个资源 "META-INF/spring.factories"
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();
    
                //将读取到的资源遍历,封装成为一个Properties
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();
    
                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryClassName = ((String)entry.getKey()).trim();
                        String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;
    
                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            result.add(factoryClassName, factoryName.trim());
                        }
                    }
                }
    
                cache.put(classLoader, result);
                return result;
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
            }
        }
    }
    

    ⭐ 在上面这段源码中,我们发现了一个多次出现的文件 spring.factories,在 External Libraries 中找到它:

    其中包含了很多自动配置的文件(自动配置类 xxxAutoConfiguration),这就是自动配置的根源所在

    我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration

    可以看到这些一个个的都是JavaConfig 配置类,而且都注入了一些 Bean。

    ⭐ 自动配置第 2 步:利用 AutoConfigurationImportSelector 给容器中导入一些组件:

    • 扫描所有 jar 包类路径下 META-INF/spring.factories

    • 把扫描到的这些文件的内容包装成 properties对象

    • properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中

    总结来说,就是利用 SpringFactoriesLoader.loadFactoryNames() 将类路径下 META-INF/spring.factories 里面配置的所有 EnableAutoConfiguration 的值加入到了容器中。每一个这样的 xxxAutoConfiguration 自动配置类都是容器中的一个组件,都加入到容器中,用他们来做自动配置;

Ⅲ 举例分析自动配置原理

📑 以 HttpEncodingAutoConfiguration(Http 编码自动配置)为例解释自动配置原理:

// 表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件
@Configuration   

// 启动指定类的ConfigurationProperties功能;
// 将配置文件中对应的值和 HttpEncodingProperties 绑定起来;
// 并把HttpEncodingProperties加入到ioc容器中
@EnableConfigurationProperties(HttpEncodingProperties.class)  

// Spring底层 @Conditional 注解,根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效;
// @ConditionalOnWebApplication 判断当前应用是否是 web 应用,如果是,当前配置类生效
@ConditionalOnWebApplication

// 判断当前项目有没有这个类 CharacterEncodingFilter:SpringMVC 中进行乱码解决的过滤器;
@ConditionalOnClass(CharacterEncodingFilter.class)  

// 判断配置文件中是否存在某个配置  spring.http.encoding.enabled;如果不存在,判断也是成立的
// 即使我们配置文件中不配置 spring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)  

public class HttpEncodingAutoConfiguration {
  
  	//他已经和 SpringBoot 的配置文件映射了
  	private final HttpEncodingProperties properties;
  
   // 只有一个有参构造器的情况下,参数的值就会从容器中拿
  	public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
		this.properties = properties;
	}
  
    @Bean   //给容器中添加一个组件,这个组件的某些值需要从 properties 中获取
	@ConditionalOnMissingBean(CharacterEncodingFilter.class) // 判断容器有没有这个组件
	public CharacterEncodingFilter characterEncodingFilter() {
		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
		filter.setEncoding(this.properties.getCharset().name());
		filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
		filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
		return filter;
	}

🚩 自动配置类:根据当前不同的条件判断,决定这个配置类是否生效。

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;

  • 这些组件的属性是从对应的 properties 类中获取的,这些类里面的每一个属性又是和配置文件绑定的;

  • 所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着,配置文件能配置什么就可以参照某个功能对应的这个属性类

    📑 比如 HttpEncodingProperties

    //从配置文件中获取指定的值和 bean 的属性进行绑定
    @ConfigurationProperties(prefix = "spring.http.encoding")  
    public class HttpEncodingProperties {
    
       public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    

    我们去配置文件里面试试前缀,看看提示:

精髓:

  • SpringBoot 启动会加载大量的自动配置类

  • 我们看我们需要的功能有没有 SpringBoot 默认写好的自动配置类;

    xxxxAutoConfigurartion:自动配置类,给容器中添加组件

    xxxxProperties:封装配置文件中相关属性

  • 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了)

  • 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们就可以在配置文件中指定这些属性的值;

② @Conditional 派生注解

作用:必须是 @Conditional 指定的条件成立,才给容器中添加组件,配置文件里面的所有内容才生效;

@Conditional扩展注解作用(判断是否满足当前指定条件)
@ConditionalOnJava系统的java版本是否符合要求
@ConditionalOnBean容器中存在指定Bean;
@ConditionalOnMissingBean容器中不存在指定Bean;
@ConditionalOnExpression满足SpEL表达式指定
@ConditionalOnClass系统中有指定的类
@ConditionalOnMissingClass系统中没有指定的类
@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty系统中指定的属性是否有指定的值
@ConditionalOnResource类路径下是否存在指定资源文件
@ConditionalOnWebApplication当前是web环境
@ConditionalOnNotWebApplication当前不是web环境
@ConditionalOnJndiJNDI存在指定项

那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

❓ 我们怎么知道哪些自动配置类生效?

我们可以通过在配置文件启用 debug = true 属性,来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效。

#开启springboot的调试类
debug = true

控制台打印输出示例如下:

=========================
AUTO-CONFIGURATION REPORT
=========================


Positive matches:(自动配置类启用的)
-----------------

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition)
        
    
Negative matches:(没有启动,没有匹配成功的自动配置类)
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)

   AopAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice' (OnClassCondition)
        
  • Positive matches:(自动配置类启用的:正匹配)

  • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

  • Unconditional classes: (没有条件的类)

📚 References

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞天小牛肉

您的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值