Spring IOC的理解

目录

1. 前言

2. 介绍IOC是什么

2.1 补充

2.1.1 依赖注入的方式

2.1.1.1 基于构造方法

2.1.1.2 基于Setter方法

2.1.1.3 基于自动织入

2.1.2 配置Bean的方式

2.1.2.1 基于XML文件的配置

2.1.2.2 基于注解的配置

3. 讲述初始化过程

3.1 IOC容器初始化流程

3.2 Bean的生命周期

3.2.1 容器的初始化

4. Spring其他用法

4.1 Aware接口

4.2 事件机制

4.3 容器的扩展点

5. SpringBean作用域、循环依赖

5.1 Spring Bean的作用域

5.2 循环依赖

6. 总结、直接背

6.1 谈谈你对IOC的理解


1. 前言

不知道为什么,虽然工作了两年有余,Spring相关的资料也看了不少,但如果被面试问到这道题还是会一脸懵逼,被克的死死的(可能是因为大学学Java web和struts的时候没认真学,导致我不清楚没有Spring的世界是什么样的,有机会可以考察一波)。最近的一次机会就是因为Spring相关回答的很差结果挂了(不讲武德不预约时间搞偷袭),痛定思痛决定开一篇自己总结的Spring专题,最老生常谈的IOC理解放到第一篇。

文章前面也有帮助理解、记录的作用,最后一节是话术总结。

参考范围:《学透Spring》(主要)、某视频、一些博客。

2. 介绍IOC是什么

Spring最关键的两个思想是IOC-控制反转和AOP-依赖注入,控制反转使你从繁琐的对象交易中解脱出来,专注于对象本身,更进一步突出“面向对象”。

IOC的作用是将业务对象(也就是Bean)和关于组件的配置元数据(比如依赖关系)输入Spring容器中,容器就能为我们组装出一个可用的系统。

IOC是通过DI-依赖注入实现的(还有DL-依赖查找,但已废弃或没人用),它的含义是把底层类作为参数传递给上层类,实现上层对下层的“控制”。

在实际的工作中,这种思想也有利于团队合作和单元测试,几个类之间只需要定义好接口就可以同时开发了,不会受到制约。

上面的例子是基于构造函数方式的注入。其他常见的还有:

  • Setter注入
  • 接口注入
  • 注解注入

注入方式有的地方说只有两种,或基本的注入方式是两种。接口注入因为很不常见所以暂不考究,注解注入其实可以放到自动织入的范畴,但也无所谓看怎么划分的了。

因为使用了依赖注入,在初始化的时候就会有很多new对象(上图右边框框),而IOC容器就解决了这个问题,IOC容器可以自动对代码进行初始化,程序员只需要维护一个configuration,比如xml文件,或配置类(通过使用@Configuration、@Bean、@ComponentScan等注解)。

整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理。

2.1 补充

上面说了依赖注入有两种方式, 类似的还有Bean的配置方式是三种:

  • 基于XML文件的配置
  • 基于注解的配置
  • 基于Java类的配置

其实依赖注入和Bean的装配是一回事,只是上面分类的维度不同。

依赖注入的本质就是装配,装配是依赖注入的具体行为。

2.1.1 依赖注入的方式

2.1.1.1 基于构造方法

所谓基于构造方法的注入,就是通过构造方法来注入依赖。首先

public class Hello {
    private String name;

    public Hello(String name) {
        this.name = name;
    }

    public String hello() {
        return "Hello World! by " + name;
    }
}

对应的XML配置文件需要使用<constructor-arg/>传入构造方法所需的内容,如下

<?xml ...?>

<beans xmlns=.......>

    <bean id = "hello" class="learning.spring.helloworld.Hello">
        <constructor-arg value="Spring"/>
    </bean>

</beans>

其中<constructor-arg>中有不少属性可以配置,如

value-参数的值、ref-Bean ID、type-参数类型、name-参数名称等。

2.1.1.2 基于Setter方法
public class Hello {
    private String name;

    public String hello() {
        return "Hello World! by " + name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
<?xml ...?>

<beans xmlns=.......>

    <bean id = "hello" class="learning.spring.helloworld.Hello">
        <property name="name" value="Spring"/>
    </bean>

</beans>

<property/>中的属性value是直接注入的值,用ref属性则可以注入其他Bean。

2.1.1.3 基于自动织入

手动配置依赖在Bean少时还能接受,当Bean的数量变多后,这种配置就变得非常繁琐。在合适的场合,可以让Spring容器替我们自动进行依赖注入,这种机制称为自动织入。自动织入默认是基于setter方法的,有几种模式:

no

不进行自动织入
byName根据属性名查找对应的Bean进行自动织入
byType根据属性类型查找对应的Bean进行自动织入
constructor同byType,但用于构造方法注入

在<bean/>中可以通过autowire属性来设置使用何种自动织入方式,也可以在<beans/>中设置default-autowire属性指定默认的自动织入方式。在使用自动织入时,需要注意以下事项:

  • 开启自动织入后,仍可以手动设置依赖,手动设置的依赖优先级高于自动织入;
  • 自动织入无法注入基本类型和字符串;
  • 对于集合类型的属性,自动织入会把上下文里找到的Bean都放进去,但如果属性不是集合类型,有多个候选Bean就会有问题。

最后,一般情况下Spring容器会根据依赖情况自动调整Bean的初始化顺序。不过有时Bean之间的依赖并不明显,如果我们想要按照某种顺序初始化,可以使用<bean/>的depends-on属性指定当前Bean还要依赖哪些Bean(如果是基于Java类的配置方式,可以用@DependsOn注解)。

2.1.2 配置Bean的方式

2.1.2.1 基于XML文件的配置

Spring Framework提供了<beans/>这个Schema来配置Bean,上面已经简单举例,下面再补充几个重要的属性:scope表明当前Bean是单例还是原型,lazy-init是指当前Bean是否是懒加载的,depends-on明确指定当前Bean的初始化顺序。

<bean id="..." scope="singleton" lazy-init="true" depends-on="xxx"/>
2.1.2.2 基于注解的配置

Spring Framework 2.0引入了@Required注解,但不建议使用;Spring Framework 2.5 又引入了@Autowired、@Component、@Service、@Repository等重要的注解,使用这些注解能简化Bean的配置。我们需要先像如下这样开启对这些注解的支持

<?xml ... ?>
<beans ......>

    <context:component-scan base-package="learning.spring"/>

</beans>

<context:component-scan/> 会隐式地配置<context:annotation-config/>,后者替我们注册了一堆BeanPostProcessor。

上述配置会扫描learning.spring包内的类,在类上添加如下四个注解都能让Spring容器把它们配置为Bean:

@Component将类标识为普通的组件,即一个Bean
@Service将类标识为服务层的服务
@Repository将类标识为数据层的数据仓库,一般是DAO
@Controller将类标识为Web层的Web控制器(后来针对REST服务又增加了一个@RestController注解)

如果不指定Bean的名称,Spring容器会自动生成一个名称,当然也可以明确指定:@Component("name")

如果要注入依赖,可以使用如下的注解:

@Autowired根据类型注入依赖,可用于构造方法、Setter方法和成员变量
@ResourceJSR-250的注解,根据名称注入依赖
@InjectJSR-350的注解,同@Autowired

@Autowired比较常用,还可以结合@Qualifier("")注解指定目标依赖的BeanID。除此之外,还可以使用@Value注解注入环境变量、Properties或YAML中配置的属性和SpEL表达式的计算结果。

2.1.2.3 基于Java类的配置

从Spring Framework3.0 开始,我们可以使用Java类代替XML文件,使用@Configuration、@Bean和@ComponentScan等一系列注解,基本可以满足日常所需。 

通过AnnotationConfigApplicationContext可以构建一个支持基于注解和Java类的Spring上下文,这个在下面讲初始化过程也会提到:

ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);

其中的Config类就是一个加了@Configuration注解的Java类,比如:

@Configuration
@ComponentScan("learning.spring")
public class Config {
    @Bean
    @Lazy
    @Scope("prototype")
    public Hello helloBean() {
        return new Hello();
    }
}

在Java配置类中指定Bean之间的依赖关系有两种方式,通过方法的参数注入依赖,或者直接调用类中带有@Bean注解的方法,如下示例,下面两个方法注入了同一个Bean

@Configuration
public class Config {
    @Bean
    public Foo foo() {
        return new Foo();
    }

    @Bean
    public Bar bar(Foo foo) {
        return new Bar(foo);
    }

    @Bean
    public Baz baz() {
        return new Baz(foo());
    }
}

另外,Spring针对@Configuration类中带有@Bean注解的方法通过CGLIB做了特殊处理,针对返回单例类型Bean的方法,调用多次返回的结果是一样的,并不会真的执行多次。

在配置类中也可以导入其他配置,例如用@Import导入其他配置类,用@ImportResource导入配置文件,如下:

@Configuration
@Import({ConfigA.class, ConfigB.class})
@ImportRespirce("classpath:/spring/*-applicationContext.xml")
public class Config {}

3. 讲述初始化过程

3.1 IOC容器初始化流程

IOC容器初始化的大致步骤如下:

  1. 从XML文件、Java类或其他地方加载配置元数据。
  2. 通过 BeanFactoryPostProcessor 对配置元数据进行一轮处理。
  3. 初始化Bean实例,并根据给定的依赖关系组装对象。
  4. 通过BeanPostProcessor对Bean进行处理,期间还会触发Bean被构造后的回调方法。

比如有一个基本的类,在没有IOC容器时,我们需要通过new新建一个实例,然后把它传给具体要调用它的对象,以此来管理它的生命周期。

如果是把实例交给Spring托管,则可以选择一种方式装配这个Bean,比如用XML的方式,通过</beans>标签来配置。

然后就需要将配置文件载入容器。BeanFactory默认使用DefaultListableBeanFactory这个实现类,他不关心配置的方式,比如用能读取XML文件的XmlBeanDefinitionReader来读取元数据,通过它来加载CLASSPATH中的beans.xml文件,将其保存到DefaultListableBeanFactory中,代码如下:

public class Application {
    private BeanFactory beanFactory;

    public static void main(String[] args) {
        Application application = new Application();
        application.sayHello();
    }

    public Application() {
        beanFactory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = 
            new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory );
        reader.loadBeanDefinitions("beans.xml");
    }

    public void sayHello() {
        // Hello hello = (Hello) beanFactory.getBean("hello");
        Hello hello = beanFactory.getBean("hello", Hello.class);
        hello.hello().sout;
    }
}

在实际工作中,更多情况下会用到ApplicationContext的各种实现,ApplicationContext接口继承了BeanFactory,在BeanFactory的基础上提供了更丰富的功能,例如事件传播、资源加载、国际化支持等。

常见实现例如:

ClassPathXmlApplicationContext——从CLASSPATH中加载XML文件来配置ApplicationContext;

FileSystemXmlApplicationContext——从文件系统中加载XML文件来配置ApplicationContext;

AnnotationConfigApplicationContext——根据注解和Java类配置ApplicationContext。

使用ApplicationContext也会比BeanFactory更方便一些,因为我们无须自己去注册很多内容,比如AnnotationConfigApplicationContext把常用的一些后置处理器都直接注册好了,所以在绝大多数情况下,建议大家使用ApplicationContext的实现类。

使用ApplicationContext后的代码变为:

public class Application {
    private ApplicationContext applicationContext;

    public static void main(String[] args) {
        Application application = new Application();
        application.sayHello();
    }

    public Application() {
        applicationContext = new ClassPathXmlApplicationContext("beans.xml");
    }

    public void sayHello() {
        Hello hello = applicationContext.getBean("hello", Hello.class);
        hello.hello().sout;
    }
}

3.2 Bean的生命周期

 一个Bean先要经过对象的创建(也就是通过new关键字创建一个对象),随后根据容器里的配置注入所需的依赖,最后调用初始化的回调方法,经过这三个步骤才算完成了Bean的初始化。

若不再需要这个Bean,则要进行销毁操作,在正式销毁对象前,会先调用容器的销毁回调方法。

由于这些都是由Spring管理的,所以我们无法任意地在Bean创建后或Bean销毁前增加某些操作。为此,Spring Framework为我们提供了几种途径,可以在这两个时间点调用我们提供给容器的回调方法:

  • 实现InitializingBean和DisposableBean接口
    • 分别用前者的afterPropertiesSet()方法和后者的destory()方法来操作两个时间点的动作。
  • 使用JSR-250的@PostConstruct和@PreDestory注解
    • 在配置类的方法上面添加上面两个注解
  • 在<bean/>或@Bean里配置初始化和销毁方法
    • 在XML文件中配置属性或在@Bean里面使用initMethod、destroyMethod属性,值为要调用的方法名。

另外,如果上述几种方法都使用了,Spring的顺序会按照上述顺序进行调用。

3.2.1 容器的初始化

上面的Bean的生命周期没有包括容器的初始化过程,在这里补充避免遗漏。

  1. 确认要使用的容器,如果是Spring一般是ClassPathXmlApplicationContext,如果是SpringBoot一般是AnnotationConfigApplicationContext。
  2. 初始化BeanFactory,扫描Aware接口,并把BeanName、BeanDefinition(定义 Bean 的配置元信息接口,里面存放了Bean的各种信息)放到BeanFactory缓存中。
  3. 触发BeanFactoryPostProcessor,它会在BeanFactory加载所有Bean定义但尚未对其初始化时介入。它的用法与BeanPostProcessor类似,可以对容器进行一些回调操作。

4. Spring其他用法

4.1 Aware接口

在大部分情况下,我们的Bean感知不到Spring容器的存在,也无须感知。但总有那么一些场景中要用到容器的一些特殊功能,比如如果希望在Bean中获取容器信息,可以通过如下两种方式:

  • 实现BeanFactoryAware或ApplicationContextAware接口
  • 用@Autowired注解来注入BeanFactory或ApplicationContext

两种方式的本质都是一样的,即让容器注入一个BeanFactory或ApplicationContext对象。

        在拿到ApplicationContext(Spring上下文)后,就能操作该对象,比如调用getBean()方法取得想要的Bean。这个接口相比其他的Aware接口使用率更高一些。

        如果Bean希望获得自己在容器中定义的Bean名称,可以实现BeanNameAware接口,这个接口的setBeanName()方法是注入一个代表名称的字符串,这算是一个依赖,因此会在Bean的初始化方法前被执行。

        如果要用到事件机制,需要用到ApplicationEventPublisher来发送事件,可以通过实现ApplicationEventPublisherAware接口,从容器中获取到ApplicationEventPublisher实例,这个会在下面4.2详细讲到。

4.2 事件机制

ApplicationContext提供了一套事件机制,在容器发生变动时我们可以通过ApplicationEvent的子类通知到ApplicationListener接口的实现类(或增加@EventListener注解),做对应的处理。例如ApplicationContext在启动、停止、关闭和刷新时,分别会发出ContextStartEvent、ContextStoppedEvent、ContextClosedEvent和ContextRefreshEvent事件,这些事件可以让我们有机会感知当前容器的状态。

我们也可以自己监听这些事件,只需实现ApplicationListener接口或在某个Bean方法上增加@EventListener注解即可。通过这个来自定义事件,不过该事件必须继承ApplicationEvent,而且产生事件的类需要实现ApplicationEventPublisherAware,还要从上下文中获取到ApplicationEventPublisher——可以通过上下文applicationContext.publishEvent或通过实现了ApplicationEventPublisherAware接口的类里面直接获得。

Spring的内置事件以及自定义事件其实就是设计模式的观察者模式,关于自定义事件的实现可以参考一下我之前写过的 设计模式-观察者模式

4.3 容器的扩展点

Spring容器时非常灵活的,Spring Framework中有很多机制是通过容器本身的扩展点来实现的,比如Spring AOP等。

BeanPostProcessor接口是用来定制Bean的,这个接口是Bean的后置处理器,接口中有两个方法,postProcessBeforeInitialization()方法在Bean初始化前执行,PostProcessAfterInitialization()方法在Bean初始化之后执行。如果有多个BeanPostProcessor,可以通过@Order注解来指定运行的顺序。

如果说BeanPostProcessor是Bean的后置处理器,那BeanFactoryPostProcessor就是BeanFactory的后置处理器,我们可以通过它来定制Bean的配置元数据,其中postProcessBeanFactory()方法会在BeanFactory加载所有Bean定义但尚未对其进行初始化时介入。

另外,Spring AOP也是通过BeanPostProcessor实现的,因此实现该接口的类,以及其中直接引用的Bean都会被特殊对待,不会被AOP增强。此外,BeanPostProcessor和BeanFactoryPostProcessor都仅对当前容器上下文的Bean有效,不会去处理其他上下文。

5. SpringBean作用域、循环依赖

5.1 Spring Bean的作用域

Spring Framework的作用域基础分为两种:单例-singleton、原型(或者叫多例)-protorype。在2.5之后又针对Web服务增加了request、session、globalSession,如果当前容器是web容器,就可以支持。

  • singleton:Spring的默认作用域,容器里拥有唯一的Bean实例。如果是有状态Bean(具有数据存储功能)可能会有线程安全问题。
  • protorype:每个getBean请求,容器都会创建一个Bean实例。类似ThreadLocal。
  • request:会为每个Http请求创建一个Bean实例。
  • session:会为每个session创建一个Bean实例。
  • globalSession:会为每个全局Http Session创建一个Bean实例,该作用域仅针对Protlet有效,在Servlet的web容器中效果同session。

5.2 循环依赖

循环依赖就是指两个类互相依赖注入了对方,形成闭环,例如A注入了B,B类又注入了A。在大部分场景下Spring会帮我们解决循环依赖问题,但某些场景却无法解决而导致启动时报错。

无法解决循环依赖的情况:

  1. 主Bean A以构造函数的方式依赖注入类B。原因是Spring注入的时候在放入三级缓存之前会经过一个步骤,如果是构造函数的方式就先创建,此时因为另一个Bean也没有在三级缓存中,就会出现问题。——也可以用这种方式检测代码中是否有循环依赖。
  2. Bean的作用域是多例(原型)的。因为多例的Bean每次调用getBean方法都会创建一次,所以是没有缓存的,不会把它放到三级缓存中。

Spring正常解决循环依赖的原理:

实例化Bean可以大致分成两步:通过反射创建对象,对该对象里面的属性赋值。当对象A创建出来后,不会马上往里面赋值,会先放到三级缓存中,接着Spring发现它需要依赖注入对象B,会先去创建对象B,同样放到三级缓存中。然后对象A会依次从一、二、三级缓存中查找B,如果是在三级缓存中查找到,会放入到二级缓存中。

缓存字段名缓存级别对象类型解释
singletonObjects1Map<String,Object>存储的是所有创建好了的单例Bean
earlySingletonObjects2Map<String,Object>保存的是创建好但还没有进行属性注入的Bean
singletonFactories3Map<String,ObjectFactory<?>>提前暴露的单例工厂,生成Bean并放入到二级缓存

6. 总结、直接背

6.1 谈谈你对IOC的理解

        IOC控制反转是Spring最重要的两个概念之一,控制反转是一种决定容器如何装配组件的模式,在Spring Framework的官方文档中有一张图,表达的是将业务对象(也就是Bean)和关于组件的配置元数据(比如依赖关系)输入Spring容器中,容器就能为我们组装出一个可用的系统。

        IOC的思想是把对象的控制者从使用者变为由Spring来管理,通过DI依赖注入来实现。

        依赖注入就是把对应的属性的值注入到具体的对象中,其中很重要的一块是管理依赖——就是管理Bean之间的依赖。有两种基本的注入方式——构造方法注入、setter方法注入。不过除了这两种手动注入,Spring还提供了自动织入的机制,可以在<bean/>(bean标签)中通过autowire属性来设置使用byName、byType或constructor的方式自动织入。此外,在Spring Framework2.5之后又增加了通过注解来实现自动织入。

        可讲可不讲:Bean有三种配置方式,分别是基于XML文件的配置、基于注解的配置和基于Java类的配置。XML文件的配置就是通过bean标签中的各种属性来配置Bean;注解的配置是先通过<context:component-scan>标签或@ComponentScan注解来指定要扫描的包名,然后用Spring引入的@Component、@Service、@Repository、@Controller注解来配置类;Java类的配置就是通过使用@Configuration、@Bean、@ComponentScan等一系列注解来创建配置类。


        然后我再讲一下IOC容器的初始化流程吧,IOC容器初始化的大致步骤是:

  1. 从XML文件、Java类或其他地方加载配置元数据。
  2. 通过 BeanFactoryPostProcessor 对配置元数据进行一轮处理。
  3. 初始化Bean实例,并根据给定的依赖关系组装对象。
  4. 通过BeanPostProcessor对Bean进行处理,期间还会触发Bean被构造后的回调方法。

在实际工作中,更多情况下会用到ApplicationContext的各种实现,ApplicationContext接口继承了BeanFactory,比如根据注解和Java类配置ApplicationContext的AnnotationConfigApplicationContext,它把常用的一些后置处理器都直接注册好了。

然后Bean的生命周期可以细化为:创建对象、注入依赖、创建后回调、正常使用、销毁前回调、销毁对象,在经过创建后的回调方法后才算是完成了Bean的初始化。创建后回调和销毁前回调时Spring开放了几种途径来操作,比如实现InitializingBean和DisposableBean接口、在配置类的方法上面使用@PostConstruct和@PreDestory注解、在XML文件中配置<bean/>标签或在@Bean注解里面使用initMethod、destroyMethod属性,值为要调用的方法名。


        还要继续说的话,再讲下扩展点:Spring还提供了Aware接口、事件机制和扩展点来满足我们的一些开发需要。Aware接口比如实现BeanFactoryAware或ApplicationContextAware接口可以获取Spring上下文,实现ApplicationEventPublisherAware接口可以从容器中获取到ApplicationEventPublisher实例,用来发送事件;事件机制就是在容器发生变动时我们可以通过ApplicationEvent的子类通知到ApplicationListener接口的实现类(或增加@EventListener注解),做对应的处理。例如ApplicationContext在启动、停止、关闭和刷新时,会发出各种ContextEvent事件,这些事件可以让我们有机会感知当前容器的状态。也可以自定义事件,也就是设计模式的观察者模式,该事件必须继承ApplicationEvent,而且产生事件的类需要实现ApplicationEventPublisherAware。然后Spring中有很多机制是通过容器本身的扩展点来实现的,比如Spring AOP,它是通过BeanPostProcessor接口来实现的,接口中有两个方法,postProcessBeforeInitialization()方法在Bean初始化前执行,PostProcessAfterInitialization()方法在Bean初始化之后执行。如果有多个BeanPostProcessor,可以通过@Order注解来指定运行的顺序。同样BeanFactoryPostProcessor也可以用来定制Bean的配置元数据,其中postProcessBeanFactory()方法会在BeanFactory加载所有Bean定义但尚未对其进行初始化时介入。


        最后还不喊停?那再加个Spring的循环依赖:Spring在依赖注入的时候还可能会有循环依赖问题,在大部分情况下Spring会帮我们解决,但如果是通过构造方法的注入,或者是Bean的作用域选择了protorype,就会在容器启动时就报错。

        为什么这两种情况无法解决?:构造方法是因为Spring注入的时候在放入三级缓存之前会经过一个步骤,如果是构造函数的方式就先创建,此时因为另一个Bean也没有在三级缓存中,就会出现问题;protorype是因为多例的Bean每次调用getBean方法都会创建一次,所以是没有缓存的,不会把它放到三级缓存中。

        一般情况下Spring是是怎么解决的?:实例化Bean可以大致分成两步:通过反射创建对象,对该对象里面的属性赋值。当对象A创建出来后,不会马上往里面赋值,会先放到三级缓存中,接着Spring发现它需要依赖注入对象B,会先去创建对象B,同样放到三级缓存中。然后对象A会依次从一、二、三级缓存中查找B,如果是在三级缓存中查找到,会放入到二级缓存中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值