Spring Batch学习与实践(一)

目录

简单介绍

核心架构

Spring Batch优势

基本概念

概述

SpringBatch主要领域对象

重要概念的说明

Job Instance

Job Parameters

Job Execution

Spring Batch元数据架构

Job配置和运行

Job

Job拦截器

JobParametersValidator

继承父Job的配置

一个完整的Job:

配置Job Repository

配置Job Launcher

启动Job

元数据的使用 

配置Step

块处理

Step重启

跳过

 Step重试

回滚控制

Step拦截器

TaskletStep

控制步骤流

Split Flows 


简单介绍

核心架构

如下所示为Spring Batch的三层架构:应用层、核心层、基础架构层。

  1. 应用层:包含所有批处理作业,通过Spring框架管理程序员自定义的代码;
  2. 核心层:包含Spring Batch启动和控制所需的核心类,如:JobLauncher、Job、Step等。
  3. 基础架构层:提供通用的读、写和服务处理。

Figure 1.1: Spring Batch Layered Architecture

Spring Batch优势

  1. 丰富的开箱即用组件
  2. 面向Chunk的处理(多次读,一次写)
  3. 默认采用Spring提供的声明式事务管理模型
  4. 元数据管理(自动记录Job执行情况)
  5. 提供多种监控技术,对批处理操作进行监控(查看数据库记录、API、Spring Batch Admin监控、JMX控制台查看)
  6. 支持顺序任务、条件任务等任务流程
  7. 支持作业的跳过、重试、重启能力,避免因为错误导致批处理作业异常中断。
  8. 易扩展,如远程分块、分区等。

基本概念

概述

如下图所示,批处理操作原型。每个作业(Job)有一个到多个步骤(Step),每个步骤都只有一个ItemReader,一个ItemProcessor和一个ItemWriter;需要作业启动器( JobLauncher),并且需要存储有关当前正在运行的进程的元数据(JobRepository)。

å¾2.1ï¼æ¹å¤çåå

SpringBatch主要领域对象

  1. Job Repositroy:作业仓库。负责Job/Step执行过程中的状态保存,为JobLaucher、Job、Step提供标准的CRUD实现。
  2. Job Launcher:作业调度器。提供执行Job的入口;根据给定的Job Paramters执行作业。
  3. Job:作业。由多个Step组成,封装整个批处理操作。Batch操作的基础单元。
  4. Job Instance:作业实例。每个作业执行时,都会生成一个实例,实例会被存放在JobRepository中,如果作业失败,下次重新执行作业时,会使用同一个作业实例。
  5. Job Parameters:作业参数。它是一组用来启动批处理任务的参数,在启用Job时,可以设置任何需要的作业参数,需要注意的作业参数会用来标识作业实例,即不同的作业实例是通过Job参数来区分的。
  6. Job Execution:作业执行器。负责具体的Job的执行,每次运行Job都会启动一个新的Job执行器。
  7. Step:作业步。Job的一个执行环节
  8. Step Execution:作业步执行器。负责具体的Step的执行,每次运行Step都会启动一个新的Step执行器。
  9. Tasklet:Step中具体执行逻辑的操作,可以重复执行,可以设置具体的同步、异步操作等。
  10. Execution Context:执行上下文。他是一组框架持久化与控制的key/value对,能够让开发者在Step Execution 或 Job Execution 范畴保存需要进行持久化的状态。
  11. Chunk:块。给定数量Item的集合,可以定义对Chunk的读操作、处理操作、写操作、提交间隔等。
  12. Iem:条目。一条数据记录。
  13. ItemReader:条目读取器。从数据源(文件系统、数据库、队列等)中读取Item,框架提供了很多开箱即用的条目读取器实现,具体可参文档
  14. ItemProcessor:条目处理器 。在Item写入数据源之前,对数据进行处理(如:数据清洗、转换、过滤、校验等)。
  15. ItemWriter:条目写入器。将Item批量写入数据源(文件系统、数据库、队列等)。矿机提供了很多开箱即用的条目吸入器实现,具体可参文档

重要概念的说明

Job Instance

  • Job Instance 来源:根据设置的Job Parameters从Job Repository中获取一个;如果没有获取到,则新创建。
  • Job通过Job Paramters区分不同的Job Instance 。即 Job Name + Job Paramters 可以唯一的确定一个Job Instance。
  • 在第一次执行Job的时候,会创建一个新的Job Instance ,但是每次执行Job的时候都会创建一个新的 Job Execution。
  • 已经完成的Job Instance 不能被重复执行。
  • 在同一时刻,只能有一个Job Execution 可以执行同一个Job Instance。

Job Parameters

Job Parameters支持的数据类型:String/Long/Date/Double,可以通过JobParametersBuilder构造参数。

new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();

Job Execution

要注意其中 一些重要属性:status(执行状态)、startTime(开始时间)、endTime(结束时间)、exitStatus(任务运行结果)、createTime(执行器第一次持久化时间)、lastUpdated(最近一次持久化时间)、executionContext(运行过程中需要被持久化的用户数据)、failureException(任务执行过程中的异常列表)。

Spring Batch元数据架构

在Spring Batch架构中提供两种元数据的存储方式,即内存、数据库。其中数据库脚本在core包中。其结构图下图所示。在Spring Batch框架中元数据指的是:Job Instance、Job Execution、Job Parameters、Step Execution、Execution Context等数据。

对数据库表的字段的具体含义可以参考Spring Batch的官方文档,其中有详细的描述。

Spring Batchåæ°æ®ERD
Spring Batch元数据ER图

 

Job配置和运行

å¾2.1ï¼å¸¦æ­¥éª¤çä½ä¸å±æ¬¡ç»æ
带步骤的作业层次结构

Job

Job的主要属性包括:id【作业唯一标识】、job-repository【定义作业仓库,默认jobRepository】、incrementer【作业参数递增器】、restartable【作业是否可以重启,默认true】、parent【指定改作业的父作业】、abstract【定义作业是否为抽象作业】。

Job主要的子元素包括Step【定义作业步】、split【定义并行作业步】、flow【引用独立定义的作业流】、decision【定义作业步执行的条件判断器、listeners【定义作业拦截器】、validator【作业参数校验器】

与Java的抽象类相似,Job也有将公共属性抽取出来,定义为AbstractJob。

Job的配置方式有两种,基于Java或者基于xml,如下代码所示:

@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .preventRestart()    //不允许重启
                     .listener(sampleListener())    //定义拦截器
                     .validator(parametersValidator())    //定义参数校验
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .end()
                     .build();
}
<job id="footballJob" restartable="false">
    <step id="playerload" next="gameLoad"/>
    <step id="gameLoad" next="playerSummarization"/>
    <step id="playerSummarization"/>
    <validator ref="parametersValidator"/>
    <listeners>
        <listener ref="sampleListener"/>
    </listeners>
</job>

Job拦截器

在配置job的时候,可以首先定义拦截器,是的在Job执行前后加入自定义的业务逻辑。Spring Batch框架默认提供的实现:CompositeJobExecutionListener【拦截器组合器,支持配置一组拦截器】、JobExecutionListenerSupport【拦截器空实现,可以直接继承,仅覆写关系的方法】。

如果使用拦截器要注意拦截器异常对Job的影响。如果定义多个拦截器,则before方法按照listener的顺序执行,after方法按照相反的顺序执行。

public interface JobExecutionListener {

    void beforeJob(JobExecution jobExecution);

    void afterJob(JobExecution jobExecution);

}

JobParametersValidator

参数校验接口只有一个方法validate() ,框架提供了两个默认的实现类CompositeJobParametersValidatorDefaultJobParametersValidator

void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException

继承父Job的配置

如果一组Jobs共享相似但不相同的配置,则定义一个Job具体的Jobs可以从其继承属性的“父级”可能会有所帮助 。与Java中的类继承类似,“子代”Job会将其元素和属性与父代结合在一起。

<job id="baseJob" abstract="true">
    <listeners>
        <listener ref="listenerOne"/>
    <listeners>
</job>

<job id="job1" parent="baseJob">
    <step id="step1" parent="standaloneStep"/>

    <listeners merge="true">
        <listener ref="listenerTwo"/>
    <listeners>
</job>

一个完整的Job:

@Configuration
 //Spring 家庭中的@Enable* 系列注解功能很类似,让我们可以运行Spring Batch。
@EnableBatchProcessing   
@Import(DataSourceConfiguration.class)
public class AppConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    /**
     * Job工厂构建器,构建Job 
    */
    @Bean
    public Job job(@Qualifier("step1") Step step1, @Qualifier("step2") Step step2) {
        return jobs.get("myJob").start(step1).next(step2).build();
    }

    /**
     * Step工厂构建器,构建Step 
    */
    @Bean
    protected Step step1(ItemReader<Person> reader,
                         ItemProcessor<Person, Person> processor,
                         ItemWriter<Person> writer) {
        return steps.get("step1")
            .<Person, Person> chunk(10)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
    }

    @Bean
    protected Step step2(Tasklet tasklet) {
        return steps.get("step2")
            .tasklet(tasklet)
            .build();
    }
}

配置Job Repository

使用时@EnableBatchProcessingJobRepository开箱即用的,但是我们也可以自定义Job Repository。同样是可以采用xml和Java两种形式。使用Java配置的时候,我们可以直接实现BatchConfigurer.java来完成,当然为了简便起见,我们可以继承DefaultBatchConfigurer,然后覆写其中的createJobRepository() 。代码示例如下:

@Override
protected JobRepository createJobRepository() throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);    //设置数据源
    factory.setTransactionManager(transactionManager);    //设置事务管理
    factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");    。。设置隔离级别
    factory.setTablePrefix("BATCH_");    //设置表前缀
    factory.setMaxVarCharLength(1000);   //设置字符串最大长度
    return factory.getObject();
}

配置Job Launcher

@Override
protected JobLauncher createJobLauncher() throws Exception {
	SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
	jobLauncher.setJobRepository(jobRepository);
	jobLauncher.afterPropertiesSet();
	return jobLauncher;
}

JobLauncher接口的简单实现。Spring Core TaskExecutor接口用于启动作业。默认设置使用同步任务执行器。SimpleJobLauncher是JobLauncher接口最基本的实现。在配置JobLaucher时,情况是同步的,时序图如【Job Launcher序列】所示;但是在很多情况下,可能存在超时问题,所以可以采用异步的方式执行作业,其时序图如【异步作业启动器序列】所示。当然此时需要在createJobLauncher() 中配置属性TaskExecutor,jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());

 

ä½ä¸å¯å¨å¨åºå
Job Launcher序列
å¼æ­¥ä½ä¸å¯å¨å¨åºå
异步作业启动器序列

启动Job

 有很多种方式可以启动Job。现在主要介绍两中,也是在开发实践中最常用的两张,通过命令行启动、从web容器中启动。

1. 命令行启动

启动作业的脚本必须启动Java虚拟机,所以需要一个具有main方法的类作为主要入口点。Spring Batch提供了一个实现此目的的实现: CommandLineJobRunner。他执行四项任务:

  • 加载适当的 ApplicationContext

  • 将命令行参数解析为 JobParameters

  • 根据参数找到合适的工作

  • 使用JobLauncher应用程序上下文中提供的启动工作。

以下示例显示了作为作业参数传递给以XML定义的作业的日期:

<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay schedule.date(date)=2007/05/05

以下示例显示了作为作业参数传递给Java中定义的作业的日期:

<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay schedule.date(date)=2007/05/05

默认情况下,CommandLineJobRunner使用DefaultJobParametersConverter来隐式将键/值对转换为标识作业参数。但是,可以通过在其前面加上+来显式指定正在识别的作业参数,用-表示不用识别哪些作业参数。如下代码所示,schedule.date是标识作业参数,而vendor.id不是。

<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay +schedule.date(date)=2007/05/05 -vendor.id=123

2. web容器启动

Web容å¨ä¸­çå¼æ­¥ä½ä¸å¯å¨å¨åºå
 Web容器中的异步作业启动器序列

如上图所示,为异步作业,需要在设置JobLaauncher时,设置为异步。代码示例如下:

@Controller
public class JobLauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

    @RequestMapping("/jobLauncher.html")
    public void handle() throws Exception{
        jobLauncher.run(job, new JobParameters());
    }
}

元数据的使用 

这一部分可以参考官方文档

ä½ä¸å­å¨åºé«çº§
作业存储库访问

 

配置Step

Step是一个域对象,它封装了批处理作业的一个独立的顺序阶段,并包含定义和控制实际批处理所需的所有信息。

æ­¥
Step

下面的代码块展示了Step的配置过程。依旧是从XML和JAVA两种方式来演示。Step配置后续内容将围绕这个展开。如下配置中配置了Job Repository、事务管理、快大小、读处理器、写处理器。

<job id="sampleJob" job-repository="jobRepository">
    <step id="step1">
        <tasklet transaction-manager="transactionManager">
            <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
        </tasklet>
    </step>
</job>
/**
 * 注意,JobRepository通常是自动导入的,不需要显式配置
 */
@Bean
public Job sampleJob(JobRepository jobRepository, Step sampleStep) {
    return this.jobBuilderFactory.get("sampleJob")
    			.repository(jobRepository)
                .start(sampleStep)
                .build();
}

/**
 * 注意,TransactionManager通常是自动导入的,不需要显式配置
 */
@Bean
public Step sampleStep(PlatformTransactionManager transactionManager) {
	return this.stepBuilderFactory.get("sampleStep")
				.transactionManager(transactionManager)
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				.build();
}

Step与Job相类似,当需要将一些公共属性抽取出来,或者有些Step不需要完整的定义ItemReader、ItemWriter时,导致无法实例化时,可以将Step定义为抽象的。子类在继承抽象Step的时候,可以覆写其中的属性,对于可配置的属性或者元素,也可以合并子类和父类的元素。

如下配置所示,父Step声明为抽象的,子Step继承了父Step后,覆写了commit-interval属性,然后通过merge属性,拦截器合并。

<step id="listenersParentStep" abstract="true">
    <listeners>
        <listener ref="listenerOne"/>
    <listeners>
    <tasklet>
        <chunk commit-interval="10"/>
    </tasklet>
</step>

<step id="concreteStep3" parent="listenersParentStep">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
    </tasklet>
    <listeners merge="true">
        <listener ref="listenerTwo"/>
    <listeners>
</step>

块处理

面向块的处理是指一次读取一个数据并创建在事务边界内写出的“块”。从读入一项ItemReader,交给一项ItemProcessor,然后汇总。一旦读取的项目数等于提交间隔,整个块就由写入 ItemWriter,然后提交事务。下图显示了该过程:

åå¤ç
块处理时序图

 通常情况下,Job会配置为事务作业,此时如果每read()一次就write()的话,开销比较大,影响系统性能。所以往往会,通过设置commit-interval来配置块大小,当read()commit-interval个Item的时候,一次性将其写入。以提高系统性能。

Step重启

Spring Batch框架支持状态为非“COMPELETED”的Job实例重新启动,Job实例重启的时候,会从当前失败的Step重新开始执行,同时可以通过start-limit属性控制任务启动的次数。默认情况下,Step Instance可以无限次重复启动;而Job Instance重启时,不会再次执行已经完成的任务,当然可以通过设置allow-start-if-complete来设置。

<step id="step1">
    <tasklet start-limit="1">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

<step id="step2">
    <tasklet allow-start-if-complete="true">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

跳过

在实际生产环境中,跳过错误非常有用,它能够让我们不因为少量的错误影响整个批处理过程。可以通过skip-limit和<skippable-exception-classes>配置跳过策略,如果想要自定义的话,可以继承SkipPolicy接口。如下的配置中,允许跳过的最大次数10次,当发生除FileNotFoundException.class之外的异常时,会跳过,直至跳过次数大于10次后,才会导致Job失败。

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(flatFileItemReader())
				.writer(itemWriter())
				.faultTolerant()
				.skipLimit(10)            //跳过次数
				.skip(Exception.class)    //跳过异常
				.noSkip(FileNotFoundException.class)    //不跳过的异常
				.build();    
}

 Step重试

Spring Batch框架提供了任务重试功能,重试次数限制功能、自定义重试策略以及重试拦截。可以通过retryable-exception-classes、retry-limit、retry-policy、cache-capacity、retry-listeners来实现重试。如下所示,配置了重试上线为3次,可以重试的异常DeadlockLoserDataAccessException.class。

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.faultTolerant()
				.retryLimit(3)
				.retry(DeadlockLoserDataAccessException.class)
				.build();
}

回滚控制

默认情况下,无论重试还是跳过,从ItemWriter引发的任何异常都会导致由Step控制的事务回滚。如果配置了skip,则从ItemReader引发的异常不会导致回滚。在有些情况下,ItemWriter发生的异常也不需要回滚,可以通过如下配置实现这一点。如果发生ValidationException.class异常不会回滚。当然,可以通过xml的no-rollback-exception-classes属性配置无序无须回滚的异常。

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.faultTolerant()
				.noRollback(ValidationException.class)
				.build();
}

 ItemReader的基本约定是仅向前。 该步骤缓冲读取器的输入,因此在回滚的情况下,不需要从读取器中重新读取项目。 但是,在某些情况下,阅读器是建立在诸如JMS队列之类的事务资源之上的。 在这种情况下,由于队列与回滚的事务相关联,因此将从队列中拉出的消息放回原处。 因此,可以将该步骤配置为不缓冲项目。

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.readerIsTransactionalQueue()
				.build();
}

关于Spring事务属性的更过设置可以参考官方文档

Step拦截器

与Job拦截器相类似,Step也有自己的拦截器。具体可以参照官方文档。文档中会介绍的StepExecutionListener、ChunkListener、ItemReadListener、ItemProcessListener、ItemWriteListener的使用,以及如何跳过监听。

TaskletStep

面向块的处理并不是一步处理的唯一方法。如果一个步骤必须由一个简单的存储过程调用组成。除了调用实现为ItemReader,并在过程结束后返回null,并且添加一个无操作的ItemWriter。

Spring Batch为这个场景提供了一种更为优雅的实现——TaskletStep。

Tasklet是一个简单的接口,它有一个方法execute,它被TaskletStep重复调用,直到它返回RepeatStatus。完成或抛出异常来表示失败。对Tasklet的每个调用都包装在一个事务中。Tasklet实现者可以调用存储过程、脚本或简单的SQL update语句。与ItemReaderItemWriter接口的其他适配器一样,该Tasklet 接口包含一个实现,使自己可以适应任何现有的类:TaskletAdapter

@Bean
public Step step1() {
    return this.stepBuilderFactory.get("step1")
    			.tasklet(myTasklet())
    			.build();
}

@Bean
public MethodInvokingTaskletAdapter myTasklet() {
	MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();

	adapter.setTargetObject(fooDao());
	adapter.setTargetMethod("updateFoo");

	return adapter;
}

应用场景示例,设置作业删除文件

@Bean
public Job taskletJob() {
	return this.jobBuilderFactory.get("taskletJob")
				.start(deleteFilesInDir())
				.build();
}

@Bean
public Step deleteFilesInDir() {
	return this.stepBuilderFactory.get("deleteFilesInDir")
				.tasklet(fileDeletingTasklet())
				.build();
}

@Bean
public FileDeletingTasklet fileDeletingTasklet() {
	FileDeletingTasklet tasklet = new FileDeletingTasklet();

	tasklet.setDirectoryResource(new FileSystemResource("target/test-outputs/test-dir"));

	return tasklet;
}

//自定义Tasklet
public class FileDeletingTasklet implements Tasklet, InitializingBean {

    private Resource directory;

    public RepeatStatus execute(StepContribution contribution,
                                ChunkContext chunkContext) throws Exception {
        File dir = directory.getFile();
        Assert.state(dir.isDirectory());

        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
            boolean deleted = files[i].delete();
            if (!deleted) {
                throw new UnexpectedJobExecutionException("Could not delete file " +
                                                          files[i].getPath());
            }
        }
        return RepeatStatus.FINISHED;
    }

    public void setDirectoryResource(Resource directory) {
        this.directory = directory;
    }

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(directory, "directory must be set");
    }
}

控制步骤流

Step流有两种执行方式:顺序流和条件流。

//顺序流
@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(stepA())
				.next(stepB())
				.next(stepC())
				.build();
}
/*
 *条件流
 *通配符:
 *       “*”匹配零个或多个字符
 *       “?” 完全匹配一个字符
 * 当stepA()返回状态为FAILED时执行stepC,否则执行StepB
*/
@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(stepA())
				.on("*").to(stepB())
				.from(stepA()).on("FAILED").to(stepC())
				.end()
				.build();
}

在为条件流配置作业时,务必理解BatchStatus和ExitStatus之间的区别。BatchStatus是一个枚举,它是JobExecution和StepExecution的属性,框架使用它来记录作业或步骤的状态。它可以是以下值之一:已完成、开始、开始、停止、停止、失败、放弃或未知。其中大多数是自说明的:完成是在步骤或作业成功完成时设置的状态,失败是在步骤或作业失败时设置的状态,依此类推。在使用Java配置条件流时,on()函数的参数值来自于ExitStatus。

如果没有为某个步骤定义转换,则作业的状态定义如下:

  • 如果该步骤以ExitStatus失败告终,则作业的BatchStatus和ExitStatus都失败。
  • 否则,该作业的BatchStatus和ExitStatus都将完成。

Split Flows 

使用并行流配置Job。

@Bean
public Flow flow1() {
	return new FlowBuilder<SimpleFlow>("flow1")
			.start(step1())
			.next(step2())
			.build();
}

@Bean
public Flow flow2() {
	return new FlowBuilder<SimpleFlow>("flow2")
			.start(step3())
			.build();
}

@Bean
public Job job(Flow flow1, Flow flow2) {
	return this.jobBuilderFactory.get("job")
				.start(flow1)
				.split(new SimpleAsyncTaskExecutor())
				.add(flow2)
				.next(step4())
				.end()
				.build();
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值