SpringCloud openFeign注入原理解析-手写实现仿Feign

一、Feign的基本使用

1.1、编写Feign客户端

假设现在我们有一个UserClient,如下:

@FeignClient(value = Services.SYSTEM_SERVER)
public interface UserClient {
 
    /**
     * 根据用户名,获取用户信息
     * @param account
     * @return
     */
    @RequestMapping(value = "/user/getByAccount", method = RequestMethod.GET)
    LoginInfo getUser(@RequestParam("account") String account);
}

1.2、 使用FeignClient远程调用

feign的调用也非常的简单,如下:

public class FeignClientTests {
 
    @Autowired
    private UserClient userClient;
 
    public void getUser() {
        LoginInfo userInfo = userClient.getUser("zhangsan");
    }
}

1.3、UserClient测试调用引发的疑问

可以看出来 UserClient使用的时候是使用@autowired注解注入的,但是很明显的是,UserClient是一个接口(interface),它并没有实现类,比如(UserClientImpl implements UserClient)。那么它是如何被注入并且可以直接使用的呢?

3 二、验证接口在Spring注入

2.1、 Spring的Class类型组件注入

编写SpringComponent组件

@Component
public class SpringComponent {
 
    public void helloWorld() {
        System.out.println("Hello World");
    }
}

编写测试类调用组件

@RunWith(SpringRunner.class)
@SpringBootTest
public class HttpApplicationTests {
 
    @Autowired
    SpringComponent springComponent;
 
    @Test
    public void contextLoads() {
        springComponent.helloWorld();
    }
 
}

执行一下测试类,输出:

Hello World

2.2、 Spring注入接口组件尝试

编写SpringInterfaceComponent接口组件

@Component
public interface SpringInterfaceComponent {
    
    default void helloWorld() {
        System.out.println("Hello World");
    }
}

编写测试类调用组件

@RunWith(SpringRunner.class)
@SpringBootTest
public class HttpApplicationTests2 {
 
    @Autowired
    SpringInterfaceComponent springInterfaceComponent;
 
    @Test
    public void contextLoads() {
        springInterfaceComponent.helloWorld();
    }
}

执行一下测试类,输出:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.jumper.lib.http.demo.SpringInterfaceComponent' available

很明显,注入不了,因为他没有实现类。

2.3、 对Spring注入接口类改写

SpringInterfaceComponent移除@Component注解

public interface SpringInterfaceComponent {
 
    default void helloWorld() {
        System.out.println("Hello World");
    }
}

编写SpringInterfaceComponent的实现类

@Component
public class SpringInterfaceComponentImpl implements SpringInterfaceComponent {
    
}
 

测试类调用组件

@RunWith(SpringRunner.class)
@SpringBootTest
public class HttpApplicationTests2 {
 
    @Autowired
    SpringInterfaceComponent springInterfaceComponent;
 
    @Test
    public void contextLoads() {
        springInterfaceComponent.helloWorld();
    }
}

执行一下测试类,输出:

Hello World

2.4、 验证猜想(Feign配置源码解析)

可以猜测,Feign一定是为接口生成了实现类注入到Spring容器里了。
那我们一起来看看Feign的注入原理

2.4.1 Feign的启动入口(@EnableFeignClients)

@EnableFeignClients(basePackages = ProjectConstants.baseScanPackage)
@SpringBootApplication(scanBasePackages = ProjectConstants.baseScanPackage)
public class AuthServer {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(AuthServer.class, args);
        String port = run.getEnvironment().getProperty("server.port");
        log.info("Auth Server is running --> http://localhost:{}", port);
    }
}

编写启动类的时候我们会加一个@EnableFeignClients注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
	String[] basePackages() default {};
}

进入@EnableFeignClients注解可以看到重点就在于存在一个@Import(FeignClientsRegistrar.class)
Feigin通过@Import来导入FeignClientsRegistrar.class(FeignClient解析客户端并注册到Spring容器的实现类)

2.4.2 FeiginClient注册器(FeignClientsRegistrar.class)

2.4.2.1、FeignClientsRegistrar声明

FeignClientsRegistrar 他分别实现了ImportBeanDefinitionRegistrar、ResourceLoaderAware、EnvironmentAware三个接口

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
		ResourceLoaderAware, EnvironmentAware {}

我们看看ImportBeanDefinitionRegistrar源码:
可以看出它声明一个注册Bean的接口

public interface ImportBeanDefinitionRegistrar {
 
	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 */
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
 
}

我们再看看ResourceLoaderAware源码:
就是一个注入ResourceLoader的接口

public interface ResourceLoaderAware extends Aware {
 
	/**
	 * Set the ResourceLoader that this object runs in.
	 * <p>This might be a ResourcePatternResolver, which can be checked
	 * through {@code instanceof ResourcePatternResolver}. See also the
	 * {@code ResourcePatternUtils.getResourcePatternResolver} method.
	 * <p>Invoked after population of normal bean properties but before an init callback
	 * like InitializingBean's {@code afterPropertiesSet} or a custom init-method.
	 * Invoked before ApplicationContextAware's {@code setApplicationContext}.
	 * @param resourceLoader the ResourceLoader object to be used by this object
	 * @see org.springframework.core.io.support.ResourcePatternResolver
	 * @see org.springframework.core.io.support.ResourcePatternUtils#getResourcePatternResolver
	 */
	void setResourceLoader(ResourceLoader resourceLoader);
 
}

再看看EnvironmentAware 源码:
是一个注入Environment 的接口

public interface EnvironmentAware extends Aware {
 
	/**
	 * Set the {@code Environment} that this component runs in.
	 */
	void setEnvironment(Environment environment);
}
 

我们再继续看FeignClientsRegistrar 它的成员变量,ResourceLoaderAware、 EnvironmentAware需要注入的资源

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
		ResourceLoaderAware, EnvironmentAware {
 
	// patterned after Spring Integration IntegrationComponentScanRegistrar
	// and RibbonClientsConfigurationRegistgrar
 
	private ResourceLoader resourceLoader;
 
	private Environment environment;
 
	public FeignClientsRegistrar() {
	
	}

2.4.2.2、registerBeanDefinitions方法

继续看看它最重要的实现接口

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

2.4.2.3、registerFeignClients方法

砸门也不关注注册配置的方法了,这次只探讨注册FeignClient的实现
我们看看registerFeignClients(metadata, registry);方法

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// ClassPath的条件扫描组件提供者
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		// 设置资源加载器
		scanner.setResourceLoader(this.resourceLoader);
		// 要扫描的包(@EnableFeignClients注解上添的那个)
		Set<String> basePackages;
 
		// 获取注解上的配置
		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		// 注解过滤器,设置只过滤出FeignClient注解标识的Bean
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			// 扫描器设置过滤器
			scanner.addIncludeFilter(annotationTypeFilter);
			// 获取注解的扫描包路径
			basePackages = getBasePackages(metadata);
		}
		else {
			final Set<String> clientClasses = new HashSet<>();
			basePackages = new HashSet<>();
			for (Class<?> clazz : clients) {
				basePackages.add(ClassUtils.getPackageName(clazz));
				clientClasses.add(clazz.getCanonicalName());
			}
			// 只知道是类型过滤器,暂时什么作用还不明白,求解!
			AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
				@Override
				protected boolean match(ClassMetadata metadata) {
					// 将类名上的[$]替换成[.]
					String cleaned = metadata.getClassName().replaceAll("\\$", ".");
					return clientClasses.contains(cleaned);
				}
			};
			scanner.addIncludeFilter(
					new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
		}
 
		for (String basePackage : basePackages) {
			// 从指定的包中扫描出和规范的BeanDefinition
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				// 扫描的Bean是否是AnnotatedBeanDefinition的子类
				// 虽然看不懂AnnotatedBeanDefinition是什么意思,但是顾名思义我觉得是通过注解扫出来的BeanDefinition就是他的子类
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
					// verify annotated class is an interface
					AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
					// 获取beanDefinition的元数据,你想要的他基本都有
					AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
					// 验证@FeignClient修饰的必须是接口
					Assert.isTrue(annotationMetadata.isInterface(),
							"@FeignClient can only be specified on an interface");
 
					// 获取@FeignClient注解的属性
					Map<String, Object> attributes = annotationMetadata
							.getAnnotationAttributes(
									FeignClient.class.getCanonicalName());
					// 获取客户端名称
					String name = getClientName(attributes);
					// 为FeignClient指定配置类
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
					// 注册客户端
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}
 

2.4.2.4、注册FeiginClient具体实现registerFeignClient方法

由上可见,准备工作做好了,那么最关键的就是registerFeignClient(registry, annotationMetadata, attributes);方法了,我们继续探索。

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		// 被@FeignClient修饰的类名,比如 com.xxx.TestFeignClient,是自己编辑的
		String className = annotationMetadata.getClassName();
		// BeanDefinitionBuilder通过FeignClientFactoryBean这个类来生成BeanDefinition
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		// 验证fallback和fallbackFactory是不是接口
		validate(attributes);
		// 通过BeanDefinitionBuilder给beanDefinition增加属性
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
 
		String alias = name + "FeignClient";
		// 用Builder获取实际的BeanDefinition
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
 
		boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
 
		beanDefinition.setPrimary(primary);
 
		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}
		// 创建一个Bean定义的持有者
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		// 这里就是将Bean注册到Spring容器中
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

由上面一段代码,FeignClient客户端注册就此完成。
但是上面还有两个重点还没看完

FeignClientFactoryBean.class是如何实现的?
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);是注册到哪里?
我们先看第二个问题,直接进BeanDefinitionReaderUtils源码:

	public static void registerBeanDefinition(
			BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
			throws BeanDefinitionStoreException {
 
		// Register bean definition under primary name.
		String beanName = definitionHolder.getBeanName();
		registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
 
		// Register aliases for bean name, if any.
		String[] aliases = definitionHolder.getAliases();
		if (aliases != null) {
			for (String alias : aliases) {
				registry.registerAlias(beanName, alias);
			}
		}
	}

Debug跟踪发现具体注册实现是在DefaultListableBeanFactory中进行的。

现在回过头来看第一个问题就是FeignClientFactoryBean.class这个类的实现是什么,因为上面的一段代码并没有看出来注册的BeanDefinition跟我们自己编写的FeignClient(例如上面的:UserClient)有什么关系?

2.4.3、FeignClientFactoryBean

根据上面的代码推断,BeanDefinitionBuilder是根据这个类创建出来的,并且与被@FeignClient修饰的客户端的关系也是在这里实现的,我们一起来看看他的源码:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
		ApplicationContextAware {}

声明部分可以看到他实现了哪几个接口,这里我们只关注FactoryBean这个接口,进去看看源码:

public interface FactoryBean<T> {
 
	@Nullable
	T getObject() throws Exception;
	
	@Nullable
	Class<?> getObjectType();
	
	default boolean isSingleton() {
		return true;
	}
 
}

既然是注册Bean那么应该关注的就是获取bean的方法,所以我们一起看看FeignClientFactoryBean 重载的getObject()方法

@Override
	public Object getObject() throws Exception {
		FeignContext context = applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
 
		if (!StringUtils.hasText(this.url)) {
			String url;
			if (!this.name.startsWith("http")) {
				url = "http://" + this.name;
			}
			else {
				url = this.name;
			}
			url += cleanPath();
			return loadBalance(builder, context, new HardCodedTarget<>(this.type,
					this.name, url));
		}
		if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
			this.url = "http://" + this.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();
			}
			builder.client(client);
		}
		Targeter targeter = get(context, Targeter.class);
		return targeter.target(this, builder, context, new HardCodedTarget<>(
				this.type, this.name, url));
	}

这段代码关注最后两行就好了,该方法的返回值是Object类型的。是通过Targeter返回的,Targeter是一个接口:

interface Targeter {
	<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
				 Target.HardCodedTarget<T> target);
}

源码里并没有Java Doc很是难受,但是看得出来他是创建一个Feign代理对象。实现类由两个分别是DefaultTargeter和HystrixTargeter

DefaultTargeter源码很简单,就是直接调用Fegin的target方法生成目标对象。

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

HystrixTargeter源码复杂点,用的是HystrixFeign.Builder来包装的

@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
						Target.HardCodedTarget<T> target) {
		if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
			return feign.target(target);
		}
		feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
		SetterFactory setterFactory = getOptional(factory.getName(), context,
			SetterFactory.class);
		if (setterFactory != null) {
			builder.setterFactory(setterFactory);
		}
		Class<?> fallback = factory.getFallback();
		if (fallback != void.class) {
			return targetWithFallback(factory.getName(), context, target, builder, fallback);
		}
		Class<?> fallbackFactory = factory.getFallbackFactory();
		if (fallbackFactory != void.class) {
			return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
		}
 
		return feign.target(target);

2.4.4、feign.target()实现

feign.target()源码:

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }
 
    public Feign build() {
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                               logLevel, decode404);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder,
                                  errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
    }
  }

其实看到这里就已经可以看出来了feign生成代理是用的反射(生成ReflectiveFeign对象)。我们通用的代理有JDK动态代理和CGLIB两种代理。我们继续看看Feign用的是哪种呢?

2.4.5、ReflectiveFeign.newInstance(target)

我们直接看ReflectiveFeign的newInstance方法的源码:

  @Override
  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)));
      }
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
 
    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

忽略他生成的细节,其中有两行代码:

 InvocationHandler handler = factory.create(target, methodToHandler);
 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

可见,feign用的是jdk动态代理。

三、小结

3.1、Spring可以直接注入接口吗?

不行,需要实现类或者代理注册到Spring容器里才能注入成功。

3.2、我想写一个框架然后像Feign这样根据自定义注解来注册自己的Bean,怎么做?

不要讲原理,直接就是一把梭子,对着Feign注册的源码就是一顿Ctrl+a/v。
梳理流程如下
编写@EnableXXX注解,并且注解提供扫描包路径,或者不提供扫描包路径,直接扫描整个项目。
@EnableXXX注解上加上@Import(MyBeanRegister.class)注解导入你自己的Bean注册器。
编写自己的MyBeanRegister并实现ImportBeanDefinitionRegistrar、ResourceLoaderAware、EnvironmentAware这三个接口,主要重写registerBeanDefinitions方法
编写一个MyFactoryBean实现FactoryBean、InitializingBean、ApplicationContextAware ,主要重写FactoryBean的几个接口,返回接口代理对象实现。

3.3 我不仅要实现自定义注册,我还想像Feign一样就通过接口来注册Bean并且可以注入直接调用自定义方法,怎么实现?

Feign使用的是JDK动态代理,那就按JDK动态代理的方式来简单实现一下这个调用,代码如下:

/**
 * 仿FeignClient注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RxClient {
    /**
     * url
     */
    String baseUrl();
    /**
     * 前缀
     */
    String prefix() default "";
}
/**
 * 仿RequestLine注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RxGet {
    /**
     * 请求路径 /login
     */
    String path();
}
 
/**
 * 模拟feign代理,简化执行过程
 */
public class RxClientProxy implements InvocationHandler {
 
    private String baseUrl;
    private String prefix;
 
    public RxClientProxy(String baseUrl, String prefix) {
        this.baseUrl = baseUrl;
        this.prefix = prefix;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RxGet annotation = method.getAnnotation(RxGet.class);
        String url = getUrl(annotation);
 
        // 模拟调用接口
        Object rs = mockGet(url);
 
        System.out.println(String.format("模拟调用接口:\nurl -> %s\nres -> %s\n", url, rs));
        return rs;
    }
 
    private Object mockGet(String url) {
        return url.contains("username")?"一上访人":"幼儿园丶抠脚上单";
    }
 
    private String getUrl(RxGet annotation) {
        return this.baseUrl + this.prefix + annotation.path();
    }
}
/**
 * 生成RxClient代理对象工厂
 */
public class RxClientFactory {
 
    @SneakyThrows
    public Object getInstance(String className) {
 
        Class<?> aClass = Class.forName(className);
        RxClient annotation = aClass.getAnnotation(RxClient.class);
        return Proxy.newProxyInstance(aClass.getClassLoader(),
                new Class[]{aClass},
                new RxClientProxy(annotation.baseUrl(), annotation.prefix()));
    }
}
/**
 * 用户接口调用测试类
 */
@RxClient(baseUrl = "http://localhost:8080", prefix = "/user")
public interface UserRxClient {
 
    @RxGet(path = "/username")
    String getUsername();
 
    @RxGet(path = "/nickname")
    String getNickname();
}
/**
 * RxClient测试类
 */
public class RxClientCallTest {
 
    public static void main(String[] args) {
 
        // 创建RxClient工厂
        RxClientFactory factory = new RxClientFactory();
 
        // 全路径反射出客户端
        UserRxClient userRxClient = (UserRxClient) factory
                .getInstance("com.jumper.lib.http.proxy.impl.UserRxClient");
 
        // 调用获取昵称接口
        userRxClient.getNickname();
 
        // 调用获取用户名接口
        userRxClient.getUsername();
    }
}

执行RxClientCallTest的main方法输出:

模拟调用接口:
url -> http://localhost:8080/user/nickname
res -> 幼儿园丶抠脚上单
 
模拟调用接口:
url -> http://localhost:8080/user/username
res -> 一上访人
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值