spring注解学习之一:IOC

组件注册1-Helloworld(使用xml配置文件)

  • 新建工程,添加Maven支持
  • 在Maven配置文件pom.xml中添加:
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.13.RELEASE</version>
        </dependency>
    </dependencies>

  • 写一个Person类,属性有name,age,在类中添加无参构造方法,有参构造方法,getter和setter,toString方法
  • 在代码根目录下添加bean.xml文件,内容:
<?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="person" class="win.bojack.bean.Person">
        <property name="age" value="33" />
        <property name="name" value="zhangsan" />
    </bean>
</beans>
  • 获取在bean.xml文件中定义的bean:写一个测试类,在main方法中添加代码:
// 老的方式(xml配置文件):
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
Object person = ac.getBean("person");
System.out.println(person)

组件注册2-Helloworld(使用注解)

  • 写一个配置类MainConfig:
@Configuration # 这个注解的含义就是告诉spring这是一个配置类.
public class MainConfig {
    @Bean("person11")  # 这个注解表示这个方法生成一个bean,默认bean的名字就是方法名,当然也可以指定bean name
    public Person person() {
        return new Person("lisi", 44);
    }
}
  • 写一个测试类MainTest:
// 注解方式:
ApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig.class);
Person person = ac.getBean(Person.class);
System.out.println(person);

组件注册3-component-scan包扫描

component-scan的目的主要是扫描指定包同级和下级包中所有标注了@Componet ,@Service ,@Repository,@Controller的类.将它们注册为bean

  • 在win.bojack包下添加几个子包:controller,dao,service,分别在这几个包下增加相应的类,并在类注解上注名: @Controller, @Repository, @Service, 例:
package win.bojack.dao;
@Repository
public class BookDao {
}
  • xml方式:在bean.xml文件中增加:
<context:component-scan base-package="win.bojack" /> 
  • 注解方式:在配置类的类注解上添加:
@ComponentScan("win.bojack") # 指定要扫描的包
  • 测试:
ApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig.class);
String[] beanNames = ac.getBeanDefinitionNames();
for (String beanName : beanNames) {
    System.out.println(beanName);
}

打印时会将MainConfig也打印出来,因为MainConfig是一个@Configuration ,它是一个@Component. @Component会被@Component-scan自动扫描

includeFilters excludeFilters useDefaultFilters

@Component-scan会扫描所有标注了@Componet ,@Service ,@Repository,@Controller的类,太过粗暴.可以使用@includeFilters来指定扫描哪些类,@excludeFilters来指定排除哪些类.需要注意的是,用includeFilters之前,要用useDefaultFilters=false将默认的过滤器禁用掉才会生效:

@ComponentScan(value = "win.bojack",
		excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class})
})
@ComponentScan(value = "win.bojack",
        useDefaultFilters = false,
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
                classes = {Controller.class, Repository.class
        })
})

@ComponentScan是一个@Repeatable的,因此可以写多个@ComponentScan.也可以用@ComponentScans,里面包含多个@ComponentScan数组:

@ComponentScans(value = {
        @ComponentScan(value = "win.bojack",
                useDefaultFilters = false,
                includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
                        classes = {Controller.class, Repository.class
                        })
                }),
        @ComponentScan(value = "win.xxxxx",
                useDefaultFilters = false,
                includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
                        classes = {Controller.class, Repository.class
                        })
                })
})

过滤规则

  • FilterType.ANNOTATION (按照注解类型来过滤)
  • FilterType.ASSIGNABLE_TYPE (按照类型(类名)来过滤)
  • FilterType.ASPECTJ (ASPECTJ表达式)
  • FilterType.REGEX (使用正则表达式来过滤)
  • FilterType.CUSTOM (使用自定义类来实现过滤,必须实现TypeFilter接口)

自定义TypeFilter类:

public class MyTypeFilter implements TypeFilter {

    /**
     *
     * @param metadataReader 读取到的当前正在扫描的类的信息
     * @param metadataReaderFactory 可以获取到其他相关类的信息(一般是超类或接口)
     * @return
     * @throws IOException
     */
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        // 获得当前类注解的信息
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        // 获得当前正在扫描的类的信息
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        String className = classMetadata.getClassName();
        System.out.println("className ---> " + className);
        // 获得当前类的资源
        Resource resource = metadataReader.getResource();
        //

        return className.contains("er");
    }
}

然后在配置类中添加如下注解:

@Configuration
@ComponentScan(value = "win.bojack", useDefaultFilters = false, includeFilters = {
        @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyTypeFilter.class)
})

即可实现只注册名称中er的bean

组件注册4-Scope和Lazy

bean的scope有四种:

  • prototype: 多实例.在使用bean对象前, 是不会生成bean对象的.每次获取时才会调用方法创建对象,而且每次获取新的对象.
  • singleton: 单实例.(默认情况)只生成一个bean对象,多次获取bean获得的是同一对象. 容器初始化时都会调用生成对象的方法, 以后每次调用都是直接从窗口中拿(map.get())
  • request 同一次请求创建一个实例(web环境下,不常见)
  • session 同一个session创建一个实例(web环境下,不常用)
Object person = ac.getBean("person");
Object person1 = ac.getBean("person");
System.out.println(person == person1);
// 结果为true
  • lazy :只针对单实例. 因为多实例本来就是要使用对象时才会加载, 单实例默认情况下是容器初始化时就加载了对象

组件注册5-Conditional

  • springboot底层大量使用此注解,按照一定的条件进行判断. 满足条件给容器注册bean
  • @Conditional注解可以用在类上,也可以用在方法上
  • 需要自定义类来实现,实现Condition接口

编写两个Condition类:

public class WindowsCondition implements Condition {
    /**
     *
     * @param context 判断条件能使用的上下文环境
     * @param metadata 注释信息
     * @return
     */
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 1获取IOC使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 2获取类加载器
        ClassLoader classLoader = context.getClassLoader();
        // 3获取bean定义的注册类
        BeanDefinitionRegistry registry = context.getRegistry();
        // 4获取当前环境信息
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        return property.contains("Windows");
    }
}
public class LinuxCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        return property.contains("Linux");
    }
}

编写两个方法,分别返回两个bean:

    @Bean("bill")
    @Conditional(WindowsCondition.class)
    public Person person01() {
        return new Person("bill gates" , 66);
    }

    @Conditional(LinuxCondition.class)
    @Bean("linus")
    public Person person02() {
        return new Person("linus", 48);
    }

测试方法:

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig2.class);

private void printBeanNames() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        System.out.println(beanDefinitionName);
    }
}
@Test
public void test3() {
    System.out.println("ioc容器创建完成...");
    printBeanNames();
    Map<String, Person> beansOfType = ac.getBeansOfType(Person.class);
    System.out.println(beansOfType);
    ConfigurableEnvironment environment = ac.getEnvironment();
    String property = environment.getProperty("os.name");
    System.out.println(property);
}
bill
{bill=Person{name='bill gates', age=66}}
Windows 10

只打印了: bill.
如果在run中指定参数 -Dos.name=Linux,则只打印linus.

还可以在Condition实现类中添加其他条件,比如检查一个类是否注册,和对类进行注册:

if (registry.containsBeanDefinition("person11")) {
    registry.registerBeanDefinition(// TODO);
}

@Conditional注解除了在方法上使用,还可以用在类上, 类中配置的所有bean注册都会生效

组件注册6-Import

给容器注册bean有几种方法:

  • 包扫描+组件标注注解(@Controller/@Service/@Repository/@Component)
  • @Bean[导入第3方包里面的组件]
  • @Import[快速导入组件]
    • 在类名上使用,导入单个组件 @Import(Color.class) ,容器会注册这个组件,组件名默认是类的全名
    • @ImportSelector 生成一个类,实现ImportSelector接口, 返回一个数组,包含所有需要导入的类的路径(springboot中用得多)
    • @ImportBeanDefinitionRegistrar 生成一个类,实现MyImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,可以实现一些Bean导入逻辑.
  • @ImportSelector:
// 自定义逻辑返回需要导入的组件
public class MyImportSelector implements ImportSelector {

    // 返回值就是导入到容器中的组件类路径
    // AnnotationMetadata : 当前标注@Import注解的类的所有注解信息
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 注意这里不要返回null,至少返回一个空数组
        return new String[]{"win.bojack.bean.Blue", "win.bojack.bean.Red", "win.bojack.bean.Yellow"};
    }
}

在配置类上添加:

@Import({Color.class, MyImportSelector.class})
  • @ImportBeanDefinitionRegistrar:
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    /**
     *
     * @param importingClassMetadata : 当前类的注解信息
     * @param registry BeanDefinition注册类:
     *                 把所有需要添加到容器中的bean:调用
     *                 BeanDefinitionRegistry.registerBeanDefinition手工注册进来
     */
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean reg1 = registry.containsBeanDefinition("win.bojack.bean.Red");
        boolean reg2 = registry.containsBeanDefinition("win.bojack.bean.Blue");
        if (reg1 && reg2) {
            // 指定Bean定义信息,{Bean的类型,scope...}
            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Rainbow.class);
            // 指定Bean名注册一个Bean
            registry.registerBeanDefinition("rainBow", rootBeanDefinition);
        }
    }
}
  • 使用Spring提供的BeanFactory(工厂Bean)
    • 默认获取到的是工厂Bean本身调用getObject产生的对象
    • 要获取工厂Bean本身,需要在id前面加一个&符号: &colorFactoryBean
      生成一个工厂类:
public class ColorFactoryBean implements FactoryBean<Color> {
    // 返回一个Color对象,这个对象会添加到容器中
    public Color getObject() throws Exception {
        return new Color();
    }
    public Class<?> getObjectType() {
        return Color.class;
    }
    // 是否单例
    public boolean isSingleton() {
        return true;
    }
}

在配置类中增加Bean:

@Bean
public ColorFactoryBean colorFactoryBean() {
    return new ColorFactoryBean();
}

测试类:

// 这里获取的Bean是colorFactoryBean,打印getClass时却是Color, 因为实际上调用的是ColorFactoryBean中的getObjectType方法, 返回的Bean也是ColorFactoryBean中的getObject方法
Object colorFactoryBean = ac.getBean("colorFactoryBean");
        System.out.println(" color 类型: " + colorFactoryBean.getClass());
// 如果想要返回colorFactoryBean,则需要:
Object bean1 = ac.getBean("&colorFactoryBean");
// 在Bean前面加&是BeanFactory这个接口的约定

生命周期:初始化和销毁方法

Bean的生命周期:bean的创建–初始化–销毁的过程
容器管理的bean的生命周期。我们可以自定义初始化和销毁方法:容器在bean进行到当前生命周期某个阶段时,自动调用指定的方法

构造(对象创建)
单实例:在容器启动的时候创建对象
多实例:在每次获取的时候创建对象

执行顺序:
BeanPostProcessor.postProcessBeforeInitialization
初始化:在对象创建完成并赋值好时,调用初始化方法…
BeanPostProcessor.postProcessAfterInitialization
销毁:容器关闭时
单实例:容器关闭的时候
多实例:容器不会管理这个Bean容器不会调用销毁方法(需要手动调用销毁方法)

指定初始化和销毁方法:在xml配置文件时,可以指定init-methoddestroy-method,或通过@Bean指定init-methoddestroy-method
通过让Bean实现过InitializingBean接口来实现定义初始化逻辑,和DisposableBean接口来实现销毁逻辑
通过@PostConstructor在Bean创建完成并且属性赋值完成时来执行方法
和@PreDestroy在容器销毁Bean之前来执行清理工作

指定初始化和销毁方法

@Configuration
@ComponentScan("win.bojack.bean")
public class MainConfigOfLifeCycle {

    // @Scope("prototype")
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Car car() {
        return new Car();
    }
}
public class Car {
    public Car() {
        System.out.println("car constructor...");
    }
    public void init() {
        System.out.println("car init...");
    }
    public void destroy() {
        System.out.println("car destroy...");
    }
}
    public void test01() {
        // 1,创建IOC容器
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
        System.out.println("容器创建完成");
        ac.close();
    }
打印结果:
car constructor...
car init...
容器创建完成
car destroy...

实现InitializingBean DisposableBean:

@Component
public class Cat implements InitializingBean, DisposableBean {
    public Cat() {
        System.out.println("cat constructor...");
    }
    public void destroy() throws Exception {
        System.out.println("cat destroy...");
    }

    public void afterPropertiesSet() throws Exception {
        System.out.println("cat propertiesSet ...");
    }
}

使用@PostConstruct @PreDestroy注解:

@Component
public class Dog {
    public Dog() {
        System.out.println("dog Constructor ... ");
    }
    @PostConstruct
    public void init() {
        System.out.println("dog PostConstruct ... ");
    }
    @PreDestroy
    public void destroy() {
        System.out.println("dog PreDestroy ... ");
    }
}

结果:

cat constructor...
cat propertiesSet ...
dog Constructor ... 
dog PostConstruct ... 
car constructor...
car init...
容器创建完成
car destroy...
dog PreDestroy ... 
cat destroy...

使用BeanPostProcessor接口:Bean的后置处理器

在Bean初始化前后进行一些处理工作,通过实现BeanPostProcessor的两个方法来实现一些功能:

  • postProcessBeforeInitialization:在任何初始化工作之前,比如afterPropertiesSet方法之前,完成某些任务
  • postProcessAfterInitialization:在初始化之后工作
/**
 * 后置处理器:在容器初始化前后进行处理工作。对所有组件都生效。
 * 将后置处理器加入到容器中
 */
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization +++ " + beanName + "==>" + bean);
        // 在这里可以对bean进行一些额外的处理,然后再返回
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInitialization ... " + beanName + "==>" + bean);
        return bean;
    }
}

结果:

postProcessBeforeInitialization +++ org.springframework.context.event.internalEventListenerProcessor==>org.springframework.context.event.EventListenerMethodProcessor@36f0f1be
postProcessAfterInitialization ... org.springframework.context.event.internalEventListenerProcessor==>org.springframework.context.event.EventListenerMethodProcessor@36f0f1be
postProcessBeforeInitialization +++ org.springframework.context.event.internalEventListenerFactory==>org.springframework.context.event.DefaultEventListenerFactory@6ee12bac
postProcessAfterInitialization ... org.springframework.context.event.internalEventListenerFactory==>org.springframework.context.event.DefaultEventListenerFactory@6ee12bac
postProcessBeforeInitialization +++ mainConfigOfLifeCycle==>win.bojack.config.MainConfigOfLifeCycle$$EnhancerBySpringCGLIB$$39885bd3@64c87930
postProcessAfterInitialization ... mainConfigOfLifeCycle==>win.bojack.config.MainConfigOfLifeCycle$$EnhancerBySpringCGLIB$$39885bd3@64c87930
cat constructor...
postProcessBeforeInitialization +++ cat==>win.bojack.bean.Cat@525f1e4e
cat propertiesSet ...
postProcessAfterInitialization ... cat==>win.bojack.bean.Cat@525f1e4e
dog Constructor ... 
postProcessBeforeInitialization +++ dog==>win.bojack.bean.Dog@5ea434c8
dog PostConstruct ... 
postProcessAfterInitialization ... dog==>win.bojack.bean.Dog@5ea434c8
car constructor...
postProcessBeforeInitialization +++ car==>win.bojack.bean.Car@1d548a08
car init...
postProcessAfterInitialization ... car==>win.bojack.bean.Car@1d548a08
容器创建完成
car destroy...
dog PreDestroy ... 
cat destroy...

可以看出,所有的组件都经过了MyBeanPostProcessor的两个方法的处理

BeanPostProcessor的原理&Spring底层应用

AnnotationConfigApplicationContext的初始化开始,一层层地调用,一直到AbstractAutowireCapableBeanFactory.initializeBean方法,这个方法执行applyBeanPostProcessorsBeforeInitialization,然后invokeInitMethods,再执行applyBeanPostProcessorsAfterInitialization。这两个方法找到所有的BeanPostProcessor,执行每个beanPostProcesssorpostProcessBeforeInitializationpostProcessAfterInitialization方法并获得结果,如果有一个方法的结果为null,直接返回结果,不会执行后面的BeanPostProcessor

populateBean(beanName, mbd, instanceWrapper); // 给bean进行属性赋值
initializeBean() {
	applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
	invokeInitMethods(beanName, wrappedBean, mbd); // 执行自定义初始化
	applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

BeanPostProcessor接口众多的实现类中,有一个ApplicationContextAwareProcessor,它的作用就是给组件注入IOC容器。如果一个组件想要IOC容器,就可以实现ApplicationContextAware并实现方法:

private ApplicationContext ac;
public void setApplicationContext(ApplicationContext ac) throws BeansException {
	this.ac = ac;
}

然后这个组件就可以很愉快的调用容器本身的一些功能了,很神奇,很蒙B有木有??这正是ApplicationContextAwareProcessor实现的,它有一个postProcessBeforeInitialization方法,这个方法判断组件有没有实现ApplicationContextAware接口,如果有实现的话就调用invokeAwareInterfaces方法,将组件转成对应的ApplicationContextAware,再将applicationContext设置进来。

BeanPostProcessor还有一些重要的实现类,比如BeanValidationPostProcessor,在做数据检验时很有用。它可以在初始化前后调用postProcessBeforeInitializationpostProcessAfterInitialization再调用doValidate来实现数据校验功能。

InitDestroyAnnotationBeanPostProcessor,它处理PostConstructPostDestroy注解的,实现原理也是通过调用调用postProcessBeforeInitializationpostProcessAfterInitialization方法来实现的。

AutowiredAnnotationBeanPostProcessor:处理@Autowired标记的组件,原理同上。

总结Spring底层,通过调用BeanPostProcessor,实现bean赋值,注入其他组件,@Autowired,生命周期注解功能,@Async,都是通过xxxxBeanPostProcessor来实现的。

属性赋值

  • 使用@Value赋值:
    • 1、直接写基本数值,如String、Integer、Boolean类型的值
    • 2、可以写SPEL:#{}
    • 3、可以通过${}取出配置文件(.properties)中的值(在运行环境变量中的值),这种情况需要在xml文件配置<context:property-placeholder location="classpath:person.properties“/>,或者在配置类中添加注解@PropertySource(value = {"classpath:/xxxx.properties"}, encoding = "GBK") #注意指定编码
// 配置类:
@Configuration
@PropertySource(value = {"classpath:/xxxx.properties"}, encoding = "GBK")
public class MainConfigOfPropertiesValue {
@Value("zhangsan")
private String name;
@Value("#{20-4}")
private Integer age;
@Value("${name.nickName}")
private String nickName;
# xxxx.properties
name.nickName=小张

还可以用这种方式获得properties文件中配置的属性值:

String property = ac.getEnvironment().getProperty("person.nickName");
System.out.println(property);

还可以使用@PropertySources来指定多个@PropertySource,因为@PropertySource是个@Repeatable

自动装配@Autowired、@Qualifier、@Primary

Spring 利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值
1、@Autowired,自动注入
默认优先按照类型去容器中找对应的组件,如果找到就进行赋值。applicationContext.getBean(BookDao.class);
如果找到多个相同类型的组件,再将属性作为组件的id去容器中查找applicationContext.getBean("bookDao");
@Qualifier("bookDao3")使用@Qualifier指定需要装配的组件的id,而不是使用属性名
自动装配一定要将属性赋值好,没有就会报错。也可以使用@Autowired(required= false)来实现非必须的依赖,也就是找得到组件就自动装配,没有找到就不装配。
可以使用@Primary设置首选的组件

2、Spring还支持@Resource(JSR250规范)和@Inject(JSR330规范)
@Resource也能实现自动装配功能,默认是按照组件名称来装配的。没有Primary功能也没有Autowired(required=xx)的功能
@Inject需要导入javax.inject依赖,功能和@Autowired类似,@Inject没有任何属性,因此不太好用

@Autowired是Spring定义的,@Resource@Inject都是Java规范

@Autowired是利用AutowiredAnnotationBeanPostProcessor来实现自动装配的

方法、构造器位置的自动装配

3、 @Autowired可以标注在方法上,也可以标注在构造方法,或者构造方法的参数上。如果组件只有一个有参构造器,这个有参构造器的@Autowired可以省略。参数位置的组件还是可以从容器中自动获取

  • 1、【标注在方法位置】 @Bean+方法参数 :参数从容器中获取,默认不写@Autowired效果是一样的;都能自动装配
  • 2、【标注在构造器上】,如果组件只有一个有参构造器,这个有参构造器的@Autowired可以省略,参数位置的组件可以自动装配
  • 3、放在参数位置,

4、自定义组件想要使用Spring容器底层的一些组件(ApplicationContext,BeanFactory,…),就可以自定义组件实现xxxAware:在创建对象的时候,会调用 接口规定的方法来注入相关的组件。有一个叫Aware的总接口,它是利用一种像回调机制的方式来实现自动调用。
自定义组件实现xxxAware:在创建对象的时候,会调用接口规定的方法注入相关的组件;Aware,把Spring底层的一些组件注入到自定义的Bean中;
每一个xxxAware都有对应的xxxBeanPostProcessor:
比如ApplicationContextAware对应着ApplicationContextAwareProcessor,后者就是一个BeanPostProcessor

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值