Spring Cloud Openfeign分析

1、@EnableFeignClients

该注解标在SpringBoot启动类上,表示开启Openfeign,看一下这个注解做了什么。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

   String[] value() default {};

   String[] basePackages() default {};

   Class<?>[] basePackageClasses() default {};

   Class<?>[] defaultConfiguration() default {};

   Class<?>[] clients() default {};

}

这个注解导入了一个FeignClientsRegistrar 类,下面看一下这个类。

2、FeignClientsRegistrar

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware

这个类实现了ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法用来注册BeanDefinition。

public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    //注册默认配置
   registerDefaultConfiguration(metadata, registry);
    //注册FeignClients
   registerFeignClients(metadata, registry);
}

registerBeanDefinitions做了两件事,注册默认配置和注册FeignClients

2.1、registerDefaultConfiguration

private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    //获取@EnableFeignClients的元数据
   Map<String, Object> defaultAttrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

   if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
      String name;
      if (metadata.hasEnclosingClass()) {
         name = "default." + metadata.getEnclosingClassName();
      }
      else {
         name = "default." + metadata.getClassName();
      }
      registerClientConfiguration(registry, name,
            defaultAttrs.get("defaultConfiguration"));
   }
}

注册beanDefinition

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
         name + "." + FeignClientSpecification.class.getSimpleName(),
         builder.getBeanDefinition());
}

2.2、@FeignClient

该注解标注在FeignClient接口上,用来声明是FeignClient的接口,类似于mybatis的@mapper。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {

   @AliasFor("name")
   String value() default "";

   @Deprecated
   String serviceId() default "";

   String contextId() default "";

   @AliasFor("value")
   String name() default "";

   @Deprecated
   String qualifier() default "";

   String[] qualifiers() default {};

   String url() default "";

   boolean decode404() default false;

   Class<?>[] configuration() default {};

   Class<?> fallback() default void.class;

   Class<?> fallbackFactory() default void.class;

   String path() default "";

   boolean primary() default true;
}

2.3、注册FeignClients

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {

   LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
   //获取@EnableFeignClients的元数据
    Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   //扫描配置包下面的被@FeignClient 注解标注的类,将Beandefinition添加到candidateComponents
    if (clients == null || clients.length == 0) {
      ClassPathScanningCandidateComponentProvider scanner = getScanner();
      scanner.setResourceLoader(this.resourceLoader);
      scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
      Set<String> basePackages = getBasePackages(metadata);
      for (String basePackage : basePackages) {
         candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
      }
   }
   else {
      for (Class<?> clazz : clients) {
         candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
      }
   }
	//注册所有扫描到的被@FeignClient 注解标注的类的Beandefinition
   for (BeanDefinition candidateComponent : candidateComponents) {
      if (candidateComponent instanceof AnnotatedBeanDefinition) {
         // verify annotated class is an interface
         AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
         AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
         Assert.isTrue(annotationMetadata.isInterface(),
               "@FeignClient can only be specified on an interface");
		//获取@FeignClient 注解配置的属性
         Map<String, Object> attributes = annotationMetadata
               .getAnnotationAttributes(FeignClient.class.getCanonicalName());
		//获取名称,依次取contextId,value,name,serviceId,配置了哪个就用哪个
         String name = getClientName(attributes);
         //注册configuration配置的类
         registerClientConfiguration(registry, name,
               attributes.get("configuration"));
		//
         registerFeignClient(registry, annotationMetadata, attributes);
      }
   }
}
private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   //FeignClient的类名,被@FeignClient标注的类
    String className = annotationMetadata.getClassName();
   //解析class
    Class clazz = ClassUtils.resolveClassName(className, null);
   ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
         ? (ConfigurableBeanFactory) registry : null;
   //获取contextId,如没有,依次取 serviceId,name value
   String contextId = getContextId(beanFactory, attributes);
    //依次取 serviceId,name value
   String name = getName(attributes);
    //生成FeignClientFactoryBean,实现FactoryBean接口
   FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
   factoryBean.setBeanFactory(beanFactory);
   factoryBean.setName(name);
   factoryBean.setContextId(contextId);
   factoryBean.setType(clazz);
    //构建BeanDefinition
   BeanDefinitionBuilder definition = BeanDefinitionBuilder
         .genericBeanDefinition(clazz, () -> {
            factoryBean.setUrl(getUrl(beanFactory, attributes));
            factoryBean.setPath(getPath(beanFactory, attributes));
            factoryBean.setDecode404(Boolean
                  .parseBoolean(String.valueOf(attributes.get("decode404"))));
            Object fallback = attributes.get("fallback");
            if (fallback != null) {
               factoryBean.setFallback(fallback instanceof Class
                     ? (Class<?>) fallback
                     : ClassUtils.resolveClassName(fallback.toString(), null));
            }
            Object fallbackFactory = attributes.get("fallbackFactory");
            if (fallbackFactory != null) {
               factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                     ? (Class<?>) fallbackFactory
                     : ClassUtils.resolveClassName(fallbackFactory.toString(),
                           null));
            }
            return factoryBean.getObject();
         });
   definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
   definition.setLazyInit(true);
   validate(attributes);

   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
   beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
   beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

   // 是否primary
   boolean primary = (Boolean) attributes.get("primary");

   beanDefinition.setPrimary(primary);
	//获取qualifier
   String[] qualifiers = getQualifiers(attributes);
   if (ObjectUtils.isEmpty(qualifiers)) {
      qualifiers = new String[] { contextId + "FeignClient" };
   }
	//注册BeanDefinition
   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
         qualifiers);
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

可以看到,@FeignClient标注的接口都会注册,使用了FactoryBean定制化beanDefinition,为每个接口生成bean,类似于mybatis和Spring集成时使用的MapperFactoryBean。

3、FeignClientFactoryBean创建bean

FeignClientFactoryBean通过重写FactoryBean的getObject()来创建bean

//org.springframework.cloud.openfeign.FeignClientFactoryBean#getObject
public Object getObject() {
   return getTarget();
}

直接调用了getTarget()方法。

<T> T getTarget() {
    //获取FeignContext
   FeignContext context = beanFactory != null
         ? beanFactory.getBean(FeignContext.class)
         : applicationContext.getBean(FeignContext.class);
   //构建builder
    Feign.Builder builder = feign(context);
	//如果没有url,组装url
   if (!StringUtils.hasText(url)) {

      if (LOG.isInfoEnabled()) {
         LOG.info("For '" + name
               + "' URL not provided. Will try picking an instance via load-balancing.");
      }
      if (!name.startsWith("http")) {
         url = "http://" + name;
      }
      else {
         url = name;
      }
      url += cleanPath();
       //调用loadBalance方法,里面还是调用的targeter.target
      return (T) loadBalance(builder, context,
            new HardCodedTarget<>(type, name, url));
   }
    //@FeignClient 里面配置了url
   if (StringUtils.hasText(url) && !url.startsWith("http")) {
      url = "http://" + url;
   }
   String url = this.url + cleanPath();
   Client client = getOptional(context, Client.class);
   if (client != null) {
      if (client instanceof LoadBalancerFeignClient) {
         // not load balancing because we have a url,
         // but ribbon is on the classpath, so unwrap
         client = ((LoadBalancerFeignClient) client).getDelegate();
      }
      if (client instanceof FeignBlockingLoadBalancerClient) {
         // not load balancing because we have a url,
         // but Spring Cloud LoadBalancer is on the classpath, so unwrap
         client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
      }
      if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
         // not load balancing because we have a url,
         // but Spring Cloud LoadBalancer is on the classpath, so unwrap
         client = ((RetryableFeignBlockingLoadBalancerClient) client)
               .getDelegate();
      }
      builder.client(client);
   }
   Targeter targeter = get(context, Targeter.class);
   return (T) targeter.target(this, builder, context,
         new HardCodedTarget<>(type, name, url));
}
//org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
      HardCodedTarget<T> target) {
   //通过contextId获取client	默认LoadBalancerFeignClient
   Client client = getOptional(context, Client.class);
   if (client != null) {
      builder.client(client);
      Targeter targeter = get(context, Targeter.class);
      return targeter.target(this, builder, context, target);
   }

   throw new IllegalStateException(
         "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon or spring-cloud-starter-loadbalancer?");
}

Targeter有三个实现类,DefaultTargeter,FeignCircuitBreakerTargeter,HystrixTargeter
具体在FeignAutoConfiguration中注册

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultFeignTargeterConditions.class)
protected static class DefaultFeignTargeterConfiguration {

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

}

@Configuration(proxyBeanMethods = false)
@Conditional(FeignCircuitBreakerDisabledConditions.class)
@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
@ConditionalOnProperty(value = "feign.hystrix.enabled", havingValue = "true",
      matchIfMissing = true)
protected static class HystrixFeignTargeterConfiguration {

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

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CircuitBreaker.class)
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
protected static class CircuitBreakerPresentFeignTargeterConfiguration {

   @Bean
   @ConditionalOnMissingBean
   @ConditionalOnBean(CircuitBreakerFactory.class)
   public Targeter circuitBreakerFeignTargeter(
         CircuitBreakerFactory circuitBreakerFactory) {
      return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
   }

}

3.1、FeignContext

FeignContext继承了NamedContextFactory,用来维护Feign的上下文

//org.springframework.cloud.openfeign.FeignAutoConfiguration#feignContext
@Bean
public FeignContext feignContext() {
   FeignContext context = new FeignContext();
   context.setConfigurations(this.configurations);
   return context;
}

3.2、LoadBalancerFeignClient

LoadBalancerFeignClient在DefaultFeignLoadBalancedConfiguration中注册,DefaultFeignLoadBalancedConfiguration在FeignRibbonClientAutoConfiguration导入

@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {

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

}

3.3、DefaultTargeter

class DefaultTargeter implements Targeter {

   @Override
   public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
         FeignContext context, Target.HardCodedTarget<T> target) {
      return feign.target(target);
   }

}

直接调用了Feign.Builder.target(target);

//feign.Feign.Builder#target(feign.Target<T>)
public <T> T target(Target<T> target) {
  return build().newInstance(target);
}
//feign.Feign.Builder#build
public Feign build() {
    Client client = Capability.enrich(this.client, capabilities);
    Retryer retryer = Capability.enrich(this.retryer, capabilities);
    List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
        .map(ri -> Capability.enrich(ri, capabilities))
        .collect(Collectors.toList());
    Logger logger = Capability.enrich(this.logger, capabilities);
    Contract contract = Capability.enrich(this.contract, capabilities);
    Options options = Capability.enrich(this.options, capabilities);
    Encoder encoder = Capability.enrich(this.encoder, capabilities);
    Decoder decoder = Capability.enrich(this.decoder, capabilities);
    InvocationHandlerFactory invocationHandlerFactory =
        Capability.enrich(this.invocationHandlerFactory, capabilities);
    QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);

    SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
        new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
            logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
    ParseHandlersByName handlersByName =
        new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
            errorDecoder, synchronousMethodHandlerFactory);
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
  }
}

build()构建了一个ReflectiveFeign

//feign.ReflectiveFeign#newInstance
public <T> T newInstance(Target<T> target) {
  Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

  for (Method method : target.type().getMethods()) {
    if (method.getDeclaringClass() == Object.class) {
      continue;
    } else if (Util.isDefault(method)) {
      DefaultMethodHandler handler = new DefaultMethodHandler(method);
      defaultMethodHandlers.add(handler);
      methodToHandler.put(method, handler);
    } else {
      methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    }
  }
    //创建FeignInvocationHandler
  InvocationHandler handler = factory.create(target, methodToHandler);
  //反射
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
      new Class<?>[] {target.type()}, handler);

  for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    //调用JDK的MethodHandler反射调用方法
      defaultMethodHandler.bindTo(proxy);
  }
  return proxy;
}

4、运行时调用逻辑

上面的反射是执行的这个方法

//feign.ReflectiveFeign.FeignInvocationHandler#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if ("equals".equals(method.getName())) {
    try {
      Object otherHandler =
          args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
      return equals(otherHandler);
    } catch (IllegalArgumentException e) {
      return false;
    }
  } else if ("hashCode".equals(method.getName())) {
    return hashCode();
  } else if ("toString".equals(method.getName())) {
    return toString();
  }

  return dispatch.get(method).invoke(args);
}

上面的方法调用SynchronousMethodHandler#invoke

//feign.SynchronousMethodHandler#invoke
public Object invoke(Object[] argv) throws Throwable {
  //创建RequestTemplate
    RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
        //执行请求
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
      try {
        retryer.continueOrPropagate(e);
      } catch (RetryableException th) {
        Throwable cause = th.getCause();
        if (propagationPolicy == UNWRAP && cause != null) {
          throw cause;
        } else {
          throw th;
        }
      }
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}
//feign.SynchronousMethodHandler#executeAndDecode
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  Request request = targetRequest(template);

  if (logLevel != Logger.Level.NONE) {
    logger.logRequest(metadata.configKey(), logLevel, request);
  }

  Response response;
  long start = System.nanoTime();
  try {
      //调用
    response = client.execute(request, options);
    // ensure the request is set. TODO: remove in Feign 12
    response = response.toBuilder()
        .request(request)
        .requestTemplate(template)
        .build();
  } catch (IOException e) {
    if (logLevel != Logger.Level.NONE) {
      logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
    }
    throw errorExecuting(request, e);
  }
  long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);


  if (decoder != null)
    return decoder.decode(response, metadata.returnType());

  CompletableFuture<Object> resultFuture = new CompletableFuture<>();
  asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
      metadata.returnType(),
      elapsedTime);

  try {
    if (!resultFuture.isDone())
      throw new IllegalStateException("Response handling not done");

    return resultFuture.join();
  } catch (CompletionException e) {
    Throwable cause = e.getCause();
    if (cause != null)
      throw cause;
    throw e;
  }
}

调用LoadBalancerFeignClient#execute

//org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute
public Response execute(Request request, Request.Options options) throws IOException {
   try {
      URI asUri = URI.create(request.url());
      String clientName = asUri.getHost();
      URI uriWithoutHost = cleanUrl(request.url(), clientName);
      FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
            this.delegate, request, uriWithoutHost);

      IClientConfig requestConfig = getClientConfig(options, clientName);
      return lbClient(clientName)
            .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
   }
   catch (ClientException e) {
      IOException io = findIOException(e);
      if (io != null) {
         throw io;
      }
      throw new RuntimeException(e);
   }
}
//com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

    try {
        return command.submit(
            new ServerOperation<T>() {
                @Override
                public Observable<T> call(Server server) {
                    //经过负载均衡后,已经获取到具体服务的服务信息,ip:port
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        //调用
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } 
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } catch (Exception e) {
        Throwable t = e.getCause();
        if (t instanceof ClientException) {
            throw (ClientException) t;
        } else {
            throw new ClientException(e);
        }
    }
    
}

上面call()方法中会执行具体的http请求

//org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
      throws IOException {
   Request.Options options;
   if (configOverride != null) {
      RibbonProperties override = RibbonProperties.from(configOverride);
      options = new Request.Options(override.connectTimeout(connectTimeout),
            TimeUnit.MILLISECONDS, override.readTimeout(readTimeout),
            TimeUnit.MILLISECONDS, override.isFollowRedirects(followRedirects));
   }
   else {
      options = new Request.Options(connectTimeout, TimeUnit.MILLISECONDS,
            readTimeout, TimeUnit.MILLISECONDS, followRedirects);
   }
   Response response = request.client().execute(request.toRequest(), options);
   return new RibbonResponse(request.getUri(), response);
}
//feign.Client.Default#execute
public Response execute(Request request, Options options) throws IOException {
    //构建链接
    HttpURLConnection connection = convertAndSend(request, options);
    //构造响应
    return convertResponse(connection, request);
}

调用负载均衡器在 submit方法中

//com.netflix.loadbalancer.reactive.LoadBalancerCommand#submit
public Observable<T> submit(final ServerOperation<T> operation) {
	...
    // Use the load balancer
    Observable<T> o = 
        	//selectServer,这里会调用LoadBalancer选择服务器
            (server == null ? selectServer() : Observable.just(server))
            .concatMap(
        	...
            });
    ...
}
//com.netflix.loadbalancer.reactive.LoadBalancerCommand#selectServer
private Observable<Server> selectServer() {
    return Observable.create(new OnSubscribe<Server>() {
        @Override
        public void call(Subscriber<? super Server> next) {
            try {
                Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                next.onNext(server);
                next.onCompleted();
            } catch (Exception e) {
                next.onError(e);
            }
        }
    });
}
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
...
    ILoadBalancer lb = getLoadBalancer();
    if (host == null) {
        if (lb != null){
            //这里会调用负载均衡器的chooseServer,Ribbon默认配置的是ZoneAwareLoadBalancer,这在前面分析Ribbon的时候有
            //com.netflix.loadbalancer.ZoneAwareLoadBalancer#chooseServer
            Server svc = lb.chooseServer(loadBalancerKey);
...
            return svc;
        } else {
        ...
}

就这样,feign做了一个伪rpc,看上去是rpc的调用方法,但是内层还是通过动态代理,调用了Ribbon,发送http请求。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值