SpringBoot源码学习——元数据

元数据:数据的数据。比如Class就是一种元数据。Metadata在org.springframework.core.type包名下,还有用于读取的子包classreading也是重要知识点。此体系大致的类结构列出如下图:
在这里插入图片描述
可以看到顶层接口有两个:ClassMetadata和AnnotatedTypeMetadata

ClassMetadata

对Class的抽象和适配

从官方注释解释:这是一个对具体类的抽象访问接口。同时不需要目标类被加载


public interface ClassMetadata {

	//返回类名
	String getClassName();

	//判断当前类是不是一个接口
	boolean isInterface();

	//判断当前类是不是一个注解类
	boolean isAnnotation();

	//判断当前类是不是抽象的
	boolean isAbstract();

	//判断当前是否能够实例化
	default boolean isConcrete() {
		return !(isInterface() || isAbstract());
	}
	//是否为final类
	boolean isFinal();

	//是否是独立的(能够创建对象的)  比如是Class、或者内部类、静态内部类
	boolean isIndependent();

	//是否有内部类之类的东西
	default boolean hasEnclosingClass() {
		return (getEnclosingClassName() != null);
	}
	//获取内部类的类名
	String getEnclosingClassName();

	//判断是否有父类
	default boolean hasSuperClass() {
		return (getSuperClassName() != null);
	}

	//获取父类的类名
	@Nullable
	String getSuperClassName();

	//获取类实现的所有接口类名,具体依赖于Class#getSuperclass
	String[] getInterfaceNames();

	//基于:Class#getDeclaredClasses  返回类中定义的公共、私有、保护的内部类
	String[] getMemberClassNames();

}

从上面接口的源码可以看出来,ClassMetadata不同于类对象,虽然它们都可以读取类中的信息,但它更加针对于类,相较于类对象它的API更直接,更丰富

StandardClassMetadata

StandardClassMetadata是ClassMetadata的一个实现类。基于Java标准的(Standard)反射实现元数据的获取。

public class StandardClassMetadata implements ClassMetadata {
	// 用于内省的Class类
	private final Class<?> introspectedClass;


	// 唯一构造函数:传进来的Class,作为内部的内省对象
	@Deprecated
	public StandardClassMetadata(Class<?> introspectedClass) {
		Assert.notNull(introspectedClass, "Class must not be null");
		this.introspectedClass = introspectedClass;
	}

	// 后面所有的方法实现,都是基于introspectedClass,类似代理模式。举例如下:
	public final Class<?> getIntrospectedClass() {
		return this.introspectedClass;
	}

	@Override
	public String getClassName() {
		return this.introspectedClass.getName();
	}

	@Override
	public boolean isInterface() {
		return this.introspectedClass.isInterface();
	}

	@Override
	public boolean isAnnotation() {
		return this.introspectedClass.isAnnotation();
	}

	@Override
	public boolean isAbstract() {
		return Modifier.isAbstract(this.introspectedClass.getModifiers());
	}

	@Override
	public boolean isFinal() {
		return Modifier.isFinal(this.introspectedClass.getModifiers());
	}

}

从上面的源码中可以看出来,StandardClassMetadata对ClassMetadata的实现其实就是基于对Class对象的API调用

ClassMetadataReadingVisitor

该类是ClassMetadata的另一种实现,但不同于StandardClassMetadata,该类继承自ClassVisitor,是通过ASM方式实现的

class ClassMetadataReadingVisitor extends ClassVisitor implements ClassMetadata {
...
}

AnnotatedTypeMetadata

对注解元素的封装适配

什么叫注解元素(AnnotatedElement)?比如我们常见的Class、Method、Constructor、Parameter等等都属于它的子类都属于注解元素。简单理解:只要能在上面标注注解都属于这种元素.这个接口提供了对注解统一的、便捷的访问,使用起来更加的方便高效了。

public interface AnnotatedTypeMetadata {

	//获取当前类上所有直接注解的注解的合并对象,也就是说通过MergedAnnotations可以获取到当前类标注的所有你想获取到的属性信息,而无需获取单个注解元数据来进行访问
	MergedAnnotations getAnnotations();

	//判断是否被某个注解标注
	default boolean isAnnotated(String annotationName) {
		return getAnnotations().isPresent(annotationName);
	}

	//取得指定类型注解的所有的属性 - 值(k-v)
	default Map<String, Object> getAnnotationAttributes(String annotationName) {
		return getAnnotationAttributes(annotationName, false);
	}

	//classValuesAsString:若是true表示 Class用它的字符串的全类名来表示。这样可以避免Class被提前加载
	default Map<String, Object> getAnnotationAttributes(String annotationName,
			boolean classValuesAsString) {

		MergedAnnotation<Annotation> annotation = getAnnotations().get(annotationName,
				null, MergedAnnotationSelectors.firstDirectlyDeclared());
		if (!annotation.isPresent()) {
			return null;
		}
		return annotation.asAnnotationAttributes(Adapt.values(classValuesAsString, true));
	}

	//获取所有的注解的注解属性
	default MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName) {
		return getAllAnnotationAttributes(annotationName, false);
	}

	//获取所有的注解的注解属性但不加载类
	default MultiValueMap<String, Object> getAllAnnotationAttributes(
			String annotationName, boolean classValuesAsString) {

		Adapt[] adaptations = Adapt.values(classValuesAsString, true);
		return getAnnotations().stream(annotationName)
				.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
				.map(MergedAnnotation::withNonMergedAttributes)
				.collect(MergedAnnotationCollectors.toMultiValueMap(map ->
						map.isEmpty() ? null : map, adaptations));
	}

}

通过源码,可以明白通过这个接口可以获取所有注解相关的信息数据

同时,这个类有两个子接口应的都提供了标准实现以及基于ASM的Visitor模式实现。

ASM 是一个通用的 Java 字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。 ASM 虽然提供与其他 Java 字节码框架如 Javassist,CGLIB类似的功能,但是其设计与实现小而快,且性能足够高。也就是说,通过ASM可以不同加载类直接获得类的信息

MethodMetadata

方法描述,代表方法的元数据接口

public interface MethodMetadata extends AnnotatedTypeMetadata {

	//返回方法名
	String getMethodName();

	//返回方法所在类的类名
	String getDeclaringClassName();

	//获取返回类型
	String getReturnTypeName();

	//是不是抽象方法
	boolean isAbstract();

	//是不是静态方法
	boolean isStatic();

	//是不是Final方法
	boolean isFinal();

	//是不是重写的方法
	boolean isOverridable();

}

StandardMethodMetadata

MethodMetadata的子类实现之一,基于Class的反射来实现

MethodMetadataReadingVisitor

MethodMetadata的子类实现之一,基于ASM来实现
继承自ASM``的org.springframework.asm.MethodVisitor采用Visitor的方式读取到元数据。

public class MethodMetadataReadingVisitor extends MethodVisitor implements MethodMetadata {
	...
}

AnnotationMetadata

从官方注释解释:这是一个对具体类的注解的抽象访问接口。同时不需要目标类被加载
意思是:我们可以通过AnnotationMetadata来访问类上标注的注解元信息,并且我们AnnotationMetadata是可以不用加载类来获取对象的。这能够极大避免许多不需要的类被加载到内存中

AnnotationMetadata代表的是被一个类的所有与注解有关的元数据信息,而并非只是单纯指某个注解.它是ClassMetadata和AnnotatedTypeMetadata的子接口,具有两者共同能力,并且新增了访问注解的相关方法。可以简单理解为它是对注解的抽象。

方法介绍

方法解释参数
getAnnotationTypes()获取类上标注的所有注解的注解类名
getMetaAnnotationTypes(String annotationName)拿到类上指定的注解类型的类名annotationName:注解类型的全类名
hasAnnotation(String annotationName)是否包含指定注解annotationName:全类名
hasMetaAnnotation(String metaAnnotationName)用于判断注解类型自己是否被某个元注解类型所标注annotationName:注解类型的全类名
hasAnnotatedMethods(String annotationName)判断类中是否存在被注解标注的方法annotationName:注解类型的全类名
getAnnotatedMethods(String annotationName)返回所有的标注有指定注解的方法元信息。注意返回的是MethodMetadataannotationName:注解类型的全类名
AnnotationMetadata introspect(Class<?> type)为传入的类对象创建一个对应的 AnnotationMetadatatype:类对象

同样的它提供了两种实现方式。

StandardAnnotationMetadata

继承了StandardClassMetadata,很明显关于ClassMetadata的实现部分就交给此父类了,自己只关注于AnnotationMetadata接口的实现。

public class StandardAnnotationMetadata extends StandardClassMetadata implements AnnotationMetadata {
...
}

前面提到了StandardClassMetadata是基于反射实现的,因此,作为对StandardClassMetadata的注解补充,自然需要同步,因此该类就是通过反射来实现的

AnnotationMetadataReadingVisitor

继承自ClassMetadataReadingVisitor,同样的ClassMetadata部分实现交给了它。

说明:ClassMetadataReadingVisitor是org.springframework.core.type.classreading包下的类,同包的还有我下面重点讲述的MetadataReader。此实现类最终委托给AnnotationMetadataReadingVisitor来做的,而它便是ClassMetadataReadingVisitor的子类(MetadataReader的底层实现就是它,使用的ASM的ClassVisitor模式读取元数据)。

public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata {
	...
}

MetadataReader接口

你是否有疑问:为何Spring要提供一个标准实现和一个ASM的实现呢?这里就能给你答案。
此接口是一个访问ClassMetadata等的简单门面,实现是委托给org.springframework.asm.ClassReader、ClassVisitor来处理的,它不用把Class加载进JVM就可以拿到元数据,因为它读取的是资源:Resource,这是它最大的优势所在。

public interface MetadataReader {

	//返回此Class的类文件对象
	Resource getResource();

	//返回此Class的ClassMetadata对象
	ClassMetadata getClassMetadata();

	//返回此Class的AnnotationMetadata对象
	AnnotationMetadata getAnnotationMetadata();
}

该接口的继承树如下:在这里插入图片描述
也就是说该接口只有一个实现SimpleMetadataReader

SimpleMetadataReader

它是基于ASM的org.springframework.asm.ClassReader的简单实现。请注意:此类是非public的,而是default包访问权限。

final class SimpleMetadataReader implements MetadataReader {

	private static final int PARSING_OPTIONS = ClassReader.SKIP_DEBUG
			| ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES;

	private final Resource resource;

	private final AnnotationMetadata annotationMetadata;


	//唯一构造函数:给上面两个final属性赋值,下面就只需提供get方法即可
	SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
		//SimpleAnnotationMetadataReadingVisitor继承了ClassVisitor可以通过ASM方式获取目标类文件中的信息
		SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
		getClassReader(resource).accept(visitor, PARSING_OPTIONS);
		this.resource = resource;
		this.annotationMetadata = visitor.getMetadata();
	}

	private static ClassReader getClassReader(Resource resource) throws IOException {
		try (InputStream is = resource.getInputStream()) {
			try {
				return new ClassReader(is);
			}
			catch (IllegalArgumentException ex) {
				throw new NestedIOException("ASM ClassReader failed to parse class file - " +
						"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
			}
		}
	}


	@Override
	public Resource getResource() {
		return this.resource;
	}

	@Override
	public ClassMetadata getClassMetadata() {
		return this.annotationMetadata;
	}

	@Override
	public AnnotationMetadata getAnnotationMetadata() {
		return this.annotationMetadata;
	}

}

MetadataReaderFactory

MetadataReader的实现都并未public暴露出来,所以我们若想得到它的实例,就只能通过此工厂。

public interface MetadataReaderFactory {

	MetadataReader getMetadataReader(String className) throws IOException;

	MetadataReader getMetadataReader(Resource resource) throws IOException;

}

该工厂的继承类如下
在这里插入图片描述

SimpleMetadataReaderFactory

public class SimpleMetadataReaderFactory implements MetadataReaderFactory {

	private final ResourceLoader resourceLoader;


	//如果构造时不传入指定资源加载器,则使用DefaultResourceLoader
	public SimpleMetadataReaderFactory() {
		this.resourceLoader = new DefaultResourceLoader();
	}

	public SimpleMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) {
		this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
	}

	public SimpleMetadataReaderFactory(@Nullable ClassLoader classLoader) {
		this.resourceLoader =
				(classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader());
	}

	//获取资源加载器
	public final ResourceLoader getResourceLoader() {
		return this.resourceLoader;
	}

	//根据类名获取对应的MetadataReader
	public MetadataReader getMetadataReader(String className) throws IOException {
		try {
			String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX +
					ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX;
			Resource resource = this.resourceLoader.getResource(resourcePath);
			return getMetadataReader(resource);
		}
		catch (FileNotFoundException ex) {
			int lastDotIndex = className.lastIndexOf('.');
			if (lastDotIndex != -1) {
				String innerClassName =
						className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1);
				String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX +
						ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX;
				Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath);
				if (innerClassResource.exists()) {
					return getMetadataReader(innerClassResource);
				}
			}
			throw ex;
		}
	}

	//根据资源对象创建一个SimpleMetadataReader
	public MetadataReader getMetadataReader(Resource resource) throws IOException {
		return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
	}

}

CachingMetadataReaderFactory

它继承自SimpleMetadataReaderFactory,没有其它特殊的,就是提供了缓存能力private Map<Resource, MetadataReader> metadataReaderCache,提高访问效率。

public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {

	public static final int DEFAULT_CACHE_LIMIT = 256;

	@Nullable
	private Map<Resource, MetadataReader> metadataReaderCache;
	...
}

因为有了它,所以SimpleMetadataReaderFactory就不需要被直接使用了,用它代替。Spring内自然也使用的便是效率更高的它

ConcurrentReferenceCachingMetadataReaderFactory

相较于前者,将前者的普通Map缓存替换为了线程安全的ConcurrentMap

Spring注解编程中AnnotationMetadata的使用

Spring从3.0开始就大量的使用到了注解编程模式,所以可想而知它对元数据(特别是注解元数据)的使用是非常多的。
对于MetadataReaderFactory的应用主要体现在几个地方

  • ConfigurationClassPostProcessor:该属性值最终会传给ConfigurationClassParser,用于@EnableXXX / @Import等注解的解析上~
  • ClassPathScanningCandidateComponentProvider:它用于@ComponentScan的时候解析,拿到元数据判断是否是@Component的派生注解
  • Mybatis的SqlSessionFactoryBean:它在使用上非常简单,只是为了从Resouece里拿到ClassName而已。classMetadata.getClassName()
  • SourceClass:它是对source对象一个轻量级的包装,持有AnnotationMetadata 元数据

说明:Spring的@EnableXXX模块注解很多都使用到了ImportSelector这个接口,此接口的回调方法参数第一个便是AnnotationMetadata代表着@Import所在类的注解的一些元数据们。通常我们会这样使用它:

// 1、转换成AnnotationAttributes(LinkedHashMap),模糊掉注解类型(常用)
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);

// 2、拿到指定类型注解的元数据信息(也较为常用)
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true))

// 3、直接使用MetaData
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false);

使用示例

package com.xhy.springapplicationrun.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.StandardAnnotationMetadata;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.HashMap;

// 准备一个Class类 作为Demo演示
@Repository("repositoryName")
@Service("serviceName")
@EnableAsync
public class MetaDemo extends HashMap<String, String> implements Serializable {
    private static class InnerClass {
    }

    @Autowired
    private String getName() {
        return "demo";
    }



    public static void main(String[] args) {
        StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(MetaDemo.class, true);

        // 演示ClassMetadata的效果
        System.out.println("==============ClassMetadata==============");
        ClassMetadata classMetadata = metadata;
        System.out.println(classMetadata.getClassName()); //com.fsx.maintest.MetaDemo
        System.out.println(classMetadata.getEnclosingClassName()); //null  如果自己是内部类此处就有值了
        System.out.println(StringUtils.arrayToCommaDelimitedString(classMetadata.getMemberClassNames())); //com.fsx.maintest.MetaDemo$InnerClass 若木有内部类返回空数组[]
        System.out.println(StringUtils.arrayToCommaDelimitedString(classMetadata.getInterfaceNames())); // java.io.Serializable
        System.out.println(classMetadata.hasSuperClass()); // true(只有Object这里是false)
        System.out.println(classMetadata.getSuperClassName()); // java.util.HashMap

        System.out.println(classMetadata.isAnnotation()); // false(是否是注解类型的Class,这里显然是false)
        System.out.println(classMetadata.isFinal()); // false
        System.out.println(classMetadata.isIndependent()); // true(top class或者static inner class,就是独立可new的)
        // 演示AnnotatedTypeMetadata的效果
        System.out.println("==============AnnotatedTypeMetadata==============");
        AnnotatedTypeMetadata annotatedTypeMetadata = metadata;
        System.out.println(annotatedTypeMetadata.isAnnotated(Service.class.getName())); // true(依赖的AnnotatedElementUtils.isAnnotated这个方法)
        System.out.println(annotatedTypeMetadata.isAnnotated(Component.class.getName())); // true

        System.out.println(annotatedTypeMetadata.getAnnotationAttributes(Service.class.getName())); //{value=serviceName}
        System.out.println(annotatedTypeMetadata.getAnnotationAttributes(Component.class.getName())); // {value=repositoryName}(@Repository的value值覆盖了@Service的)
        System.out.println(annotatedTypeMetadata.getAnnotationAttributes(EnableAsync.class.getName())); // {order=2147483647, annotation=interface java.lang.annotation.Annotation, proxyTargetClass=false, mode=PROXY}

        // 看看getAll的区别:value都是数组的形式
        System.out.println(annotatedTypeMetadata.getAllAnnotationAttributes(Service.class.getName())); // {value=[serviceName]}
        System.out.println(annotatedTypeMetadata.getAllAnnotationAttributes(Component.class.getName())); // {value=[, ]} --> 两个Component的value值都拿到了,只是都是空串而已
        System.out.println(annotatedTypeMetadata.getAllAnnotationAttributes(EnableAsync.class.getName())); //{order=[2147483647], annotation=[interface java.lang.annotation.Annotation], proxyTargetClass=[false], mode=[PROXY]}

        // 演示AnnotationMetadata子接口的效果(重要)
        System.out.println("==============AnnotationMetadata==============");
        AnnotationMetadata annotationMetadata = metadata;
        System.out.println(annotationMetadata.getAnnotationTypes()); // [org.springframework.stereotype.Repository, org.springframework.stereotype.Service, org.springframework.scheduling.annotation.EnableAsync]
        System.out.println(annotationMetadata.getMetaAnnotationTypes(Service.class.getName())); // [org.springframework.stereotype.Component, org.springframework.stereotype.Indexed]
        System.out.println(annotationMetadata.getMetaAnnotationTypes(Component.class.getName())); // [](meta就是获取注解上面的注解,会排除掉java.lang这些注解们)

        System.out.println(annotationMetadata.hasAnnotation(Service.class.getName())); // true
        System.out.println(annotationMetadata.hasAnnotation(Component.class.getName())); // false(注意这里返回的是false)

        System.out.println(annotationMetadata.hasMetaAnnotation(Service.class.getName())); // false(注意这一组的结果和上面相反,因为它看的是meta)
        System.out.println(annotationMetadata.hasMetaAnnotation(Component.class.getName())); // true

        System.out.println(annotationMetadata.hasAnnotatedMethods(Autowired.class.getName())); // true
        annotationMetadata.getAnnotatedMethods(Autowired.class.getName()).forEach(methodMetadata -> {
            System.out.println(methodMetadata.getClass()); // class org.springframework.core.type.StandardMethodMetadata
            System.out.println(methodMetadata.getMethodName()); // getName
            System.out.println(methodMetadata.getReturnTypeName()); // java.lang.String
        });
    }
}

像这些元数据,在框架设计时候很多时候我们都希望从File(Resource)里得到,而不是从Class文件里获取,所以就是MetadataReader和MetadataReaderFactory。
因为MetadataReader的实现类都是包级别的访问权限,所以它的实例只能来自工厂

public static void main(String[] args) throws IOException {
    CachingMetadataReaderFactory readerFactory = new CachingMetadataReaderFactory();
    // 下面两种初始化方式都可,效果一样
    //MetadataReader metadataReader = readerFactory.getMetadataReader(MetaDemo.class.getName());
    MetadataReader metadataReader = readerFactory.getMetadataReader(new ClassPathResource("com/fsx/maintest/MetaDemo.class"));

    ClassMetadata classMetadata = metadataReader.getClassMetadata();
    AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
    Resource resource = metadataReader.getResource();

    System.out.println(classMetadata); // org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor@79079097
    System.out.println(annotationMetadata); // org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor@79079097
    System.out.println(resource); // class path resource [com/fsx/maintest/MetaDemo.class]

}

总结

元数据,是框架设计中必须的一个概念,所有的流行框架里都能看到它的影子,包括且不限于Spring、SpringBoot、SpringCloud、MyBatis、Hibernate等。它的作用肯定是大大的,它能模糊掉具体的类型,能让数据输出变得统一,能解决Java抽象解决不了的问题,比如运用得最广的便是注解,因为它不能继承无法抽象,所以用元数据方式就可以完美行成统一的向上抽取让它变得与类型无关,也就是常说的模糊效果,这便是框架的核心设计思想。

不管是ClassMetadata还是AnnotatedTypeMetadata都会有基于反射和基于ASM的两种解决方案,他们能使用于不同的场景:

标准反射:它依赖于Class,优点是实现简单,缺点是使用时必须把Class加载进来。
ASM:无需提前加载Class入JVM,所有特别特别适用于形如Spring应用扫描的场景(扫描所有资源,但并不是加载所有进JVM/容器~)

在这里插入图片描述

此文章基于https://blog.csdn.net/f641385712/article/details/88765470学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来是肖某人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值