Dubbo深入源码学习-与Spring的整合探究

Dubbo深入源码学习-与Spring的整合探究

在正式进入源码学习之前,我们先对Dubbo整体整合的架构进行一个了解。

image-20231206083920325

我们知道当我们使用Dubbo的时候,需要配置注册中心、协议、应用名称等信息,这些信息如何被Spring中的Bean接收,还有标志@Service、@Reference注解的类,如何注入到Spring中,以及Spring如何进行管理,这都是我们看源码去分析的地方。其实根据以上架构图,整个Spirng和Dubbo的整合分为几个部分

1.启动Spring

2.处理配置文件,解析Dubbo配置生成配置对象,并把配置信息加载到配置对象中

3.处理@Service注解,生成一个ServiceBean对象和服务实现类对象

4.将配置对象、服务实现类对象和ServiceBean对象关联起来

5.服务导出

6.处理@Reference注解

7.服务引入,生成ReferemceBean对象以及接口服务的代理对象

在这一篇中,我们分三个步骤分析Spirng和Dubbo的整合机制。

1.处理Dubbo配置信息

2.处理@Service注解

3.处理@Reference注解

1.处理Dubbo配置信息

image-20231206084828193

如图所示,当我们使用Dubbo的时候,需要配置应用名称,注册中心,协议等信息。这些信息的解析以及和Spring的Bean是如何绑定的呢?

这都要从一个注解开始分析:

@EnableDubbo

image-20231206085205841

在这个注解上有还有两个比较重要的注解:@EnableDubboConfig和@DubboComponentScan

@EnableDubboConfig注解,作用是解析配置文件,把Dubbo的配置信息生成配置对象放在Spring容器中

image-20231206085433627

在@EnableDubboConfig注解上,通过@Import注解往Spring容器中注册了一个DubboConfigConfigurationRegistrar对象

image-20231206085521825

DubboConfigConfigurationRegistrar类实现了ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法可以往Spring容器中注入一些Bean对象(Spring的知识)。

我们就先看看registerBeanDefinitions方法,这里注入了哪些Bean呢?

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		//1.解析EnableDubboConfig注解的multiple属性,代表是单例还是多例,默认是true
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
                importingClassMetadata.getAnnotationAttributes(EnableDubboConfig.class.getName()));

        boolean multiple = attributes.getBoolean("multiple");

        // 2.注册单例配置
        registerBeans(registry, DubboConfigConfiguration.Single.class);
		
        if (multiple) {
            // 3.注册多例配置
            registerBeans(registry, DubboConfigConfiguration.Multiple.class);
        }

        // Register DubboConfigAliasPostProcessor
        registerDubboConfigAliasPostProcessor(registry);

        // Register NamePropertyDefaultValueDubboConfigBeanCustomizer
        registerDubboConfigBeanCustomizers(registry);

    }

image-20231206085948310

接下来我们先看 registerBeans(registry, DubboConfigConfiguration.Single.class)这个方法。

    public static void registerBeans(BeanDefinitionRegistry registry, Class<?>... annotatedClasses) {

        if (ObjectUtils.isEmpty(annotatedClasses)) {
            return;
        }

        //省略

        AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(registry);

  
		
        reader.register(annotatedClasses);

    }

其实就是把DubboConfigConfiguration.Single.class)这个类注入到容器中,接着看DubboConfigConfiguration.Single.class这个类是干嘛的

image-20231206090705144

可以看到Single这个类上的@EnableConfigurationBeanBinding注解,prefix属性代表我们Dubbo配置项的前缀,type代表这个配置项被解析会生成的对象类型,最上层还有一个@EnableConfigurationBeanBindings注解。

image-20231206090804062

在@EnableConfigurationBeanBindings注解上通过@Import注解往容器注入了一个ConfigurationBeanBindingRegistrar对象

image-20231206091258610

ConfigurationBeanBindingRegistrar实现了ImportBeanDefinitionRegistrar和EnvironmentAware两个接口,分别获取往Spring容器注入Bean以及获取配置信息的能力。然后看下registerBeanDefinitions方法,看看是注入了什么对象到Spring容器中

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		//1.获取EnableConfigurationBeanBinding注解属性
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
                importingClassMetadata.getAnnotationAttributes(EnableConfigurationBeanBindings.class.getName()));

        AnnotationAttributes[] annotationAttributes = attributes.getAnnotationArray("value");
		//创建一个注册器
        ConfigurationBeanBindingRegistrar registrar = new ConfigurationBeanBindingRegistrar();
		//注入environment对象,因为实现了EnvironmentAware接口,可以进行回调拿到。
        registrar.setEnvironment(environment);

        for (AnnotationAttributes element : annotationAttributes) {
            //根据注解属性,向容器中注入配置对象
            registrar.registerConfigurationBeanDefinitions(element, registry);
        }
    }

image-20231206091633481

在这个方法中,首先获取EnableConfigurationBeanBinding注解属性值,拿到prefix、type等属性值,然后创建ConfigurationBeanBindingRegistrar对象并把environment对象注入其中,再遍历EnableConfigurationBeanBinding注解属性,调用ConfigurationBeanBindingRegistrar类的registerConfigurationBeanDefinitions方法创建配置对象,我们接着看registerConfigurationBeanDefinitions方法

    protected void registerConfigurationBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {

        String prefix = environment.resolvePlaceholders(attributes.getString("prefix"));

        Class<?> configClass = attributes.getClass("type");

        boolean multiple = attributes.getBoolean("multiple");

        boolean ignoreUnknownFields = attributes.getBoolean("ignoreUnknownFields");

        boolean ignoreInvalidFields = attributes.getBoolean("ignoreInvalidFields");

        registerConfigurationBeans(prefix, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields, registry);
    }

image-20231206092432801

这里拿到我们注解上的属性值,调用registerConfigurationBeans方法进行一个配置对象的注入

    private void registerConfigurationBeans(String prefix, Class<?> configClass, boolean multiple,
                                            boolean ignoreUnknownFields, boolean ignoreInvalidFields,
                                            BeanDefinitionRegistry registry) {
		//1.根据前缀获取 value值
        //k为 前缀后边的值  v为配置项的值
        //{p2.id=dubbo2, p1.name=dubbo, p1.port=20881, p2.host=0.0.0.0, p1.id=dubbo1, p2.name=dubbo, 		p2.port=20882, p3.host=0.0.0.0, p3.name=dubbo, p3.id=dubbo3, p3.port=20883, p1.host=0.0.0.0}	
        Map<String, Object> configurationProperties = PropertySourcesUtils.getSubProperties(environment.getPropertySources(), environment, prefix);

        if (CollectionUtils.isEmpty(configurationProperties)) {
            if (log.isDebugEnabled()) {
                log.debug("There is no property for binding to configuration class [" + configClass.getName()
                        + "] within prefix [" + prefix + "]");
            }
            return;
        }
		//生成一个beanName的集合
        Set<String> beanNames = multiple ? resolveMultipleBeanNames(configurationProperties) :
                singleton(resolveSingleBeanName(configurationProperties, configClass, registry));
		//注册Bean
        for (String beanName : beanNames) {
            registerConfigurationBean(beanName, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields,
                    configurationProperties, registry);
        }
		//注册一个ConfigurationBeanBindingPostProcessor对象
        registerConfigurationBindingBeanPostProcessor(registry);
    }

image-20231206093851888

在这个方法的主要逻辑有

1.获取配置项的值

2.根据配置项生成对应的config类并注册到容器中

3.往容器中注入一个ConfigurationBeanBindingPostProcessor对象

ConfigurationBeanBindingPostProcessor对象是干嘛的呢?

image-20231206095555274

这个类实现了BeanPostProcessor,其主要目的就是在我们配置对象生成之后,对其内部属性进行赋值,就是把我们配置文件的信息赋值到对应的Config对象中。主要逻辑在postProcessBeforeInitialization方法中

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        BeanDefinition beanDefinition = getNullableBeanDefinition(beanName);
		//判断当前Bean是否的Duboo的一个配置类类型
        if (isConfigurationBean(bean, beanDefinition)) {
            //进行赋值
            bindConfigurationBean(bean, beanDefinition);
            customize(beanName, bean);
        }

        return bean;
    }

真正赋值的方法在bindConfigurationBean中

    private void bindConfigurationBean(Object configurationBean, BeanDefinition beanDefinition) {
		//根据beanDefinition获取配置项信息
        Map<String, Object> configurationProperties = getConfigurationProperties(beanDefinition);

        boolean ignoreUnknownFields = getIgnoreUnknownFields(beanDefinition);

        boolean ignoreInvalidFields = getIgnoreInvalidFields(beanDefinition);
		//进行绑定
        configurationBeanBinder.bind(configurationProperties, ignoreUnknownFields, ignoreInvalidFields, configurationBean);

        if (log.isInfoEnabled()) {
            log.info("The configuration bean [" + configurationBean + "] have been binding by the " +
                    "configuration properties [" + configurationProperties + "]");
        }
    }

image-20231206100112475

根据beanDefinition可以获取我们之前解析好的配置信息,通过configurationBeanBinder进行绑定完成赋值。至此我们的配置项对象在Spring容器中以生成

2.处理@Service注解

我们使用Dubbo的时候,可以使用Dubbo定义的@Service注解标注这是一个Dubbo服务,那么这个类是如何被Spring识别并管理的呢?这就依托于@EnableDubbo注解上的@DubboComponentScan注解

image-20231206213228129

熟悉的套路,在这个注解上通过@Import注解往Spring容器中注入了DubboComponentScanRegistrar类

image-20231206213305341

DubboComponentScanRegistrar类实现了ImportBeanDefinitionRegistrar接口,那么就获得了往Spring容器注册Bean的能力,我们看看它向Spring容器中注入了哪些Bean把。

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		//拿到我们配置的扫描路径
        Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
		//注册ServiceAnnotationBeanPostProcessor
        registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);
		//注册ReferenceAnnotationBeanPostProcessor
        registerReferenceAnnotationBeanPostProcessor(registry);

    }

这里我们先看registerServiceAnnotationBeanPostProcessor方法

    private void registerServiceAnnotationBeanPostProcessor(Set<String> packagesToScan, BeanDefinitionRegistry registry) {
		//获得ServiceAnnotationBeanPostProcessor类的BeanBeanDefinition
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationBeanPostProcessor.class);
        //添加扫描路径
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        //注册
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);

    }

这里就是配置我们的扫描路径,并向容器中注入ServiceAnnotationBeanPostProcessor对象,接下来看ServiceAnnotationBeanPostProcessor这个类是干嘛的。

image-20231206213621102

ServiceAnnotationBeanPostProcessor实现了BeanDefinitionRegistryPostProcessor类,那么也可以向我们的Spring容器注入Bean,我们继续看实现BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry方法。

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        // @since 2.7.5
        registerBeans(registry, DubboBootstrapApplicationListener.class);
		//获取扫描路径
        Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

        if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
            //注册servBean
            registerServiceBeans(resolvedPackagesToScan, registry);
        } else {
            if (logger.isWarnEnabled()) {
                logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
            }
        }

    }

在这个方法中主要逻辑是registerServiceBeans方法

    private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {
		//新建一个Dubbo实现的扫描器
        DubboClassPathBeanDefinitionScanner scanner =
                new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

        BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

        scanner.setBeanNameGenerator(beanNameGenerator);
		//设置扫描的注解
        scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));

        /**
         * Add the compatibility for legacy Dubbo's @Service
         *
         * The issue : https://github.com/apache/dubbo/issues/4330
         * @since 2.7.3
         */
        scanner.addIncludeFilter(new AnnotationTypeFilter(com.alibaba.dubbo.config.annotation.Service.class));

        for (String packageToScan : packagesToScan) {
			//进行扫描加了@Service注解的类
            scanner.scan(packageToScan);

            // 获取所有的加了@Service注解的BeanDefinitionHolder
            Set<BeanDefinitionHolder> beanDefinitionHolders =
                    findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

            if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

                for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
                    //注册ServiceBean
                    registerServiceBean(beanDefinitionHolder, registry, scanner);
                }

                if (logger.isInfoEnabled()) {
                    logger.info(beanDefinitionHolders.size() + " annotated Dubbo's @Service Components { " +
                            beanDefinitionHolders +
                            " } were scanned under package[" + packageToScan + "]");
                }

            } else {

                if (logger.isWarnEnabled()) {
                    logger.warn("No Spring Bean annotating Dubbo's @Service was found under package["
                            + packageToScan + "]");
                }

            }

        }

    }

image-20231206214315842

在registerServiceBeans方法中主要做的事情有

1.新建一个Dubbo自定义的扫描器

2.设置扫描的注解

3.进行扫描,获取所有加了@Service注解的类并生成对应的BeanDefinitionHolder对象,如上图,但是只生成我们服务实现类的对象是不够的,比如我们@Service注解可以配置超时时间,协议,版本,注册中心等信息,这些信息是无法和实现类绑定的,所以Dubbo除了为我们生成一个实现类的对象之外,还会为我们生成一个ServiceBean类型的对象,具体实现在registerServiceBean方法中,我们接着往下看registerServiceBean方法

    private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
                                     DubboClassPathBeanDefinitionScanner scanner) {
		
        //获取服务实现类的class对象
        Class<?> beanClass = resolveClass(beanDefinitionHolder);
		//拿到service注解
        Annotation service = findServiceAnnotation(beanClass);

        //解析注解上的值
        AnnotationAttributes serviceAnnotationAttributes = getAnnotationAttributes(service, false, false);
		//获取服务接口
        Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);
	
        String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();
		//生成ServiceBean的BeanDefinition
        AbstractBeanDefinition serviceBeanDefinition =
            	
                buildServiceBeanDefinition(service, serviceAnnotationAttributes, interfaceClass, annotatedServiceBeanName);

        // 创建一个ServiceBean名称
        String beanName = generateServiceBeanName(serviceAnnotationAttributes, interfaceClass);

        if (scanner.checkCandidate(beanName, serviceBeanDefinition)) { // check duplicated candidate bean
            //注入
            registry.registerBeanDefinition(beanName, serviceBeanDefinition);

            if (logger.isInfoEnabled()) {
                logger.info("The BeanDefinition[" + serviceBeanDefinition +
                        "] of ServiceBean has been registered with name : " + beanName);
            }

        } else {

            if (logger.isWarnEnabled()) {
                logger.warn("The Duplicated BeanDefinition[" + serviceBeanDefinition +
                        "] of ServiceBean[ bean name : " + beanName +
                        "] was be found , Did @DubboComponentScan scan to same package in many times?");
            }

        }

    }

image-20231206215329349

在registerServiceBean中,主要做了

1.解析服务实现类

2.获取服务实现类上的@Service注解

3.获取@Service注解上的属性值

4.根据注解的属性,生成一个ServiceBean的BeanDefinition

5.注入到容器中

那么ServiceBean有什么作用呢?

image-20231206215659883

ServiceBean表示⼀个Dubbo服务,它有⼀些参数,⽐如:

  1. ref,表示服务的具体实现类

  2. interface,表示服务的接⼝

  3. parameters,表示服务的参数(@Service注解中所配置的信息)

  4. application,表示服务所属的应⽤

  5. protocols,表示服务所使⽤的协议

  6. registries,表示服务所要注册的注册中⼼

并且需要注意的是,ServiceBean实现了ApplicationListener接⼝,所以当Spring启动完成后会触发onApplicationEvent()⽅法的调⽤,⽽在这个⽅法内会调⽤export(),这个⽅法就是服务导出的⼊⼝⽅法。

@Service注解解析整体过程架构图:

image-20231206221634708

3.处理@Reference注解

当我们消费者需要调用Dubbo服务的时候,可以通过@Reference注解进行注入,Spring是怎么往类中的属性注入呢?我们接着往下分析,从上边我们可以知道通过@DubboComponentScan注解导入的DubboComponentScanRegistrar类会为我们注入一个ReferenceAnnotationBeanPostProcessor类型的对象。我们看看这个类是干什么的。

image-20231206222156916

ReferenceAnnotationBeanPostProcessor实际上是Bean的后置处理器,在Spring的Bean创建的时候会调用相关方法。

Spring在对Bean进⾏依赖注⼊时会调⽤AnnotationInjectedBeanPostProcessor的postProcessPropertyValues()⽅法来给某个Bean按照ReferenceAnnotationBeanPostProcessor的逻辑进⾏依赖注⼊。

    @Override
    public PropertyValues postProcessPropertyValues(
            PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
		//寻找注入点
        InjectionMetadata metadata = findInjectionMetadata(beanName, bean.getClass(), pvs);
        try {
            //注入点注入
            metadata.inject(bean, beanName, pvs);
        } catch (BeanCreationException ex) {
            throw ex;
        } catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of @" + getAnnotationType().getSimpleName()
                    + " dependencies is failed", ex);
        }
        return pvs;
    }

寻找注入点,这块是Spring的知识点,其实就是找到加了@Reference的属性或者方法,把他们整合到InjectionMetadata,然后调用inject方法,这里我们就以属性为注入点,通过反射为属性进行赋值。

        @Override
        protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
			//获取要注入的类型
            Class<?> injectedType = field.getType();
			//获取对象
            Object injectedObject = getInjectedObject(attributes, bean, beanName, injectedType, this);

            ReflectionUtils.makeAccessible(field);
			//反射注入
            field.set(bean, injectedObject);

        }

此时就会调⽤ReferenceAnnotationBeanPostProcessor的doGetInjectedBean()⽅法来得到⼀个对象,⽽这个对象的构造就⽐较复杂了,因为对于Dubbo来说,注⼊给某个属性的应该是当前这个属性所对应的服务接⼝的代理对象。但是在⽣成这个代理对象之前,还要考虑问题:

  1. 当前所需要引⼊的这个服务,是不是在本地就存在?不存在则要把按Dubbo的逻辑⽣成⼀个代理对象

  2. 当前所需要引⼊的这个服务,是不是已经被引⼊过了(是不是已经⽣成过代理对象了),如果是应该是不⽤再重复去⽣成了。

我们接着看getInjectedObject方法的逻辑:

    @Override
    protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
                                       InjectionMetadata.InjectedElement injectedElement) throws Exception {
		//根据属性获取一个被引用服务的名称  也就是ServiceBean的名称,后续判断本地是否存在这个服务调用
        String referencedBeanName = buildReferencedBeanName(attributes, injectedType);
		//创建一个ReferenceBean
        ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referencedBeanName, attributes, injectedType);
		//注册ReferenceBean
        registerReferenceBean(referencedBeanName, referenceBean, attributes, injectedType);
		//缓存
        cacheInjectedReferenceBean(referenceBean, injectedElement);
		//创建代理对象
        return buildProxy(referencedBeanName, referenceBean, injectedType);
    }

这个方法的整体逻辑就是:

  1. 得到当前所引⼊服务对应的ServiceBean的beanName(源码中叫referencedBeanName)

  2. 根据@Reference注解的所有信息+属性接⼝类型得到⼀个referenceBeanName

  3. 根据referenceBeanName从referenceBeanCache获取对应的ReferenceBean,如果没有则创建⼀

    个ReferenceBean

  4. 根据referencedBeanName(ServiceBean的beanName)判断Spring容器中是否存在该bean,如果

存在则给ref属性所对应的bean取⼀个别名,别名为referenceBeanName。

​ a. 如果Spring容器中不存在referencedBeanName对应的bean,则判断容器中是否存在

referenceBeanName所对应的Bean,如果不存在则将创建出来的ReferenceBean注册到Spring

容器中(此处这么做就⽀持了可以通过@Autowired注解也可以使⽤服务了,ReferenceBean是

⼀个FactoryBean)

  1. 如果referencedBeanName存在对应的Bean,则额外⽣成⼀个代理对象,代理对象的

InvocationHandler会缓存在localReferenceBeanInvocationHandlerCache中,这样如果引⼊的是

同⼀个服务,并且这个服务在本地,

  1. 如果referencedBeanName不存在对应的Bean,则直接调⽤ReferenceBean的get()⽅法得到⼀个代

理对象.

至于代理类如何生成,后续看服务导入的源码分析,本章节主要看的是Spring和Dubbo整合的代码。

e(ServiceBean的beanName)判断Spring容器中是否存在该bean,如果

存在则给ref属性所对应的bean取⼀个别名,别名为referenceBeanName。

​ a. 如果Spring容器中不存在referencedBeanName对应的bean,则判断容器中是否存在

referenceBeanName所对应的Bean,如果不存在则将创建出来的ReferenceBean注册到Spring

容器中(此处这么做就⽀持了可以通过@Autowired注解也可以使⽤服务了,ReferenceBean是

⼀个FactoryBean)

  1. 如果referencedBeanName存在对应的Bean,则额外⽣成⼀个代理对象,代理对象的

InvocationHandler会缓存在localReferenceBeanInvocationHandlerCache中,这样如果引⼊的是

同⼀个服务,并且这个服务在本地,

  1. 如果referencedBeanName不存在对应的Bean,则直接调⽤ReferenceBean的get()⽅法得到⼀个代

理对象.

至于代理类如何生成,后续看服务导入的源码分析,本章节主要看的是Spring和Dubbo整合的代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值