spring源码------@Configuration跟@Component及其派生注解@Service等的区别以及spring对其代理增强的原理

1.常用的注解,以及@Configuration的特殊性

 Spring中提供了很多将对象注入到容器中的注解,这里将这些注解列举出来。@Component@Indexed@Controller@Repository@Service@Configuration。另外还有一个需要跟@Configuration@Component以及派生注解一起使用的注解@Bean
 虽然我们经常使用这些注解中的,但是其实@Configuration跟其他的注解有点不一样,这个注解在默认情况下Spring会通过CGLIB的方式对贴有这个注解的对象进行增强。而这个决定是否进行增强的关键,在于@Configuration注解中的proxyBeanMethods属性。当这个属性为true的时候会进行增强,而默认情况这个属性值就是true

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
	@AliasFor(annotation = Component.class)
	String value() default "";
	
	boolean proxyBeanMethods() default true;
}

&esmp;从上面其实能看到@Configuration注解本身也是@Component注解的一个派生注解,但是为什么Spring会对其进行增强呢,接下来进行分析。

2. @Configuration特殊性的源码解析
2.1 贴有@Configuration的bean的获取

 spring对于@Configuration注解的解析,跟@Conditional注解的解析大部分过程一样都是在ConfigurationClassPostProcessor中进行的,可以通过@Conditional注解解析的文章看看前面的过程。spring源码------@Conditional注解的解析Condition接口,以及springboot中的扩展。这里天过前面的步骤,直接进入到此类中进行分析。

2.1.1 bean注册前进行分类的方法checkConfigurationClassCandidate

 在这个类中有一个方法checkConfigurationClassCandidate用来检查当前注册的类是不是候选configuration class也就是需要进行代理的类。这个方法的调用的位置都是在,将对应的候选bean放入到需要注册的候选bean的集合这个步骤前。这个贴代码看看

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	......
	for (String beanName : candidateNames) {
			//获取对应的BeanDefinition
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			//检查BeanDefinition是不是Configuration类型的候选bean,
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}
	......
		//检查BeanDefinition是不是Configuration类型的候选bean,并且这没有被解析过
		if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
								!alreadyParsedClasses.contains(bd.getBeanClassName())) {
							candidates.add(new BeanDefinitionHolder(bd, candidateName));
		}
		......				
}

 可以看到在将候选bean放入到需要注册的候选bean集合之前都会对bean进行的检查,这也可以看成是一个过滤 的过程,将那些需要进行代理的bean进行分类。分类的过程就在checkConfigurationClassCandidate方法中。这个方法在ConfigurationClassUtils类中,现在看看这个方法的主要步骤

	public static boolean checkConfigurationClassCandidate(
			BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
		......
		//获取源数据中包含Configuration注解的信息
		Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
		//如果存在Configuration注解,并且proxyBeanMethods是true,则表示需要进行代理
		if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
			beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
		}
		//如果存在Configuration注解,但是proxyBeanMethods是false,则表示不需要进行代理
		//如果不存在Configuration注解,但是如果bean中包含Component,ComponentScan,Import,ImportResource或者Bean注解,则也不需要代理,但是是候选bean
		else if (config != null || isConfigurationCandidate(metadata)) {
			beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
		}
		//如果满足上面判断条件则表示不是候选bean
		else {
			return false;
		}
		......
	}

 这里做的就是获取传进来的候选bean上面的@Configuration注解,然后进行判断,主要有以下几种判断。

贴有@Configuration注解@Configuration注解的proxyBeanMethodstrue是否需要代理
满足满足需要
满足不满足不需要
不满足-不需要

 上面满足条件需要代理的bean,会在封装其信息的BeanDefinition对象中加上一个属性CONFIGURATION_CLASS_ATTRIBUTE值为CONFIGURATION_CLASS_FULL。这里需要注意这个属性有两种值,需要进行区分。

说明情况
CONFIGURATION_CLASS_FULL需要对bean进行代理贴有@Configuration注解的对象的内部所有的注入到容器的对象(贴有@Bean注解的对象)
CONFIGURATION_CLASS_LITE不需要对bean进行代理没有贴有@Configuration注解的对象,即使内部存在注入到容器的对象

 在上面将bean进行分类之后,后面就是对bean进行代理增强的阶段了。对bean进行代理增强的过程的起始调用也是在ConfigurationClassPostProcessor类中。

2.2 对bean进行增强
2.2.1 获取需要代理增强的bean

 增强的初始过程在ConfigurationClassPostProcessor类的enhanceConfigurationClasses方法中。这个方法的调用在spring上下文初始化之后会调用。

	public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
		Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
		for (String beanName : beanFactory.getBeanDefinitionNames()) {
			//从容器中获取BeanDefinition
			BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
			//获取BeanDefinition中的CONFIGURATION_CLASS_ATTRIBUTE属性
			Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE);
			MethodMetadata methodMetadata = null;
			if (beanDef instanceof AnnotatedBeanDefinition) {
				methodMetadata = ((AnnotatedBeanDefinition) beanDef).getFactoryMethodMetadata();
			}
			//从加载了@Configuration注解的BeanDefinition的beanClassLoader中获取对应的beanClass,因为是这个类加载的@Configuration注解的BeanDefinition
			if ((configClassAttr != null || methodMetadata != null) && beanDef instanceof AbstractBeanDefinition) {
				// Configuration class (full or lite) or a configuration-derived @Bean method
				// -> resolve bean class at this point...
				AbstractBeanDefinition abd = (AbstractBeanDefinition) beanDef;
				if (!abd.hasBeanClass()) {
					try {
						//从beanClassLoader中获取到真正加载的bean的Classes对象
						abd.resolveBeanClass(this.beanClassLoader);
					}
					catch (Throwable ex) {
						throw new IllegalStateException(
								"Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
					}
				}
			}
			//如果CONFIGURATION_CLASS_ATTRIBUTE属性是CONFIGURATION_CLASS_FULL
			if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
				if (!(beanDef instanceof AbstractBeanDefinition)) {
					throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
							beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
				}
				//如果是AbstractBeanDefinition类型的BeanDefinition需要检查这个bean是不是已经被实例化了,如果是的则不能进行代理
				else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
					logger.info("Cannot enhance @Configuration bean definition '" + beanName +
							"' since its singleton instance has been created too early. The typical cause " +
							"is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
							"return type: Consider declaring such methods as 'static'.");
				}
				//放到需要进行代理的对象中
				configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
			}
		}
		if (configBeanDefs.isEmpty()) {
			// nothing to enhance -> return immediately
			return;
		}
		ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
		for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
			//获取AbstractBeanDefinition
			AbstractBeanDefinition beanDef = entry.getValue();
			// If a @Configuration class gets proxied, always proxy the target class
			//如果一个bean已经代理过了,则设置这个状态未true
			beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
			// Set enhanced subclass of the user-specified bean class
			//进行代理
			Class<?> configClass = beanDef.getBeanClass();
			Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
			if (configClass != enhancedClass) {
				if (logger.isTraceEnabled()) {
					logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " +
							"enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
				}
				beanDef.setBeanClass(enhancedClass);
			}
		}
	}

 总结以下上面的步骤:

  1. 从当前的容器中获取所有的需要注册的bean集合,然后循环开始2-3步骤,直到结束
  2. 获取BeanDefinition中的CONFIGURATION_CLASS_ATTRIBUTE属性,然后获取需要代理的bean的真实的Class对象
  3. 检查CONFIGURATION_CLASS_ATTRIBUTE属性是不是CONFIGURATION_CLASS_FULL,不是则跳过进入下一个bean然后到步骤2。是的则将bean放到需要代理的bean的集合中,然后进入2步骤
  4. 迭代需要代理的bean的集合进行代理增强。
2.2.2 对bean进行代理增强

&esmp;代理增强的逻辑在ConfigurationClassEnhancer类的enhance方法中。

	public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
		//如果当前的configClass已经是EnhancedConfiguration的子类,说明这个类已经被代理了,因为后面代理的类会设置接口是EnhancedConfiguration
		if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("Ignoring request to enhance %s as it has " +
						"already been enhanced. This usually indicates that more than one " +
						"ConfigurationClassPostProcessor has been registered (e.g. via " +
						"<context:annotation-config>). This is harmless, but you may " +
						"want check your configuration and remove one CCPP if possible",
						configClass.getName()));
			}
			return configClass;
		}
		//创建一个Enhancer然后进行代理
		Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
		if (logger.isTraceEnabled()) {
			logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
					configClass.getName(), enhancedClass.getName()));
		}
		//返回代理类
		return enhancedClass;
	}

 可以看到上面的就是用CGILB进行代理的,在其创建Enhancer的时候,会设置一些参数,这些参数就是用来与普通的CGLIB进行区分的。

	private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
		Enhancer enhancer = new Enhancer();
		//设置父类或者接口类为需要代理的目标类
		enhancer.setSuperclass(configSuperClass);
		//设置实现的接口EnhancedConfiguration
		enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
		//是否需要实现Factory接口
		enhancer.setUseFactory(false);
		//设置命名规则为了避免Spring的CGLIB版本与常规的CGLIB之间的冲突
		enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
		//设置生成策略
		enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
		//设置回调
		enhancer.setCallbackFilter(CALLBACK_FILTER);
		//设置回调类型
		enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
		return enhancer;
	}

 在这里需要关注一下setCallbackFilter这个方法设置的CALLBACK_FILTERCALLBACK_FILTER是一个对象的数组。

	private static final Callback[] CALLBACKS = new Callback[] {
			new BeanMethodInterceptor(),
			new BeanFactoryAwareMethodInterceptor(),
			NoOp.INSTANCE
	};

	private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);

 这里包含四个类BeanMethodInterceptorBeanFactoryAwareMethodInterceptorNoOp的空实现类SerializableNoOpConditionalCallbackFilter,其中第三个NoOp在spring中是空实现不用管。主要看ConditionalCallbackFilterBeanFactoryAwareMethodInterceptorBeanMethodInterceptor这三个类。

2.2.3 方法回调的过滤处理ConditionalCallbackFilter

CallbackFilter在CGLIB 中起到回调的作用,就是一个方法的执行时候的拦截过滤的作用。spring实现了自己的ConditionalCallbackFilter。在这个里面会对方法进行过滤。

	private static class ConditionalCallbackFilter implements CallbackFilter {

		private final Callback[] callbacks;

		private final Class<?>[] callbackTypes;

		public ConditionalCallbackFilter(Callback[] callbacks) {
			//设置Callback,这里就是BeanMethodInterceptor, BeanFactoryAwareMethodInterceptor, NoOp的空实现类SerializableNoOp这三个类
			this.callbacks = callbacks;
			this.callbackTypes = new Class<?>[callbacks.length];
			for (int i = 0; i < callbacks.length; i++) {
				this.callbackTypes[i] = callbacks[i].getClass();
			}
		}

		@Override
		public int accept(Method method) {
			//进行循环处理
			for (int i = 0; i < this.callbacks.length; i++) {
				Callback callback = this.callbacks[i];
				//如果是ConditionalCallback的实现类,并且isMatch方法通过则返回对应的callback所在的顺序
				if (!(callback instanceof ConditionalCallback) || ((ConditionalCallback) callback).isMatch(method)) {
					return i;
				}
			}
			throw new IllegalStateException("No callback available for method " + method.getName());
		}

 可以看到这里最主要的作用就是设置自己的Callback,也就是BeanMethodInterceptorBeanFactoryAwareMethodInterceptorNoOp。然后进行过滤这些Callback,其中ConditionalCallback也是ConfigurationClassEnhancer类的内部类。只定义了一个方法isMatch,作用是检查对应的拦截方法是否需要进行拦截。

	private interface ConditionalCallback extends Callback {

		boolean isMatch(Method candidateMethod);
	}

 其中isMatch方法由BeanFactoryAwareMethodInterceptorBeanMethodInterceptor实现。接下来就是这两个类

2.2.4 将容器设置到代理对象中的BeanFactoryAwareGeneratorStrategy

BeanFactoryAwareGeneratorStrategy实现了CGLIB的DefaultGeneratorStrategy用来在生成代理对象的时候,设置额外的信息到代理对象中。也是在这里将,容器对象BeanFactory放到了代理对象中,这里只截取部分的代码。

	private static class BeanFactoryAwareGeneratorStrategy extends DefaultGeneratorStrategy {
		......
		@Override
		protected ClassGenerator transform(ClassGenerator cg) throws Exception {
			//实现ClassEmitterTransformer,为代理对象增加额外的字段BEAN_FACTORY_FIELD,访问级别是PUBLIC,类型为BeanFactory
			ClassEmitterTransformer transformer = new ClassEmitterTransformer() {
				@Override
				public void end_class() {
					declare_field(Constants.ACC_PUBLIC, BEAN_FACTORY_FIELD, Type.getType(BeanFactory.class), null);
					super.end_class();
				}
			};
			return new TransformingClassGenerator(cg, transformer);
		}
		......
	}

 设置的这个值,在后面的两个拦截其中都会用到

2.2.5 增加容器到代理对象中的BeanFactoryAwareMethodInterceptor

BeanFactoryAwareMethodInterceptor主要用来处理实现了BeanFactoryAware类的setBeanFactory方法类。

	private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor, ConditionalCallback {

		@Override
		@Nullable
		public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
			//从代理的对象中获取BEAN_FACTORY_FIELD字段
			Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
			Assert.state(field != null, "Unable to find generated BeanFactory field");
			//将BEAN_FACTORY_FIELD字段设置到obj对象中
			field.set(obj, args[0]);

			//如果当前代理类的父类实现了BeanFactoryAware,则调用setBeanFactory方法,如果不是就推出
			if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
				return proxy.invokeSuper(obj, args);
			}
			return null;
		}

		@Override
		public boolean isMatch(Method candidateMethod) {
			//检查是否是设置BeanFactory的方法
			return isSetBeanFactory(candidateMethod);
		}

		public static boolean isSetBeanFactory(Method candidateMethod) {
			//方法名是setBeanFactory,参数个数为1个,平且参数类型是BeanFactory类型,方法是BeanFactoryAware的实现类
			return (candidateMethod.getName().equals("setBeanFactory") &&
					candidateMethod.getParameterCount() == 1 &&
					BeanFactory.class == candidateMethod.getParameterTypes()[0] &&
					BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass()));
		}
	}
2.2.6 处理@Configuration注解中@Bean注解的BeanMethodInterceptor

BeanMethodInterceptor主要处理@Configuration注解中贴有@Bean注解的方法。也正是这个方法,让@Configuration有了区别于@Component注解中贴有@Bean注解的方法的原因之一。主要逻辑集中在这个类的
intercept方法中,现在来看看:
 这里要先看看实现了ConditionalCallback接口的isMatch方法,因为这个会先被调用

		public boolean isMatch(Method candidateMethod) {
			//定义当前方法的对象不是Object,并且不是方法不是setBeanFactory方法,并且方法上面包含@Bean注解
			return (candidateMethod.getDeclaringClass() != Object.class &&
					!BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&
					BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
		}

 到这里就知道,只要是声明方法的类Object类,方法名不是setBeanFactory贴了@Bean注解就会被拦截。
 接下来就是拦截之后的处理逻辑了。

		public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
					MethodProxy cglibMethodProxy) throws Throwable {
			//从代理的实例中获取beanFactory,根据BEAN_FACTORY_FIELD字段获取,而这个字段在enhanced对象创建设置stratege的BeanFactoryAwareGeneratorStrategy类中设置的
			ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
			//获取到方法上@Bean注解的name属性或者方法名来决定bean的名称
			String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
			// Determine whether this bean is a scoped-proxy
			//检查缓存中是否有方法的@Scope注解信息缓存,如果没有则检查是否有@Scope注解并且proxyMode不是NO
			if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
				//生成作用域代理内用于引用目标bean的bean名称
				String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
				if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
					beanName = scopedBeanName;
				}
			}

			/**
			 * 为了处理bean间方法引用的情况,我们必须显式地检查容器中已经缓存的实例。
			 * 检查当前的bean是不是一个FactoryBean,如果是这样创建一个子类roxy,拦截对getObject()的调用并返回所有缓存的bean实例
			 * 这确保了从@Bean方法中调用FactoryBean的语义与在XML中引用FactoryBean的语义相同
			 */
			if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
					factoryContainsBean(beanFactory, beanName)) {
				Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
				if (factoryBean instanceof ScopedProxyFactoryBean) {
					//作用域代理工厂bean是一种特殊情况,不应该进一步代理
					// Scoped proxy factory beans are a special case and should not be further proxied
				}
				else {
					// It is a candidate FactoryBean - go ahead with enhancement
					//进行增强
					return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
				}
			}
			//检查当前的方法是不是当前调用的工厂方法,就是当前拦截的方法是不是当前贴有@Configuration注解类里面贴有@Bean注解的方法
			if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
				// The factory is calling the bean method in order to instantiate and register the bean  (i.e. via a getBean() call) -> invoke the super implementation of the method to actually create the bean instance.
				//工厂调用bean方法是为了实例化和注册bean(即通过getBean()调用)->调用父类方法来实际创建bean实例。
				if (logger.isInfoEnabled() &&
						BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
					logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
									"assignable to Spring's BeanFactoryPostProcessor interface. This will " +
									"result in a failure to process annotations such as @Autowired, " +
									"@Resource and @PostConstruct within the method's declaring " +
									"@Configuration class. Add the 'static' modifier to this method to avoid " +
									"these container lifecycle issues; see @Bean javadoc for complete details.",
							beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
				}
				//调用父类方法
				return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
			}
			//从容器获取bean
			return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
		}

 这里需要说明的以下几点

  1. BeanMethodInterceptorConfigurationClassEnhancer的一个内部类
    enhancedConfigInstance对象中的
  2. 当贴有@Bean注解的方法返回的是BeanFactory的子类的时候,会先获取到实际的bean,也就是beanName前面加上“&”符号,然后进行代理
  3. isCurrentlyInvokedFactoryMethod用来验证当前拦截的方法是不是贴有@Bean注解的方法,是的话会调用代理的方法。如果不是则直接从容器中获取bean。
3.@Configuration@Component例证

如果在@Configuration注解的类的中有@Bean注解在方法上来注入对象,当调用这个方法的时候,返回的是容器里面已经创建好的对象。举个例子

@Configuration
public class ConfigurationTest {
    @Bean
    public BeanB beanB(){
        BeanB beanB = new BeanB();
        System.out.println("beanB one---"+beanB);
        return beanB;
    }

    @Bean
    @DependsOn(value = "beanB")
    public BeanA beanA(){
        System.out.println("beanA");
        BeanA beanA = new BeanA();
        beanA.setBeanB(beanB());
        System.out.println("beanB two---"+beanA.getBeanB());
        return beanA;
    }
}

 当从容器中获取beanA的时候。打印的结果如下

beanB one---springstudy.configuration.BeanB@2611b9a3
beanA
beanB two---springstudy.configuration.BeanB@2611b9a3

 当我们把ConfigurationTest类上面的@Configuration注解换成@Component的时候,看结果如下

beanB one---springstudy.configuration.BeanB@6d24ffa1
beanA
beanB one---springstudy.configuration.BeanB@65a4798f
beanB two---springstudy.configuration.BeanB@65a4798f

 发现会创建两次BeanB对象。
 会发生这种区别的原因就是,当时存在@Configuration注解的时候,beanBbeanA方法会被代理增强,在beanA方法中调用beanB方法的时候会被拦截,并返回容器中已经创建的BeanB对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值