Spring Boot学习笔记总结(二)

1. SpringBoot底层注解 @Configuration

1.1 @Configuration 和 @Bean的作用


配置类,必须能让SpringBoot扫描到才能装配到容器中!

创建一个MyConfig类,作为配置类:

  • @Configuration注解作用:告诉springboot这是一个配置类。等同于我们创建了一个spring.xml配置文件。
  • @Bean注解作用:给容器中添加bean对象(组件)。相当于在spring.xml文件中创建了一个bean对象。
    • 方法名就是对应bean对象(组件)的id。
    • 返回值类型就是bean对象(组件)的类型。
    • 返回的值就是bean对象(组件)在容器中的实例。
  • 配置类本身也是一个对象(组件),也会被注入到容器中。
package com.itholmes.boot.config;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//@Configuration注解作用:告诉springboot这是一个配置类。等同于我们创建了一个spring.xml配置文件。
@Configuration
public class MyConfig {

    /*
        @Bean注解作用:给容器中添加bean对象(组件)。相当于在spring.xml文件中创建了一个bean对象。
            方法名就是对应bean对象(组件)的id。
            返回值类型就是bean对象(组件)的类型。
            返回的值就是bean对象(组件)在容器中的实例。
    */
    @Bean
    public User user01(){
        return new User("张三",18);
    }

    @Bean
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

}

MainApplication,主程序类中,可以获取对应的bean对象(组件):

  • SpringApplication.run(MainApplication.class, args)的返回值就是ioc容器。
  • 可以直接getBean获取容器中的对象(组件),验证上面的注解类。
package com.itholmes.boot;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import com.itholmes.boot.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages="com.itholmes")
public class MainApplication {
    public static void main(String[] args) {

        //1. 返回IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2. 查看容器里面的组件(就是bean的对象)
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        //3. 从容器中获取组件。
        Pet pet = run.getBean("tomcatPet", Pet.class);
        User user01 = run.getBean("user01", User.class);
        User user02 = run.getBean("user01", User.class);
}

1.2 @Configuration注解的 proxyBeanMethods参数变量

1.2.1 @Configuration(proxyBeanMethods = true)


SpringBoot2的@Configuration比SpringBoot1版本多了一个proxyBeanMethods 参数。

proxyBeanMethods注解,英文直译过来就是代理bean方法。其实就是告诉SpringBoot当前的配置类是否生成代理对象。

从容器中,获取配置类对象,如果@Configuration(proxyBeanMethods = true(默认就是true)) ,那么生成的就是代理对象。代理对象每次执行方法的时候都是先去容器中拿对象(组件),没有才会创建。

 // 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

// 配置类本身也是一个对象(组件)
MyConfig myConfig = run.getBean(MyConfig.class);

System.out.println(myConfig);
//com.itholmes.boot.config.MyConfig$$EnhancerBySpringCGLIB$$f2269e91@5e7c141d

打印出的代理对象,一般都会带着$$这样的标识。

这里的代理对象,无论调用多少次对象中的方法,拿到的依然是组件里面的单例对象。

总之,被@Configuration(proxyBeanMethods = true)修饰的配置类,SpringBoot会生成一个代理对象,每一个代理对象都先会先去容器中拿对象,也就是所谓的单例模式的效果。

1.2.2 @Configuration(proxyBeanMethods = false)


@Configuration(proxyBeanMethods = false),设置为了false,就不会在容器中创建代理对象。当然每一次调用方法,都会产生一个新的对象,也就是契合了多例模式。


SpringBoot2 @Configuration新增的两个模式:

  • full (全配置模式) proxyBeanMethods = true , 被修饰的配置类,SpringBoot会生成一个代理对象并装配到容器中。每次代理对象,都会先检查容器有没有。
  • Lite (轻量级配置模式) proxyBeanMethods = false , 不会生成代理对象。

full模式,因为只要外部调用代理对象方法,代理对象会不断的检查容器中是否有,这个过程很浪费时间。

1.3 什么时候proxyBeanMethods设置为true,什么时候设置false?


当有容器中,有组件(对象)依赖的时候,会将proxyBeanMethods设置为true。

当有容器中,没有组件(对象)依赖会设置为false。


什么是组件依赖?

  • 相当于我在xml文件中创建了两个bean,但是其中有一个bean对象的内部属性依赖于另一bean。

例如:

在User实体类中,创建一个Pet实体类的成员变量。

package com.itholmes.boot.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String name;
    int age;
    Pet pet;
}

MyConfig类的proxyBeanMethods设置为true,就可以识别到容器中的对象。这样就可以实现组件依赖。

package com.itholmes.boot.config;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = true)
public class MyConfig {

    @Bean
    public User user01(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        //依赖于tomcatPet方法,注入容器的对象。
        user.setPet(tomcatPet());
        return user;
    }

    @Bean
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

}
package com.itholmes.boot;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import com.itholmes.boot.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages="com.itholmes")
public class MainApplication {
    public static void main(String[] args) {

        //返回IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //查看容器里面的组件(就是bean的对象)
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        //从容器中获取组件。
        Pet pet = run.getBean("tomcatPet", Pet.class);
        User user = myConfig.user01();

        System.out.println("测试是否是同一个bean对象:"+(user.getPet() == pet));
        //测试是否是同一个bean对象:true
    }
}

2. SpringBoot底层注解 @import注解


@Import({User.class,Dog.class})就会给容器注入一个无参的User类和Dog类。

  • 在任何类都可以用它修饰装配到容器中,前提是这个类一定要被SpringBoot扫描到!
  • 被装配的类的名字是对应的全类名(路径加类名),例如:com.itholmes.boot.bean.User。
package com.itholmes.boot.config;

import com.itholmes.boot.bean.Dog;
import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/*
    @Import({User.class,Dog.class})
        给容器中自动创建出这两个类型的组件。
*/
@Import({User.class, Dog.class})
@Configuration(proxyBeanMethods = true)
public class MyConfig {

    @Bean
    public User user01(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        //依赖于tomcatPet方法,注入容器的对象。
        user.setPet(tomcatPet());
        return user;
    }

    @Bean
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

}

主程序类,测试:

package com.itholmes.boot;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import com.itholmes.boot.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages="com.itholmes")
public class MainApplication {
    public static void main(String[] args) {

        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        Pet pet = run.getBean("tomcatPet", Pet.class);
        User user01 = run.getBean("user01", User.class);

        //getBeanNamesForType(xxx)方法会获取所有xxx.class类型的参数。
        String[] users = run.getBeanNamesForType(User.class);
        for (String user : users) {
            System.out.println(user);
        }

        //com.itholmes.boot.bean.User 一个是对应@import注入
        //user01 一个是对应@Bean注入

    }
}

3. SpringBoot底层注解 @Conditional注解


@Conditional注解:条件装配,满足Conditional指定的条件,则进行组件装配和注入。

在这里插入图片描述

再例如,@ConditionalOnBean注解使用。

  • @ConditionalOnBean(name=“tom”)注解:意思是当name中的有id为tom的对象组件的时候,再装配user01这个组件对象。
package com.itholmes.boot.config;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/*
    @ConditionalOnBean注解:意思是当name中的有id为tom的对象组件的时候,再装配user01这个组件对象。
*/

//修饰到类上面,就是如果容器中,没有id为tom的对象组件,则类下面方法都不会装配到容器中。
//@ConditionalOnBean(name="tom")
@Configuration(proxyBeanMethods = true)
public class MyConfig {


    @ConditionalOnBean(name="tom") //修饰到方法上,如果容器中,没有id为tom对象组件,则该方法不会装配到容器中。
    @Bean("user")
    public User user01(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        //依赖于tomcatPet方法,注入容器的对象。
        user.setPet(tomcatPet());
        return user;
    }

    //@Bean("tom")//这里注释了@Bean注解,这样该方法就不会被装配到容器中。
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

}
package com.itholmes.boot;

import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import com.itholmes.boot.config.MyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages="com.itholmes")
public class MainApplication {
    public static void main(String[] args) {

        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //containsBean方法:判断容器中是否包含一个id为tom的组件对象。
        boolean user = run.containsBean("user");
        System.out.println("判断容器中,是否还有user的组件对象:"+user);

        boolean tom = run.containsBean("tomcatPet");
        System.out.println("判断容器中,是否还有tom的组件对象:"+tom);

		//判断容器中,是否还有user的组件对象:false
		//判断容器中,是否还有tom的组件对象:false
		
    }
}

这些当什么什么才会装配到容器。一般用在SpringBoot的底层中,经常使用!

4. SpringBoot底层注解 @ImportResource


有些使用,可能还是会用到xml配置文件,那么想要让该xml配置,装配到springboot的ioc容器中的话,就可以使用@ImportResource注解来装配。

在这里插入图片描述

5. SpringBoot底层注解 @ConfigurationProperties

5.1 方式一:@Component注解 + @ConfigurationProperties注解


方式一:两个注解都用到实体类中。

@Component注解 + @ConfigurationProperties注解:

  • 使用@Component注解装配到容器中。
  • @ConfigurationProperties(prefix = “mycar”)注解:
    就可以在application.properties配置文件中拿到前缀为mycar对应属性的值。将这些值注入到对应的容器组件对象中。

创建一个Car类:

package com.itholmes.boot.bean;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/*
    只有容器中的组件,才会拥有SpringBoot提供的强大功能。所以使用@Component注解装配到容器中。

    @ConfigurationProperties(prefix = "mycar")注解:
        就可以在application.properties配置文件中拿到前缀为mycar对应属性的值。将这些值注入到对应的组件对象中。
*/
@Component//先将当前的类,装配到容器中。
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
    private Integer price;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

application.properties配置文件:

# 修改端口号
server.port=8888

# 相当于将下面的两个属性内容,注入到了对应的组件对象中。
mycar.brand=BYD
mycar.price=100000

这样就相当于在容器中装配了对象,并且给该对象注入了相关信息。
在这里插入图片描述

5.1 方式二:@EnableConfigurationProperties(Car.class)注解 + @ConfigurationProperties注解


方式二:@ConfigurationProperties注解用于实体类,@EnableConfigurationProperties(Car.class)注解用于配置类中。

  • @ConfigurationProperties注解作用同上。
  • @EnableConfigurationProperties(Car.class)注解,1.开启Car配置绑定功能,也就是读取properties文件。2.把这个Car这个组件自动注册到容器中。

Car实体类:

package com.itholmes.boot.bean;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/*
    只有容器中的组件,才会拥有SpringBoot提供的强大功能。所以使用@Component注解装配到容器中,如果配置类中有@EnableConfigurationProperties(Car.class)也可以是可以装配到容器中,并且注入属性的。

    @ConfigurationProperties(prefix = "mycar")注解:
        就可以在application.properties配置文件中拿到前缀为mycar对应属性的值。将这些值注入到对应的容器组件对象中。
*/
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
    private Integer price;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

MyConfig配置类:

package com.itholmes.boot.config;

import com.itholmes.boot.bean.Car;
import com.itholmes.boot.bean.Pet;
import com.itholmes.boot.bean.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

/*
    @EnableConfigurationProperties注解作用:
        1.开启Car配置绑定功能。
        2.把这个Car这个组件自动注册到容器中。
*/
@EnableConfigurationProperties(Car.class)
@Configuration(proxyBeanMethods = true)
public class MyConfig {


    @ConditionalOnBean(name="tom") //修饰到方法上,如果容器中,没有id为tom对象组件,则该方法不会注入到容器中。
    @Bean("user")
    public User user01(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        user.setPet(tomcatPet());
        return user;
    }

    //@Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }

}

6. @SpringBootApplication注解源码分析

6.1 @SpringBootConfiguration作用


@SpringBootApplication注解源码上,还有其他三个注解:

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

@SpringBootConfiguration注解源码分析:


//三个元注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented

//说明代表当前是一个配置类。
@Configuration

@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

也就是说主程序本身就是一个配置类:
在这里插入图片描述

6.2 @ComponentScan注解


@ComponentScan注解,只是指定SpringBoot应该扫描哪些包。

6.3 @EnableAutoConfiguration注解


@EnableAutoConfiguration注解源码如下

// 四个元注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited

//@EnableAutoConfiguration注解主要的功能也是是现在下面这几个上面。
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})

public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

那就继续分析@AutoConfigurationPackage和@Import注解。


@AutoConfigurationPackage注解分析:自动配置包。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//@Import({Registrar.class})注解作用:给容器中导入Registrar.class这个类的组件(对象)
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

@Import({Registrar.class})注解作用:给容器中导入Registrar.class这个类的组件(对象)。

Registrar.class类负责给容器中导入一系列组件。

  • 从AnnotationMetadata metadata(是注解的元信息,也就是@AutoConfigurationPackage注解)中拿到要扫描的包名。
  • BeanDefinitionRegistry是一个接口,它定义了关于 BeanDefinition 的注册、移除、查询等一系列的操作。
    在这里插入图片描述
    在这里插入图片描述

也就是@AutoConfigurationPackage注解本身的做就是负责扫描包下的组件对象,并装配。


@Import({AutoConfigurationImportSelector.class})注解分析:

AutoConfigurationImportSelector.class类源码中有一个方法:

//AnnotationMetadata annotationMetadata 得到注解的元信息。
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
    	//getAutoConfigurationEntry(annotationMetadata)方法很关键!
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

就是利用getAutoConfigurationEntry(annotationMetadata)方法来给容器批量导入一些组件。

在这里插入图片描述
在getAutoConfigurationEntry方法中:

  • 调用List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);获取到所有需要导入容器中的配置类。
  • 可以debug中step into一下,一层层的看源码。
    • 先会看到List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());。
    • loadFactoryNames方法中,有loadSpringFactories方法。
    • private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader),发现就是一个工厂加载,从而得到所有组件。
  • 在loadSpringFactories里面有一个classLoader.getResources(“META-INF/spring.factories”);作用是加载一个META-INF/spring.factories的配置文件
  • 在spring-boot-autoconfigure:2.6.4.jar包下,也有对应的META-INF/spring.factories文件,也就是文件里面写死了springboot一启动就要给容器中加载所有配置类。
    在这里插入图片描述

按需开启自动配置:

  • 虽然我们100多个场景的所有自动配置启动的时候默认全部加载。但是最终会按需配置!
  • 按需配置,就是通过@Conditional一系列的条件装配,来按需引入。
    在这里插入图片描述

7. SpringBoot的自动配置流程


springboot的自动配置,离不开spring-boot-autoconfigure的jar包。配置的流程就是根据spring-boot-autoconfigure中的spring.factories中的内容,以及spring-boot-autoconfigure包中下面的配置类。如下图:
在这里插入图片描述


例如:spring-boot-autoconfigure包下的autoconfigure/web/servlet包中DispatcherServletAutoConfiguration类下:

有一个MultipartResolver多文件处理器方法。

@Bean
@ConditionalOnBean({MultipartResolver.class}) //当容器中有这个类型组件时。
@ConditionalOnMissingBean(
    name = {"multipartResolver"}
)//当容器中没有这个名字multipartResolver的组件或者对象时,就是如果名字不相同。

//SpringBoot也会找到这个方法,就是为了防止有写用户配置的MultipartResolver文件上传解析器了。。
//给@Bean标注的方法传入对象参数,这个参数的值就会从容器中找到。
public MultipartResolver multipartResolver(MultipartResolver resolver){
    return resolver;
}

像ssm中,HttpEncodingAutoConfiguration解决乱码问题的内容,SpringBoot都在spring-boot-autoconfigure包下配置好了。
在这里插入图片描述
在这里插入图片描述


通过上面了解,SpringBoot的设计模式是:

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了那么就会使用用户配置的组件对象。

实现这种效果,就是@ConditionalOnMissingBean注解的功劳。判断容器中是否有这个bean对象。


还有很多xxxProperties类,这些类在SpringBoot的核心配置文件application.properties中拿到相关的属性配置。
在这里插入图片描述

当我们想要修改一些内容信息时,例如:修改HttpEncodingAutoConfiguration的编码格式,就可以如下图直接在application.properties中配置就可以:
在这里插入图片描述

8. 总结 自动配置流程原理


** 自动配置流程就是:**

  • SpringBoot先加载所有的自动配置类,自动配置类的格式一般是:xxxAutoConfiguration。
  • 每个配置类按照条件装配进行生效,默认都会绑定配置文件指定的值。绑定的类的格式一般是:xxxProperties,xxxProperties回去application.properties中找配置数据。
  • 生效的配置类就会给容器中装配很多组件。
  • 只要容器中有这些组件,相当于这些功能就有了。
  • 定制化配置:
    • 用户直接自己@Bean替换底层的组件。
    • 用户去看这个组件是获取的配置文件什么值就去修改。

先是一个xxxAutoConfiguration的配置类 ==》 然后导入很多组件 ==》这些组件在从xxxProperties中拿值 ==》再去application.properties配置文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xupengboo

你的鼓励将是我创作最大的动力。

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

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

打赏作者

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

抵扣说明:

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

余额充值