Spring源码分析系列——AnnotationConfigApplicationContext(String... basePackages)扫描加载注解bean

分析方法

上一篇分析ClassPathXmlApplicationContext加载xml过程采用debug方式,本篇分析AnnotationConfigApplicationContext(String… basePackages)同样采用debug方式分析调用栈。

找到具体beanFactory,确定debug断点位置

先来看一下AnnotationConfigApplicationContext在ApplicationContext继承体系中的位置
在这里插入图片描述

在AnnotationConfigApplicationContext中查找getBean()方法,可以看到是在AbstractApplicationContext父类方法

public <T> T getBean(Class<T> requiredType) throws BeansException {
		assertBeanFactoryActive();
		return getBeanFactory().getBean(requiredType);
	}

查看getBeanFactory()方法

@Override
	public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

抽象方法
查看实现,在GenericApplicationContext中有实现

@Override
	public final ConfigurableListableBeanFactory getBeanFactory() {
		return this.beanFactory;
	}

查看this.beanFactory

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {

	private final DefaultListableBeanFactory beanFactory;
	public GenericApplicationContext() {
		this.beanFactory = new DefaultListableBeanFactory();
	}
}

可以看到beanFactory实际仍然是DefaultListableBeanFactory。
将断点打在DefaultListableBeanFactory的registerBeanDefinition方法
在这里插入图片描述

编写测试类,执行debug

编写的测试类

public class AnnotationConfig {

	public static void main(String[] args) {
		ApplicationContext context = new AnnotationConfigApplicationContext(
				"com.xxx.xxx");
		Boy boy = context.getBean(Lad.class);
		boy.sayLove();
	}
}

执行debug
在这里插入图片描述
我们看到第一次执行到DefaultListableBeanFactory的registerBeanDefinition方法注册的beanDefinition的beanName是“org.springframework.context.annotation.internalConfigurationAnnotationProcessor”
但这明显不是我们自己定义的bean,看名称应该是spring自己内部的processor,现在来看下调用栈,做了什么。
第一步调用AnnotationConfigApplicationContext的构造方法的this()无参构造方法

public AnnotationConfigApplicationContext(String... basePackages) {
		this();
		scan(basePackages);
		refresh();
	}
public AnnotationConfigApplicationContext() {
		this.reader = new AnnotatedBeanDefinitionReader(this);
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

第二步初始化reader和scanner,调用栈发生在reader的实例化过程中
AnnotatedBeanDefinitionReader构造方法

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
		this(registry, getOrCreateEnvironment(registry));
	}

下一步

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		Assert.notNull(environment, "Environment must not be null");
		this.registry = registry;
		this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
		AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
	}

走到AnnotationConfigUtils类中

public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
		registerAnnotationConfigProcessors(registry, null);
	}

registerAnnotationConfigProcessors()方法分析

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
			BeanDefinitionRegistry registry, @Nullable Object source) {

		DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
		if (beanFactory != null) {
			if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
				beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
			}
			if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
				beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
			}
		}

		Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);

		if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
		if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
		if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition();
			try {
				def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,
						AnnotationConfigUtils.class.getClassLoader()));
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
			}
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
		}

		return beanDefs;
	}

核心代码片段

Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);

		if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

@Configuration注解是如何生效的

这里定义了RootBeanDefinition,beanClass是ConfigurationClassPostProcessor,看名称推测和@Configuration注解相关,简单看一下

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
}

实现了BeanDefinitionRegistryPostProcessor,而这个PostProcessor实现了BeanFactoryPostProcessor
我们之前的文章提过,在AbstractApplicationContext的refresh()方法中加载完beanDefinitions后会执行invokeBeanFactoryPostProcessors(beanFactory);方法,此时就会调用所有的BeanFactoryPostProcessor,在这个ConfigurationClassPostProcessor里就会对@configuration注解的类做处理,下一篇文章单独细分析这个类。
可以看到这里除了会注册ConfigurationClassPostProcessor,还有别的内部processor

Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);

		if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
		if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
		if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition();
			try {
				def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,
						AnnotationConfigUtils.class.getClassLoader()));
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
			}
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));
		}

		if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
			RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
			def.setSource(source);
			beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
		}

scanner扫描自定义bean

这些内部processor注册完后才会注册我们自己定义的bean
在这里插入图片描述
此时注册的是我们自己定义的bean “lad”
看下调用栈

public AnnotationConfigApplicationContext(String... basePackages) {
		this();
		scan(basePackages);
		refresh();
	}

走了构造方法中扫描包的方法。

@Override
	public void scan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		this.scanner.scan(basePackages);
	}

委托给scanner
继续ClassPathBeanDefinitionScanner的scan方法

public int scan(String... basePackages) {
		int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

		doScan(basePackages);

		// Register annotation config processors, if necessary.
		if (this.includeAnnotationConfig) {
			AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
		}

		return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
	}

走doScan方法

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}

看一下doScan的关键环节
在这里插入图片描述
1.先将basePackages扫描包遍历,2.再将basePackage单个扫描包通过findCandidateComponents()方法把string路径转换为BeanDefinition的Set集合,3.遍历candidates这个BeanDefinition集合,4.调用registerBeanDefinition()方法将beanDefinition注册到bean工厂。

关键是findCandidateComponents方法是怎么把string路径转换为BeanDefinition的set集合的呢?

findCandidateComponents()方法分析

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
		if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
			return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
		}
		else {
			return scanCandidateComponents(basePackage);
		}
	}

上面一种方法是利用索引插件快速扫描,我们看后一种通用的scanCandidateComponents(basePackage)方法
该方法的核心代码

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + '/' + this.resourcePattern;
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setSource(resource);
							if (isCandidateComponent(sbd)) {								
								candidates.add(sbd);
							}
						}
						
					}					
				}				
			}
		}		
		return candidates;
	}

方法的关键步骤
在这里插入图片描述

先将basePackage转换为Resource 数组,这个Resource就是封装了包内每个class文件。
遍历Resource数组,将resource转换为MetadataReader,isCandidateComponent(metadataReader)方法判断是否满足要求的候选组件,如果是则创建ScannedGenericBeanDefinition,并添加到candidates。
我们重点分析一下MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
和isCandidateComponent(metadataReader)这两个方法。

Resource转换为MetadataReader分析

MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
先看getMetadataReaderFactory()方法

public final MetadataReaderFactory getMetadataReaderFactory() {
		if (this.metadataReaderFactory == null) {
			this.metadataReaderFactory = new CachingMetadataReaderFactory();
		}
		return this.metadataReaderFactory;
	}

创建的是CachingMetadataReaderFactory()。
再看它的getMetadataReader(resource)方法

@Override
	public MetadataReader getMetadataReader(Resource resource) throws IOException {
		if (this.metadataReaderCache instanceof ConcurrentMap) {
			// No synchronization necessary...
			MetadataReader metadataReader = this.metadataReaderCache.get(resource);
			if (metadataReader == null) {
				metadataReader = super.getMetadataReader(resource);
				this.metadataReaderCache.put(resource, metadataReader);
			}
			return metadataReader;
		}
		else if (this.metadataReaderCache != null) {
			synchronized (this.metadataReaderCache) {
				MetadataReader metadataReader = this.metadataReaderCache.get(resource);
				if (metadataReader == null) {
					metadataReader = super.getMetadataReader(resource);
					this.metadataReaderCache.put(resource, metadataReader);
				}
				return metadataReader;
			}
		}
		else {
			return super.getMetadataReader(resource);
		}
	}

this.metadataReaderCache是一个ConcurrentMap 这里不展示具体查找细节,这里用了一个缓存,所以走的是

metadataReader = super.getMetadataReader(resource);

跟进去父类SimpleMetadataReaderFactory

@Override
	public MetadataReader getMetadataReader(Resource resource) throws IOException {
		return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
	}

创建了一个SimpleMetadataReader,继续看一下

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
		SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
		getClassReader(resource).accept(visitor, PARSING_OPTIONS);
		this.resource = resource;
		this.annotationMetadata = visitor.getMetadata();
	}

用ASM字节码技术读取类注解等元信息

这里创建了一个SimpleAnnotationMetadataReadingVisitor 这样一个visitor
先查看一下这个类

final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor

继承自ClassVisitor,而ClassVisitor这个类则是位于spring的asm包下

package org.springframework.asm;


public abstract class ClassVisitor

再看这句

getClassReader(resource).accept(visitor, PARSING_OPTIONS);
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);
			}
		}
	}

这里的ClassReader也是spring的asm包下的,读取了resource内容,通过accept方法,visitor就可以读取resource内容的元信息

再看这句

this.annotationMetadata = visitor.getMetadata();
public SimpleAnnotationMetadata getMetadata() {
		Assert.state(this.metadata != null, "AnnotationMetadata not initialized");
		return this.metadata;
	}
private SimpleAnnotationMetadata metadata;

返回一个 SimpleAnnotationMetadata 并赋值给SimpleMetadataReader 的this.annotationMetadata属性
这样就可以通过SimpleMetadataReader的annotationMetadata属性获取这个Resource的类元信息。

思考一下为什么用ASM字节码技术读取类的元信息,而不用反射的方式?

最主要的原因是:运用反射就会把类加载到内存,而这个类不一定需要,用ASM字节码技术并不会把这个类加载到内存。

isCandidateComponent(metadataReader)方法分析

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return false;
			}
		}
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}
为什么加了@component注解就会被spring管理?

这里可以看到对我们经常配置的excludeFilters和includeFilters这样的TypeFileter做判断,如果默认都没配置的话includeFilters会包含AnnotationTypeFilter,它的annotationType属性是Component.class。name它是在什么时候放进includeFilters里的呢?我们简单看一下

public AnnotationConfigApplicationContext(String... basePackages) {
		this();
		scan(basePackages);
		refresh();
	}

看this()构造函数方法

public AnnotationConfigApplicationContext() {
		this.reader = new AnnotatedBeanDefinitionReader(this);
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

看ClassPathBeanDefinitionScanner构造方法,层层跟进到

public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment, @Nullable ResourceLoader resourceLoader) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;

		if (useDefaultFilters) {
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(resourceLoader);
	}

useDefaultFilters传的是true,看registerDefaultFilters()方法

protected void registerDefaultFilters() {
		this.includeFilters.add(new AnnotationTypeFilter(Component.class));
		ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
		try {
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
			logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
		}
		catch (ClassNotFoundException ex) {
			// JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
		}
		try {
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
			logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
		}
		catch (ClassNotFoundException ex) {
			// JSR-330 API not available - simply skip.
		}
	}

第一行代码就是在includeFilters加入@Component注解的AnnotationTypeFilter,此外,如果项目引入了javax包,还会尝试去加入javax中的@ManagedBean和@Named注解的AnnotationTypeFilter。

好了,现在知道为什么会把有@component注解的类注册进bean工厂了。

TypeFilter的match()方法

接着看TypeFilter的match()方法,
会调用子类AbstractTypeHierarchyTraversingFilter的重写方法,只看matchSelf(metadataReader)方法

@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
			throws IOException {
		if (matchSelf(metadataReader)) {
			return true;
		}
	}

最终还是调用到AnnotationTypeFilter自己的matchSelf(metadataReader)方法

@Override
	protected boolean matchSelf(MetadataReader metadataReader) {
		AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
		return metadata.hasAnnotation(this.annotationType.getName()) ||
				(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
	}

从metadataReader中获取注解元信息,看看有没有包含@component注解,如果有返回true。
了解原理之后我们也可以自定义TypeFilter实现该接口,重写match()方法。

总结

到此可以知道,AnnotationConfigApplicationContext(String… basePackages) 是把string类型的basePackage扫描到包下边的class文件,转换为Resource数组,再通过spring.asm包下的SimpleAnnotationMetadataReadingVisitor和ClassReader两个类,用ASM字节码技术读取resource的类的元信息。用MetadataReader的annotationMetadata属性封装这些类的元信息。判断扫描出来的这些类是否满足TypeFilter。此外scanner初始化时会把@Component封装为AnnotationTypeFileter加入到includeFilters,所以扫描到的类如果有@Component注解,则会注册到beanFactory。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值