java-spring 图灵 04 doscan方法,重点是scanCandidateComponents方法

01.本次的重点依旧是扫描函数,这次是spring中的源码:

02.第一步,构造AnnotationConfigApplicationContext

主方法:

	public static void main(String[] args) {

		// 创建一个Spring容器
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

		UserService userService = (UserService) applicationContext.getBean("userService");


		userService.test();
		}

AnnotationConfigApplicationContext 的构造函数:(这个时候是有参)

	public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
		// 构造DefaultListableBeanFactory、AnnotatedBeanDefinitionReader、ClassPathBeanDefinitionScanner
		this();
		register(componentClasses);
		refresh();
	}

this()是指的AnnotationConfigApplicationContext 的无参构造函数:

StartupStep 这个接口是用来记录 AnnotatedBeanDefinitionReader 这个函数运行的时间的

public AnnotationConfigApplicationContext() {
		StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
		// 额外会创建StandardEnvironment
		this.reader = new AnnotatedBeanDefinitionReader(this);
		createAnnotatedBeanDefReader.end();
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

03.来到了本次的重点类:ClassPathBeanDefinitionScanner类,这里依次执行方法

public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
		this(registry, true);
	}
	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
		this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
	}
	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment) {

		this(registry, useDefaultFilters, environment,
				(registry instanceof ResourceLoader ? (ResourceLoader) registry : null));
	}
	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);
	}

注:
这个BeanDefinitionRegistry 是注册 是一个接口

public interface BeanDefinitionRegistry extends AliasRegistry {
}

他其实有一个很重要的实现类,也是DefaultListableBeanFactory ,在扫描中起作用的其实也是DefaultListableBeanFactory ,但是这里是只需要注册,所以,还是用接口比较准确

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
		implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
		
		}

最后一个的registerDefaultFilters:

	protected void registerDefaultFilters() {

		// 注册@Component对应的AnnotationTypeFilter
		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.
		}
	}

04.扫描scan:我也不知道怎么就到了这个地方

	public int scan(String... basePackages) {
	    //在容器中计算BeanDefinition的数量
		int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
        //真正扫描的函数
		doScan(basePackages);

		// Register annotation config processors, if necessary.
		if (this.includeAnnotationConfig) {
			AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
		}
        //得到本次的扫描的BeanDefinition的数量
		return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
	}

05.scan中的doScan方法(重点)

//参数传入一组包名
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { 
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		
		//创建一个BeanDefinitionHolder的集合
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); 

        //循环这组包名
		for (String basePackage : basePackages) {
           //扫描之后得到一个包的全部BeanDefinition
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            //循环这个集合
			for (BeanDefinition candidate : candidates) {
			    //获取BeanDefinition 中的Scope注解的value值
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
                //获取BeanDefinition 所代表的bean 的名字
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

				if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition) {
					// 解析@Lazy、@Primary、@DependsOn、@Role、@Description
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}

				// 检查Spring容器中是否已经存在该beanName
				if (checkCandidate(beanName, candidate)) {
				//用BeanDefinitionHolder 来包装 已经通过检查的BeanDefinition
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					//添加到BeanDefinitionHolder集合中
					beanDefinitions.add(definitionHolder);

					// 注册
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}

06.findCandidateComponents方法: 扫描之后得到一个包的全部BeanDefinition

	public Set<BeanDefinition> findCandidateComponents(String basePackage) {
	    //这个和简便扫描的索引文件有关,在MENF-INF的文件中的spring.components
		if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
			return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
		}
		else {
		//一般都是这里的函数
			return scanCandidateComponents(basePackage);
		}
	}

07.scanCandidateComponents方法(重点重点重点重点重点):

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			// 获取basePackage下所有的文件资源  
			//ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX  是 classpath*:
			//在Spring框架中,可以使用org.springframework.core.io.support.ResourcePatternResolver接口的resolveBasePackage方法来将指定的基础包解析为用于包搜索路径的模式规范。
           //例如,如果基础包是com.example.app,则可以使用resolveBasePackage方法将其解析为类路径下的资源路径,如classpath:com/example/app/。
           //this.resourcePattern 是  **/*.class
           //classpath*:com.xxx.xxx//*.class
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + '/' + this.resourcePattern;
		  //得到所有的class 的file对象
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				if (resource.isReadable()) {
					try {
					//可读的话,读入元数据
						MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
						// excludeFilters、includeFilters判断
						if (isCandidateComponent(metadataReader)) { // @Component-->includeFilters判断
							//构造一个BeanDefinition
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							//
							sbd.setSource(resource);
							//判断是不是抽象类,接口
							if (isCandidateComponent(sbd)) {
								if (debugEnabled) {
									logger.debug("Identified candidate component class: " + resource);
								}
								candidates.add(sbd);
							}
							else {
								if (debugEnabled) {
									logger.debug("Ignored because not a concrete top-level class: " + resource);
								}
							}
						}
						else {
							if (traceEnabled) {
								logger.trace("Ignored because not matching any filter: " + resource);
							}
						}
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
				else {
					if (traceEnabled) {
						logger.trace("Ignored because not readable: " + resource);
					}
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}


注释:

ResourcePatternResolver接口:默认实现类是PathMatchingResourcePatternResolver类。

public interface ResourcePatternResolver extends ResourceLoader {

	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

	Resource[] getResources(String locationPattern) throws IOException;

}

Resource接口:Resource一般包括这些实现类:UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResource、ByteArrayResource

public interface Resource extends InputStreamSource {
 
  boolean exists();
  
  default boolean isReadable() {
		return true;
	}
  
  default boolean isOpen() {//返回一个布尔值,指示此资源是否具有开放流的句柄
		return false;
	}
  
  default boolean isFile() {
		return false;
	}
  
  URL getURL() throws IOException;//返回一个URL句柄,如果资源不能够被解析为URL,将抛出IOException
  
  URI getURI() throws IOException;
  
  File getFile() throws IOException;//返回某个文件,如果资源不能够被解析称为绝对路径,将会抛出FileNotFoundException
  
  default ReadableByteChannel readableChannel() throws IOException {
		return Channels.newChannel(getInputStream());
	}
  
  long contentLength() throws IOException;
  
  long lastModified() throws IOException;
  
  Resource createRelative(String relativePath) throws IOException;
  
	String getFilename();
  
  String getDescription();
}

这里的语句的结果:resources 数组是一个个的class 的file对象 具体的类是FileSystemResource

Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

在这里插入图片描述

08.isCandidateComponent方法:
通过传入的元数据,判断是不是这个类和容器中的已经扫描存在的excludeFilter是不是匹配,有的话,排除,不是一个bean

在@Component注解中,如果没有显示声明includeFilter,那么就会这个类的Component的includeFilter是被修饰类名

	protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return false;
			}
		}

		// 符合includeFilters的会进行条件匹配,通过了才是Bean,也就是先看有没有@Component,再看是否符合@Conditional
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}

09.isConditionMatch 进一步 查看是不是有@Conditional注解

	private boolean isConditionMatch(MetadataReader metadataReader) {
		if (this.conditionEvaluator == null) {
			this.conditionEvaluator =
					new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
		}
		return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
	}

10.shouldSkip方法:表示是不是要跳过,如果有@Conditional ,不要跳过

	public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
		return shouldSkip(metadata, null);
	}

shouldSkip

	public boolean  shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
	//判断有无Conditional  没有就跳过
		if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
			return false;
		}
//有的话,这块是编译原理的phase  
		if (phase == null) {
			if (metadata instanceof AnnotationMetadata &&
					ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
				return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
			}
			return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
		}
  }
//后面还有,但不是重点,会去执行那个实现接口的类,看一下条件是什么,成立的话,@Conditional修饰的类就会是一个bean

注释:@Conditional,参数是一个类
怎么使用@Conditional注解了?
01.先创建一个实现类,实现Condition接口:
这里写的是条件

public class User implements Condition {
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		try {
			context.getClassLoader().loadClass("com.zhouyu.service.OrderService");
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
		return false;
	}
}

02.使用@Conditional注解

@Component
@Conditional(User.class)
public class UserService  {



}

11.回到scanCandidateComponents方法:

第7到10步,此时已经扫描完成了包,完成了判断通过,符合构造一个BeanDefinition 的条件,不匹配容器中excludeFilter,在includeFilter上有符合的,也完成了关于@Conditional判断

构造一个BeanDefinition :
ScannedGenericBeanDefinition 方法:

	public ScannedGenericBeanDefinition(MetadataReader metadataReader) {
		Assert.notNull(metadataReader, "MetadataReader must not be null");

        //获取元数据
		this.metadata = metadataReader.getAnnotationMetadata();
		// 这里只是把className设置到BeanDefinition中
		
		setBeanClassName(this.metadata.getClassName());
		setResource(metadataReader.getResource());
	}
@Override
	public void setBeanClassName(@Nullable String beanClassName) {
		this.beanClass = beanClassName;
	}
@Override
	public void setResource(@Nullable Resource resource) {
		this.resource = resource;
	}

12.创建完成了,要判断isCandidateComponent方法,判断01.这个类是不是内部类,因为内部类也有class文件,内部类不能成为bean,02.是不是接口或者抽象类 ,这些都不能成为bean ,03.是抽象类,但是类中有一个注解@Lookup,可以成为bean

	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
		AnnotationMetadata metadata = beanDefinition.getMetadata();
		return (metadata.isIndependent() && (metadata.isConcrete() ||
				(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
	}

成功的话,把这个BeanDefinition加入数组中,结束了这个scanCandidateComponents方法

if (isCandidateComponent(sbd)) {
								if (debugEnabled) {
									logger.debug("Identified candidate component class: " + resource);
								}
								candidates.add(sbd);
							}

13.回到了doscan方法,这里获取的是Scope注解的值和beanname的值

for (BeanDefinition candidate : candidates) {
				//获取Scope注解的值
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
//获取beanname的值,在注解@Component中
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

14.其中的this.beanNameGenerator:

	private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;

AnnotationBeanNameGenerator类:

	public static final AnnotationBeanNameGenerator INSTANCE = new AnnotationBeanNameGenerator();

用到的方法:
AnnotationBeanNameGenerator的generateBeanName方法:

	public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
	// 获取注解@Component的注解所指定的beanName
		if (definition instanceof AnnotatedBeanDefinition) {
			
			String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
			if (StringUtils.hasText(beanName)) {
				// 返回一个
				return beanName;
			}
		}
		// 返回一个默认的名字
		return buildDefaultBeanName(definition, registry);
	}

determineBeanNameFromAnnotation方法:返回注解中的指定的名字,如果没有返回一个null

@Nullable
	protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
	    //获得元数据
		AnnotationMetadata amd = annotatedDef.getMetadata();
		//获取元数据中的注解信息
		Set<String> types = amd.getAnnotationTypes();
		String beanName = null;
		for (String type : types) {
			AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);
			if (attributes != null) {
				Set<String> metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> {
					Set<String> result = amd.getMetaAnnotationTypes(key);
					return (result.isEmpty() ? Collections.emptySet() : result);
				});
				if (isStereotypeWithNameValue(type, metaTypes, attributes)) {
				//获取value值
					Object value = attributes.get("value");
					if (value instanceof String) {
						String strVal = (String) value;
						if (StringUtils.hasLength(strVal)) {
							if (beanName != null && !strVal.equals(beanName)) {
								throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
										"component names: '" + beanName + "' versus '" + strVal + "'");
							}
							beanName = strVal;
						}
					}
				}
			}
		}
		return beanName;
	}

isStereotypeWithNameValue方法:
对注解@Component判断是不是有指定的value,有的话返回value


用到的常量
	private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component";


	protected boolean isStereotypeWithNameValue(String annotationType,
			Set<String> metaAnnotationTypes, @Nullable Map<String, Object> attributes) {

		boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) ||
				metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) ||
				annotationType.equals("javax.annotation.ManagedBean") ||
				annotationType.equals("javax.inject.Named");

		return (isStereotype && attributes != null && attributes.containsKey("value"));
	}

15.回到generateBeanName方法:
buildDefaultBeanName:如果没有指定value,那么就生成一个默认的名字

	protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
		return buildDefaultBeanName(definition);
	}
	protected String buildDefaultBeanName(BeanDefinition definition) {
		String beanClassName = definition.getBeanClassName();
		Assert.state(beanClassName != null, "No bean class name set");
		String shortClassName = ClassUtils.getShortName(beanClassName);
		return Introspector.decapitalize(shortClassName);
	}

这句话回生成一个默认的名字:

Introspector.decapitalize(shortClassName);
  public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                        Character.isUpperCase(name.charAt(0))){
            return name;
        }
        char[] chars = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }

16.再回到doscan方法:
这一步设置一些默认的值

if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
		// 设置BeanDefinition的默认值
		beanDefinition.applyDefaults(this.beanDefinitionDefaults);

		// AutowireCandidate表示某个Bean能否被用来做依赖注入
		if (this.autowireCandidatePatterns != null) {
			beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName));
		}
	}

具体设置那些

public void applyDefaults(BeanDefinitionDefaults defaults) {
		Boolean lazyInit = defaults.getLazyInit();
		if (lazyInit != null) {
			setLazyInit(lazyInit);
		}
		setAutowireMode(defaults.getAutowireMode());
		setDependencyCheck(defaults.getDependencyCheck());
		setInitMethodName(defaults.getInitMethodName());
		setEnforceInitMethod(false);
		setDestroyMethodName(defaults.getDestroyMethodName());
		setEnforceDestroyMethod(false);
	}

17.一些其他的注解

if (candidate instanceof AnnotatedBeanDefinition) {
					// 解析@Lazy、@Primary、@DependsOn、@Role、@Description
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}

	public static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) {
		processCommonDefinitionAnnotations(abd, abd.getMetadata());
	}
	static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
		AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
		if (lazy != null) {
			abd.setLazyInit(lazy.getBoolean("value"));
		}
		else if (abd.getMetadata() != metadata) {
			lazy = attributesFor(abd.getMetadata(), Lazy.class);
			if (lazy != null) {
				abd.setLazyInit(lazy.getBoolean("value"));
			}
		}

		if (metadata.isAnnotated(Primary.class.getName())) {
			abd.setPrimary(true);
		}
		AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
		if (dependsOn != null) {
			abd.setDependsOn(dependsOn.getStringArray("value"));
		}

		AnnotationAttributes role = attributesFor(metadata, Role.class);
		if (role != null) {
			abd.setRole(role.getNumber("value").intValue());
		}
		AnnotationAttributes description = attributesFor(metadata, Description.class);
		if (description != null) {
			abd.setDescription(description.getString("value"));
		}
	}

18.再回到doscan方法:
最后一步,判断是不是已经存在bean

// 检查Spring容器中是否已经存在该beanName
				if (checkCandidate(beanName, candidate)) {
				//没有的话
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);

					// 注册
					registerBeanDefinition(definitionHolder, this.registry);
				}
	protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
		if (!this.registry.containsBeanDefinition(beanName)) {
		//如果没有的话,就直接返回ture,容器中没有相同的beanName
			return true;
		}
		BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
		BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
		if (originatingDef != null) {
			existingDef = originatingDef;
		}
		// 是否兼容,如果兼容返回false表示不会重新注册到Spring容器中,如果不冲突则会抛异常。
		if (isCompatible(beanDefinition, existingDef)) {
			return false;
		}
		throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
				"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
				"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
	}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值