OpenFeign学习(九):Spring Cloud OpenFeign的加载配置原理 II

说明

在上篇博文《OpenFeign学习(八):Spring Cloud OpenFeign的加载配置原理》中,我简单介绍了Spring Cloud 是如何通过注解对Feign Client进行加载配置的。主要介绍了通过FeignClientsRegistrar类,对所有使用@FeignClient注解的类进行加载配置,实现Feign Client的配置类Bean的注册和相对应Client的FeignClientFactoryBean的注册。同时还提到在spring.factories配置文件中,配置了有关自动装配的类FeignRibbonClientAutoConfiguration和FeignAutoConfiguration等类。在本篇博文中,我将继续通过源码学习介绍Spring Cloud OpenFeign的加载配置原理。

正文

通过上篇博文我们知道了对Feign Client进行扫描注册的实际为其对应的FeignClientFactoryBean,beanName为@FeignClient作用接口类的ClassName。

接下来,我们了解下FeignRibbonClientAutoConfiguration类的作用:

FeignRibbonClientAutoConfiguration

该类从名称可知属于RibbonClient的支持类,

@ConditionalOnClass({ILoadBalancer.class, Feign.class})
@Configuration
@AutoConfigureBefore({FeignAutoConfiguration.class})
@EnableConfigurationProperties({FeignHttpClientProperties.class})
@Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class})
public class FeignRibbonClientAutoConfiguration {
    
}

通过源码可以看到,该类作用的前提是类路径中必需包含了ILoadBalancer类和Feign类,而ILoadBalancer类属于com.netflix.loadbalancer包,则证明类路径必须存在Netflix Ribbon的相关依赖。并且该类将会在FeignAutoConfiguration类前加载。

同时通过@EnableConfigurationProperties注解将FeignHttpClientProperties配置类进行注入,该类为FeignHttpClient的配置类。接着通过@Import注解分别引入了HttpClientFeignLoadBalancedConfiguration,OkHttpFeignLoadBalancedConfiguration,DefaultFeignLoadBalancedConfiguration类,顾名思义,这些类为不同类型LoadBalancerClient的配置类。

可以看到,该类的作用主要是引入不同配置类,在创建client时,根据不同的配置创建不同的feignClient。

关于feignClient的创建,官方文档对此有以下描述:

if Ribbon is in the classpath and is enabled it is a LoadBalancerFeignClient, otherwise if Spring Cloud LoadBalancer is in the classpath, FeignBlockingLoadBalancerClient is used. If none of them is in the classpath, the default feign client is used.

在创建feignClient时,不同的client优先级为LoadBalancerFeignClient > FeignBlockingLoadBalancerClient > Default Feign Client 。

而且spring-cloud-starter-openfeign同时包含了spring-cloud-starter-netflix-ribbon和spring-cloud-starter-loadbalancer依赖。

由此可知,默认创建的feignClient都为LoadBalancerFeignClient类型。

我们可以通过设置feign.okhttp.enabled或feign.httpclient.enabled为ture来选择不同的client,但前提是必须引入相关依赖

FeignAutoConfiguration

该类作用于FeignRibbonClientAutoConfiguration之后,与之类似,为FeignClient引入注册相关配置。

@Configuration
@ConditionalOnClass({Feign.class})
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {

    @Autowired(
        required = false
    )
    private List<FeignClientSpecification> configurations = new ArrayList();
    
    ....
    
    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }
    
    ....

}

通过源码可以看到,该类通过@EnableConfigurationProperties注解注入了FeignClientProperties和FeignHttpClientProperties配置类。在其内部,注入了FeignClientSpecification集合,配置了FeignContext,还有对OkHttpClient, ApacheHttpClient,HystrixFeign的相关配置。这里我们重点关注以上所显示的代码相关配置。

通过上篇博文我们可以知道,在注册时对在@EnableFeignClients注解中配置的defaultConfiguration和在@FeignClient注解中配置的configuration都会注册为FeignClientSpecification类型的bean

在该类中,通过对FeignClientSpecificatio类型的List集合进行自动装配,将之前注册的配置类注入到该类中。同时,在实例化FeignContext类型的bean时,将其设置到上下文中。(有关集合的自动装配请看:https://blog.csdn.net/tales522/article/details/89683282)

配置类FeignClientProperties为获取feign.client前缀的配置值,对不同的client进行配置则在前缀后加指定的clientName,对所有client的默认配置则加default(i.e. feign.client.default)

配置类FeignHttpClientProperties则为获取httpclient的配置值。配置前缀为feign.httpclient。

FeignClientFactoryBean

在使用feignClient时,我们会使用@Autowired注解进行自动装配依赖注入。此时,会通过factoryBean的getObject()方法获取对应类的实例对象。

接下来,通过源码来了解如何创建Feign Client:

public Object getObject() throws Exception {
        return this.getTarget();
    }

    <T> T getTarget() {
        // 获取Feign上下文
        FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
        // 根据上下文创建FeignClient的构造器
        Builder builder = this.feign(context);
        // 创建对应的client
        if (!StringUtils.hasText(this.url)) {
            if (!this.name.startsWith("http")) {
                this.url = "http://" + this.name;
            } else {
                this.url = this.name;
            }

            this.url = this.url + this.cleanPath();
            return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
        } else {
            if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
                this.url = "http://" + this.url;
            }

            String url = this.url + this.cleanPath();
            Client client = (Client)this.getOptional(context, Client.class);
            if (client != null) {
                if (client instanceof LoadBalancerFeignClient) {
                    client = ((LoadBalancerFeignClient)client).getDelegate();
                }

                builder.client(client);
            }

            Targeter targeter = (Targeter)this.get(context, Targeter.class);
            return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
        }
    }

通过源码可以看出,在getTarget()方法中主要分为三步来创建feignClient:

  1. 获取Feign的上下文对象 FeignContext
  2. 根据上下文创建FeignClient的构造器
  3. 根据是否配置url来创建对应的client

FeignContext

通过上面的介绍我们已经知道在FeignAutoConfiguration类中已经声明配置了对应的Bean。在实例化时,创建对象后为其设置了cleint的配置类集合。

@Bean
public FeignContext feignContext() {
    FeignContext context = new FeignContext();
    context.setConfigurations(this.configurations);
    return context;
}
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    public FeignContext() {
        super(FeignClientsConfiguration.class, "feign", "feign.client.name");
    }
}

FeignContext继承自NamedContextFactory类,默认构造函数调用了父类的构造函数,其中参数FeignClientsConfiguration类为Spring Cloud Netflix为Client提供的默认配置类

FeignClientsConfiguration.class is the default configuration provided by Spring Cloud Netflix.

FeignClientsConfiguration提供了一些Feign Client的默认配置,如Decoder,Encoder,Contract,Retryer等。

我们已经知道Spring Cloud对OpenFeign的集成使其支持SpringMVC注解及Spring Web的HttpMessageConverters。而这些配置,正是由FeignClientsConfiguration配置类进行声明配置的。

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
    return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass({"org.springframework.data.domain.Pageable"})
public Encoder feignEncoder() {
    return new SpringEncoder(this.messageConverters);
}
    
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}

可以看到SpringCloud用ResponseEntityDecoder作为默认Decoder, SpringEncoder作为默认Encoder,SpringMvcContract代替了OpenFeign默认的Contract,使其能支持SpringMVC的注解。

在SpringEncoder源码中,可以看到其已经配置生成了SpringFormEncoder对象,所以在表单提交或者文件传输时,不用再手动配置SpringFormEncoder。

public class SpringEncoder implements Encoder {
    private static final Log log = LogFactory.getLog(SpringEncoder.class);
    private final SpringFormEncoder springFormEncoder = new SpringFormEncoder();
    private ObjectFactory<HttpMessageConverters> messageConverters;

    public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        this.messageConverters = messageConverters;
    }
    
    .....
}

Builder

接下来通过Feign上下文创建Client构造器对象。

Builder builder = this.feign(context);
protected Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = (FeignLoggerFactory)this.get(context, FeignLoggerFactory.class);
    // 生成logger 默认仍为Slf4jLogger
    Logger logger = loggerFactory.create(this.type);
    
    // 从AnnotationConfigApplicationContext获取对应配置
    Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class));
    // 配置其他参数
    this.configureFeign(context, builder);
    return builder;
}

首先获取FeignLoggerFactory对象,但是在获取时,先生成了对应client的AnnotationConfigApplicationContext,并根据之前注解中的配置类进行配置。

protected <T> T get(FeignContext context, Class<T> type) {
    T instance = context.getInstance(this.contextId, type);
    if (instance == null) {
        throw new IllegalStateException("No bean found of type " + type + " for " + this.contextId);
    } else {
        return instance;
    }
}
public <T> T getInstance(String name, Class<T> type) {
        // 根据contextId配置生成对应的AnnotationConfigApplicationContext
        AnnotationConfigApplicationContext context = this.getContext(name);
        return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0 ? context.getBean(type) : null;
    }
protected AnnotationConfigApplicationContext getContext(String name) {
    // 每个client对应context都是单例,这里使用了双重检查加锁
    if (!this.contexts.containsKey(name)) {
        synchronized(this.contexts) {
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, this.createContext(name));
            }
        }
    }

    return (AnnotationConfigApplicationContext)this.contexts.get(name);
}

创建AnnotationConfigApplicationContext时,首先获取注册client对应配置的configuration,然后再注册client的默认配置,最后注册defaultConfigType,也就是Spring Cloud Netflix提供的默认配置类FeignClientsConfiguration。

protected AnnotationConfigApplicationContext createContext(String name) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    
    // 获取client对应的配置类进行注册
    if (this.configurations.containsKey(name)) {
        Class[] var3 = ((NamedContextFactory.Specification)this.configurations.get(name)).getConfiguration();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Class<?> configuration = var3[var5];
            context.register(new Class[]{configuration});
        }
    }

    Iterator var9 = this.configurations.entrySet().iterator();

    // 循环注册所有默认的以default开头的配置
    while(true) {
        Entry entry;
        do {
        
            // 最后注册配置类FeignClientsConfiguration
            if (!var9.hasNext()) {
                context.register(new Class[]{PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType});
                context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName, Collections.singletonMap(this.propertyName, name)));
                if (this.parent != null) {
                    context.setParent(this.parent);
                    context.setClassLoader(this.parent.getClassLoader());
                }

                context.setDisplayName(this.generateDisplayName(name));
                context.refresh();
                return context;
            }

            entry = (Entry)var9.next();
        } while(!((String)entry.getKey()).startsWith("default."));

        Class[] var11 = ((NamedContextFactory.Specification)entry.getValue()).getConfiguration();
        int var12 = var11.length;

        for(int var7 = 0; var7 < var12; ++var7) {
            Class<?> configuration = var11[var7];
            context.register(new Class[]{configuration});
        }
    }
}

创建完成client对应的AnnotationConfigApplicationContext后,根据配置类创建生成构建起Builder。通过以上代码可以看到,从自定义配置类或者默认配置类中,可以获取先进行配置的有Logger,Encoder,Decoder, Contract。

Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class));

之后,在通过configureFeign方法配置client其他参数:

this.configureFeign(context, builder);
protected void configureFeign(FeignContext context, Builder builder) {
    // 获取配置文件中关于client配置的参数
    FeignClientProperties properties = (FeignClientProperties)this.applicationContext.getBean(FeignClientProperties.class);
    // 按优先级顺序进行配置
    if (properties != null) {
        if (properties.isDefaultToProperties()) {
            this.configureUsingConfiguration(context, builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
        } else {
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
            this.configureUsingConfiguration(context, builder);
        }
    } else {
        this.configureUsingConfiguration(context, builder);
    }

}

通过以上代码我们可以看到,先获取了FeignClientProperties示例,该类用来读取配置文件中有关feign client的配置参数。根据defaultToProperties参数来决定不同的配置顺序,对此,官方文档中也进行了说明:

If we create both @Configuration bean and configuration properties, configuration properties will win. It will override @Configuration values. But if you want to change the priority to @Configuration, you can change feign.client.default-to-properties to false.

可以看到如果同时使用了@Configuration bean 也就是注解中设置了配置类和配置文件配置了相关属性时,默认情况下,配置文件会覆盖配置类的值。但是可以通过设置feign.client.default-to-properties 值为false改变优先级,此参数值对应为FeignClientProperties的defaultToProperties参数值。

若默认情况,会先根据默认配置类和自定配置类的值进行配置:

this.configureUsingConfiguration(context, builder);

接下来根据配置文件中配置的对所有client的默认值进行配置:

this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);

最后,根据配置文件的自定义参数值进行配置:

this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);

当设置为false时,调换配置顺序,将配置类的配置放到最后。
主要配置了Logger.Level,Retryer,ErrorDecoder,RequestInterceptor,decode404及httpclient相关配置。

在FeignClientsConfiguration类,配置了Retryer的bean,默认情况下,Spring Cloud OpenFeign的feignClient不会进行重试,这与原生的OpenFeign有所不同。

@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
    return Retryer.NEVER_RETRY;
}

Client

在完成Buidler的创建配置后,根据@FeignClient注解中是否配置了url来完成最终的FeignClient创建。

在未配置时,进行了以下配置:

if (!StringUtils.hasText(this.url)) {
    if (!this.name.startsWith("http")) {
        this.url = "http://" + this.name;
    } else {
        this.url = this.name;
    }
    // url 值类似为 http://clientName
    this.url = this.url + this.cleanPath();
    return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
} 

可以看到调用loadBalance方法进行创建

protected <T> T loadBalance(Builder builder, FeignContext context, HardCodedTarget<T> target) {
    Client client = (Client)this.getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        Targeter targeter = (Targeter)this.get(context, Targeter.class);
        return targeter.target(this, builder, context, target);
    } else {
        throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
    }
}

在该方法中,获取了配置的Client对象,上面提到在FeignRibbonClientAutoConfiguration配置类中引入了三个配置类,分别为HttpClientFeignLoadBalancedConfiguration,OkHttpFeignLoadBalancedConfiguration和DefaultFeignLoadBalancedConfiguration。

要使用 ApacheHttpClient 或 OkHttpClient 时,必须引入对应的依赖,同时配置文件中feign.okhttp.enabled或feign.httpclient.enabled对应值true,否则就使用默认的client。

这里在DefaultFeignLoadBalancedConfiguration配置类中配置创建了默认的LoadBalancerFeignClient。

@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) {
    return new LoadBalancerFeignClient(new Default((SSLSocketFactory)null, (HostnameVerifier)null), cachingFactory, clientFactory);
}

该默认LoadBalancerFeignClient中delegate即为feign默认的client,使用的是HttpURLConnection:

public static class Default implements Client {
    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

    public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
        this.sslContextFactory = sslContextFactory;
        this.hostnameVerifier = hostnameVerifier;
    }
    ....
}

再获取对应的Targeter,该接口共有两个实现类,分别为DefaultTargeter和HystrixTargeter,这里获取默认的DefaultTargeter。

@Configuration
@ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"})
protected static class DefaultFeignTargeterConfiguration {
    protected DefaultFeignTargeterConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean
    public Targeter feignTargeter() {
        return new DefaultTargeter();
    }
}

最后调用feign.target(target)创建对应的代理target。

若配置有url,则进行以下配置:

else {
    if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
        this.url = "http://" + this.url;
    }

    String url = this.url + this.cleanPath();
    Client client = (Client)this.getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof LoadBalancerFeignClient) {
            client = ((LoadBalancerFeignClient)client).getDelegate();
        }
        // 获取最终代理client
        builder.client(client);
    }

    Targeter targeter = (Targeter)this.get(context, Targeter.class);
    return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
}

由源码可知,若配置了url,在创建代理时使用的client不是LoadBalancerFeignClient,也就无法实现负载均衡

至此,FeignClient配置加载创建原理介绍完毕。

总结

通过源码我们了解了Spring Cloud OpenFeign的加载配置创建流程。通过注解@FeignClient和@EnableFeignClients注解实现了client的配置声明注册,再通过FeignRibbonClientAutoConfiguration和FeignAutoConfiguration类进行自动装配。

在通过FeignClientFactoryBean创建client对象时,了解了Spring Cloud是如何进行配置,不同配置的优先级顺序的选择,及根据配置生成不同配置类的流程,及Spring Cloud对OpenFeign的集成支持如何实现,及与原生OpenFeign的区别之处。

更多的使用方法及配置请阅读官方文档

接下来,我将继续学习阅读源码,了解Ribbon是如何在Spring Cloud OpenFeign中进行负载均衡的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ork.cloud:spring-cloud-starter-openfeign:jar是一个基于Spring Cloud的开源库,用于简化微服务架构中服务之间的调用和通信。 OpenFeign是一个声明式的Web服务客户端,它简化了编写HTTP请求的代码,使得服务间调用更加简单和高效。它是Spring Cloud提供的一个集成了Ribbon和Hystrix的库,可以方便地与其他微服务组件集成使用。 使用OpenFeign,我们可以通过编写接口的方式来定义服务间的调用,而不需要关注底层的HTTP请求。通过注解配置OpenFeign会自动根据接口定义生成对应的代理类,并且提供了负载均衡、断路器等功能,方便处理高并发和服务故障的情况。 在微服务架构中,服务之间的调用是非常频繁的,而且随着微服务的增多,手动编写HTTP请求的代码会变得非常繁琐和容易出错。使用OpenFeign可以大大简化服务之间的调用流程,提高开发效率和代码质量。 总结来说,ork.cloud:spring-cloud-starter-openfeign:jar是一个方便而强大的库,可以帮助我们简化微服务架构中服务之间的调用和通信,并且提供了负载均衡和断路器等功能,能够提高系统的可靠性和性能。 ### 回答2: ork.cloud:spring-cloud-starter-openfeign:jar是一个基于Spring Cloud的开源项目,它提供了一种方便的编写和调用RESTful服务的方式。Feign是一个声明式的Web服务客户端,它可以简化HTTP请求的处理和封装,使得开发者可以更加专注于业务逻辑的编写。 使用Spring Cloud Starter OpenFeign可以快速地编写和调用其他微服务。它通过注解的方式将HTTP请求映射到对应的方法上,自动进行了服务的发现和负载均衡。 Feign支持多种请求方式,包括GET、POST、PUT、DELETE等,还可以使用@PathVariable、@RequestParam等注解处理路径参数和查询参数。Feign还支持对请求体进行处理,可以将请求体转换成Java对象,方便业务逻辑的处理。 在使用Feign时,不需要手动编写HTTP请求的代码,只需要定义一个接口并使用Feign的注解进行标记即可。Feign会根据注解生成代理对象来完成请求的发送和接收。这样可以大大简化开发的工作量,并且使得代码更加清晰易读。 Spring Cloud Starter OpenFeign还集成了Ribbon和Hystrix,这使得我们在使用Feign时可以实现负载均衡和熔断的功能。即使请求的目标服务发生宕机或故障,也能够保证系统的高可用性和稳定性。 总之,Spring Cloud Starter OpenFeign是一个非常实用和方便的工具,可以简化微服务架构下的服务调用,并提供了负载均衡和熔断等功能。它的使用可以加快开发速度,提高系统的可靠性和稳定性。 ### 回答3: spring-cloud-starter-openfeign是一个开源的Spring Cloud组件,用于简化在微服务架构中进行远程服务调用的过程。它基于Netflix的Feign库进行开发,提供了一种声明式的、基于接口的远程服务调用方式,可以方便地实现服务之间的通信和数据交互。 ork.cloud:spring-cloud-starter-openfeign:jar是spring-cloud-starter-openfeign组件的一个特定版本的jar包。在使用Spring Boot构建的项目中,可以通过引入这个jar包来集成并使用spring-cloud-starter-openfeign组件,从而简化远程服务调用的代码编写和配置。 使用spring-cloud-starter-openfeign,我们只需要定义一个接口,通过注解的方式声明远程服务的地址和调用方法,然后在需要调用远程服务的地方直接调用这个接口的方法即可。Spring Cloud会根据注解信息自动进行服务发现和负载均衡,将我们的调用请求转发到对应的服务实例上。 该jar包中除了包含spring-cloud-starter-openfeign的核心功能外,还可能包含一些额外的依赖库或工具,以及特定版本的相关代码和配置文件。通过引入这个jar包,我们可以一键集成和启用spring-cloud-starter-openfeign组件,省去了手动添加依赖和配置的步骤,能够更快速地搭建起微服务架构中的服务调用机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值