spring-tx源码分析(4)_事务通知代理的创建

本文讨论一下spring事务通知的初始化过程。

AutoProxyRegistrar

我们回到TransactionManagementConfigurationSelector类,这个类除了导入了ProxyTransactionManagementConfiguration外,还导入了AutoProxyRegistrar类,这个类实现了ImportBeanDefinitionRegistrar接口,在registerBeanDefinitions方法中使用AopConfigUtils.registerAutoProxyCreatorIfNecessary方法向spring容器注入了一个InfrastructureAdvisorAutoProxyCreator类。

public void registerBeanDefinitions(
    AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

	boolean candidateFound = false;
	Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
	for (String annType : annTypes) {
		AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
		if (candidate == null) {
			continue;
		}
		Object mode = candidate.get("mode");
		Object proxyTargetClass = candidate.get("proxyTargetClass");
		if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
				Boolean.class == proxyTargetClass.getClass()) {
			candidateFound = true;
			if (mode == AdviceMode.PROXY) {
                // 在这里注入了InfrastructureAdvisorAutoProxyCreator
				AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                // proxyTargetClass参数默认是false所以这个分支我们暂时不展开分析
				if ((Boolean) proxyTargetClass) {
					AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
					return;
				}
			}
		}
	}
}

// AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
	return registerAutoProxyCreatorIfNecessary(registry, null);
}

public static BeanDefinition registerAutoProxyCreatorIfNecessary(
		BeanDefinitionRegistry registry, Object source) {
	return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

我们的故事就从InfrastructureAdvisorAutoProxyCreator开始讲起。

InfrastructureAdvisorAutoProxyCreator类

InfrastructureAdvisorAutoProxyCreator类

这个类的继承关系如下:
在这里插入图片描述

这个类实现了BeanPostProcessor接口,几个方法的具体实现在AbstractAutoProxyCreator类中,我们关心的创建代理的逻辑在postProcessAfterInitialization(Object, String)方法中:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
	if (bean != null) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 这里在做代理的包装
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}

wrapIfNecessary方法

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
	if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
		return bean;
	}
	if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
		return bean;
	}
	if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}
    // 上面的三个分支判断先略过

	// Create proxy if we have advice.
	Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
	if (specificInterceptors != DO_NOT_PROXY) {
		this.advisedBeans.put(cacheKey, Boolean.TRUE);
		Object proxy = createProxy(
				bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}

	this.advisedBeans.put(cacheKey, Boolean.FALSE);
	return bean;
}

创建通知代理

这部分单独提出来。

// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
	this.advisedBeans.put(cacheKey, Boolean.TRUE);
	Object proxy = createProxy(
			bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
	this.proxyTypes.put(cacheKey, proxy.getClass());
	return proxy;
}

这段代码大致分成两步:

  1. 获取目标bean的所有可用通知
  2. 基于可用通知为目标bean创建代理

获取目标bean的所有可用通知

获取可以作用于目标bean的所有通知

Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

protected Object[] getAdvicesAndAdvisorsForBean(
		Class<?> beanClass, String beanName, TargetSource targetSource) {

    // 获取可以作用于目标bean的所有的通知
	List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
	if (advisors.isEmpty()) {
		return DO_NOT_PROXY;
	}
	return advisors.toArray();
}

// 获取可以作用于目标bean的所有的通知
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    // 查找可以用于自动代理的所有的通知
	List<Advisor> candidateAdvisors = findCandidateAdvisors();
    // 从所有的候选通知中筛选可以用于目标bean的通知
	List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    // 在PROXY模式下,默认什么都不做,暂时不展开分析
	extendAdvisors(eligibleAdvisors);
	if (!eligibleAdvisors.isEmpty()) {
        // 排个序
		eligibleAdvisors = sortAdvisors(eligibleAdvisors);
	}
	return eligibleAdvisors;
}

// Find all candidate Advisors to use in auto-proxying.
protected List<Advisor> findCandidateAdvisors() {
	return this.advisorRetrievalHelper.findAdvisorBeans();
}

// Search the given candidate Advisors to find all Advisors that
// can apply to the specified bean.
protected List<Advisor> findAdvisorsThatCanApply(
		List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

	ProxyCreationContext.setCurrentProxiedBeanName(beanName);
	try {
		return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
	} finally {
		ProxyCreationContext.setCurrentProxiedBeanName(null);
	}
}

从上一篇文章,我们了解到,spring-tx使用的是BeanFactoryTransactionAttributeSourceAdvisor作为通知实现,所以getAdvicesAndAdvisorsForBean方法获取到的Object[]中至少会包含一个BeanFactoryTransactionAttributeSourceAdvisor对象。

下面我们就看一下是如何获取目标bean的所有可用通知的,分为两个步骤:

  1. 使用findEligibleAdvisors方法查找可以用于自动代理的所有的通知
  2. 之后使用findAdvisorsThatCanApply方法筛选可以用于目标bean的通知

findEligibleAdvisors方法查找可以用于自动代理的所有的通知

protected List<Advisor> findCandidateAdvisors() {
	return this.advisorRetrievalHelper.findAdvisorBeans();
}

// this.advisorRetrievalHelper.findAdvisorBeans()
// 其实就是从spring容器获取所有的Advisor实现bean
public List<Advisor> findAdvisorBeans() {
	// Determine list of advisor bean names, if not cached already.
	String[] advisorNames = this.cachedAdvisorBeanNames;
	if (advisorNames == null) {
		// Do not initialize FactoryBeans here: We need to leave all regular beans
		// uninitialized to let the auto-proxy creator apply to them!
		advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
				this.beanFactory, Advisor.class, true, false);
		this.cachedAdvisorBeanNames = advisorNames;
	}
	if (advisorNames.length == 0) {
		return new ArrayList<>();
	}

	List<Advisor> advisors = new ArrayList<>();
	for (String name : advisorNames) {
		if (isEligibleBean(name)) {
			if (this.beanFactory.isCurrentlyInCreation(name)) {
				// trace log
			} else {
				try {
                    // 从容器获取或创建新的
					advisors.add(this.beanFactory.getBean(name, Advisor.class));
				} catch (BeanCreationException ex) {
					// 一些异常处理
				}
			}
		}
	}
	return advisors;
}

findAdvisorsThatCanApply方法筛选可以用于目标bean的通知

/**
 * Search the given candidate Advisors to find all Advisors that
 * can apply to the specified bean.
 */
protected List<Advisor> findAdvisorsThatCanApply(
		List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

	ProxyCreationContext.setCurrentProxiedBeanName(beanName);
	try {
		return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
	} finally {
		ProxyCreationContext.setCurrentProxiedBeanName(null);
	}
}

AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass)方法:

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
	List<Advisor> eligibleAdvisors = new ArrayList<>();
	for (Advisor candidate : candidateAdvisors) {
        // 我们的事务Advisor是PointcutAdvisor接口的实现,所以这个分支进不来,暂时不做分析
		if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
			eligibleAdvisors.add(candidate);
		}
	}
    // false
	boolean hasIntroductions = !eligibleAdvisors.isEmpty();
	for (Advisor candidate : candidateAdvisors) {
		if (candidate instanceof IntroductionAdvisor) {
			// already processed
			continue;
		}
        // 判断是否可以作用与目标bean
		if (canApply(candidate, clazz, hasIntroductions)) {
			eligibleAdvisors.add(candidate);
		}
	}
	return eligibleAdvisors;
}

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
	if (advisor instanceof IntroductionAdvisor) {
        // 这个分支暂时不分析
		return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
	} else if (advisor instanceof PointcutAdvisor) {
        // 执行这个分支
		PointcutAdvisor pca = (PointcutAdvisor) advisor;
		return canApply(pca.getPointcut(), targetClass, hasIntroductions);
	} else {
		// It doesn't have a pointcut so we assume it applies.
		return true;
	}
}

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
    // 事务通知使用的是StaticMethodMatcherPointcut的子类,使用的是ClassFilter.TRUE过滤类型,所以这个分支通常进不来
	if (!pc.getClassFilter().matches(targetClass)) {
		return false;
	}

    // 这里也进不来
	MethodMatcher methodMatcher = pc.getMethodMatcher();
	if (methodMatcher == MethodMatcher.TRUE) {
		return true;
	}

    // 这里暂时不分析
	IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
	if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
		introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
	}

	Set<Class<?>> classes = new LinkedHashSet<>();
	if (!Proxy.isProxyClass(targetClass)) {
		classes.add(ClassUtils.getUserClass(targetClass));
	}
	classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

	for (Class<?> clazz : classes) {
		Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
		for (Method method : methods) {
			if (introductionAwareMethodMatcher != null ?
					introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
					methodMatcher.matches(method, targetClass)) {
                // 这个分支最终使用的是methodMatcher.matches(method, targetClass)做的判断
                // 而事务通知中使用的是TransactionAttributeSourcePointcut实现
                // 我们下文就介绍一下这个方法的实现
				return true;
			}
		}
	}

	return false;
}

TransactionAttributeSourcePointcut类的matches(method, targetClass)方法实现

public boolean matches(Method method, Class<?> targetClass) {
	TransactionAttributeSource tas = getTransactionAttributeSource();
	return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

这里的getTransactionAttributeSource是一个抽象方法,用于获取TransactionAttributeSource的实例,TransactionAttributeSource用于解析事务配置。

在BeanFactoryTransactionAttributeSourceAdvisor定义了一个内部类,实现了这个方法,就是把外部类持有的TransactionAttributeSource对象返回了:

private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
	@Nullable
	protected TransactionAttributeSource getTransactionAttributeSource() {
		return transactionAttributeSource;
	}
};

从ProxyTransactionManagementConfiguration类的代码可以知道,这个TransactionAttributeSource是AnnotationTransactionAttributeSource类型。

我们再退回去看这行代码:

return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);

调用了TransactionAttributeSource的getTransactionAttribute(method, targetClass)方法来判断是否需要为目标类加代理:

public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {

	// First, see if we have a cached value.
	Object cacheKey = getCacheKey(method, targetClass);
	TransactionAttribute cached = this.attributeCache.get(cacheKey);
	if (cached != null) {
		// Value will either be canonical value indicating there is no transaction attribute,
		// or an actual transaction attribute.
		if (cached == NULL_TRANSACTION_ATTRIBUTE) {
			return null;
		} else {
			return cached;
		}
	} else {
		// We need to work it out.
		TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
		// Put it in the cache.
		if (txAttr == null) {
			this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
		} else {
			String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
			if (txAttr instanceof DefaultTransactionAttribute) {
				((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification);
			}
			this.attributeCache.put(cacheKey, txAttr);
		}
		return txAttr;
	}
}

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
	// Don't allow no-public methods as required.
	if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
		return null;
	}

	// The method may be on an interface, but we need attributes from the target class.
	// If the target class is null, the method will be unchanged.
	Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

	// First try is the method in the target class.
	TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
	if (txAttr != null) {
		return txAttr;
	}

	// Second try is the transaction attribute on the target class.
	txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
	if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
		return txAttr;
	}

	if (specificMethod != method) {
		// Fallback is to look at the original method.
		txAttr = findTransactionAttribute(method);
		if (txAttr != null) {
			return txAttr;
		}
		// Last fallback is the class of the original method.
		txAttr = findTransactionAttribute(method.getDeclaringClass());
		if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
			return txAttr;
		}
	}

	return null;
}

基于可用通知为目标bean创建代理

// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

// 基于可用通知为目标bean创建代理
if (specificInterceptors != DO_NOT_PROXY) {
	this.advisedBeans.put(cacheKey, Boolean.TRUE);
	Object proxy = createProxy(
			bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
	this.proxyTypes.put(cacheKey, proxy.getClass());
	return proxy;
}

createProxy创建代理

Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

createProxy方法:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
		@Nullable Object[] specificInterceptors, TargetSource targetSource) {

    // Expose the given target class for the specified bean, if possible.
	if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
		AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
	}

	ProxyFactory proxyFactory = new ProxyFactory();
	proxyFactory.copyFrom(this);

    // 这里是区分一下是为目标类型创建代理或者是为目标类型所属接口创建代理
    // 决定了后续是使用JDK Proxy还是CGLib技术
	if (!proxyFactory.isProxyTargetClass()) {
		if (shouldProxyTargetClass(beanClass, beanName)) {
			proxyFactory.setProxyTargetClass(true);
		} else {
			evaluateProxyInterfaces(beanClass, proxyFactory);
		}
	}

	Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
	proxyFactory.addAdvisors(advisors);
	proxyFactory.setTargetSource(targetSource);
	customizeProxyFactory(proxyFactory);

	proxyFactory.setFrozen(this.freezeProxy);
	if (advisorsPreFiltered()) {
		proxyFactory.setPreFiltered(true);
	}

    // 创建代理
	return proxyFactory.getProxy(getProxyClassLoader());
}

proxyFactory.getProxy(ClassLoader)创建代理

public Object getProxy(ClassLoader classLoader) {
    // 使用AopProxy创建代理
    // ObjenesisCglibAopProxy or JdkDynamicAopProxy
	return createAopProxy().getProxy(classLoader);
}

// 这里创建AopProxy
protected final synchronized AopProxy createAopProxy() {
	if (!this.active) {
		activate();
	}
	return getAopProxyFactory().createAopProxy(this);
}

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
	if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
		Class<?> targetClass = config.getTargetClass();
		if (targetClass == null) {
			throw new AopConfigException("");
		}
		if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
			return new JdkDynamicAopProxy(config);
		}
		return new ObjenesisCglibAopProxy(config);
	} else {
        // 我们的bean通常都是实现了某个接口的,所以执行这个分支,暂时分析JdkDynamicAopProxy创建代理的过程
        // 这里传递的config参数就是proxyFactory对象,其中封装着目标类型、代理通知等重要信息
		return new JdkDynamicAopProxy(config);
	}
}

cglib的代理创建我们暂时不展开分析,下面详细分析一下JdkDynamicAopProxy创建代理的过程。

JdkDynamicAopProxy.getProxy(ClassLoader)创建代理

public Object getProxy(ClassLoader classLoader) {
    // this.advised = proxyFactory对象
    // 在这里获取到目标类实现的所有接口
	Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
	findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    // 使用JDK的Proxy创建代理对象
	return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

使用JDK的Proxy创建代理时使用的InvocationHandler实现就是JdkDynamicAopProxy对象,所以可以知道所有的代理逻辑都在JdkDynamicAopProxy.invoke方法中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	Object oldProxy = null;
	boolean setProxyContext = false;

	TargetSource targetSource = this.advised.targetSource;
	Object target = null;

	try {
		// ...
        // 此处的几个分支判断代码不需要展开分析

		Object retVal;

		// 缓存old aop上下文

		// Get as late as possible to minimize the time we "own" the target,
		// in case it comes from a pool.
		target = targetSource.getTarget();
		Class<?> targetClass = (target != null ? target.getClass() : null);

		// Get the interception chain for this method.
        // 获取目标方法的拦截器链
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

		// Check whether we have any advice. If we don't, we can fallback on direct
		// reflective invocation of the target, and avoid creating a MethodInvocation.
		if (chain.isEmpty()) {
			Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
			retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
		} else {
			// We need to create a method invocation...
			MethodInvocation invocation =
					new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // 调用拦截器和目标方法
			retVal = invocation.proceed();
		}

		// Massage return value if necessary.
		Class<?> returnType = method.getReturnType();
		if (retVal != null && retVal == target &&
				returnType != Object.class && returnType.isInstance(proxy) &&
				!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
			retVal = proxy;
		} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
			throw new AopInvocationException("");
		}
		return retVal;
	} finally {
		// 一些aop上下文的恢复和清理工作
	}
}

核心的逻辑就是这两行:

// We need to create a method invocation...
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 调用拦截器和目标方法
retVal = invocation.proceed();

MethodInvocation的proceed方法:

public Object proceed() throws Throwable {
	// We start with an index of -1 and increment early.
    // 拦截器调用完成之后,调用目标方法
	if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
		return invokeJoinpoint();
	}

    // 获取拦截器
	Object interceptorOrInterceptionAdvice =
			this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

    // 我们的通知是MethodInterceptor的实现,所以这个if分支不会执行
	if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
		// Evaluate dynamic method matcher here: static part will already have
		// been evaluated and found to match.
		InterceptorAndDynamicMethodMatcher dm =
				(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
		Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
		if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
			return dm.interceptor.invoke(this);
		} else {
			// Dynamic matching failed.
			// Skip this interceptor and invoke the next in the chain.
			return proceed();
		}
	} else {
        // 调用拦截器的invoke方法
		// It's an interceptor, so we just invoke it: The pointcut will have
		// been evaluated statically before this object was constructed.
		return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
	}
}

protected Object invokeJoinpoint() throws Throwable {
	return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}

阶段性总结

至此,mybatis和spring-tx的基础版源码分析就结束了,在这部分源码分析中,我们了解到:

  • mybatis
    • SqlSessionFactory的创建、mapper xml配置文件的解析
    • open session的流程
    • 查询和插入流程
    • Mapper接口的实现方式
    • 与spring、spring boot集成流程
    • mybatis plus base mapper api注入流程
    • mybatis plus mapper api执行流程
  • spring-tx
    • @EnableTransactionManagement注解的作用
    • @Import注解的作用和示例
    • TransactionInterceptor类事务环绕通知及7种事务传播机制的事务实现方式
    • spring的事务通知代理的创建方式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值