Spring AOP工作机制进一步理解(中篇)

       在Spring AOP工作机制进一步理解(上篇)中,通过源码,分析了<aop:config>配置下的Spring AOP工作机制,本文在总结上篇的基础上,分析<aop:aspectj-autoproxy>配置下的工作机制。

<aop:aspectj-autoproxy>

Spring xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:aop="http://www.springframework.org/schema/aop" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
						http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
						http://www.springframework.org/schema/context  
						http://www.springframework.org/schema/context/spring-context-3.0.xsd
						http://www.springframework.org/schema/aop
						http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
  	<!-- 用户业务逻辑bean --> 
	<bean id="userAction" class="org.test.aop.UserAction"/>
  	
	<!-- 扫描日志切面bean -->
	<context:component-scan base-package="org.test.aop"/>
  	
	<!-- 开启AOP注解方式 -->
	<aop:aspectj-autoproxy/>
		  
</beans>

解析

工作流程

        Spring xml的解析过程详见《Spring ioc容器启动流程—obtainFreshBeanFactory》,这里只涉及<aop:aspectj-autoproxy>的解析工作。AopNamespaceHandler会委托AspectJAutoProxyBeanDefinitionParser来进行解析工作,详细流程如下:


相关源码

/** AopConfigUtils.java */
// 注册AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition
// 其中cls的值为AnnotationAwareAspectJAutoProxyCreator.class
private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, Object source) {
	Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
	// 判断是否已注册AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition
	// 其在beanFactory中对应的key为"org.springframework.aop.config.internalAutoProxyCreator"
	if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
		BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
		if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
			int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
			int requiredPriority = findPriorityForClass(cls);
			if (currentPriority < requiredPriority) {
				apcDefinition.setBeanClassName(cls.getName());
			}
		}
		return null;
	}
	// 创建且注册AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition
	RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
	beanDefinition.setSource(source);
	beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
	beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
	registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
	return beanDefinition;
}


/** AopNamespaceUtils.java */
private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Element sourceElement) {
	if (sourceElement != null) {
		boolean proxyTargetClass = Boolean.valueOf(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE));
		// 获取<aop:aspectj-autoproxy/>的proxy-target-class属性值
		if (proxyTargetClass) {
			// 设置AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition的proxyTargetClass属性为true
			AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
		}
		// 获取<aop:aspectj-autoproxy/>的expose-proxy属性值
		boolean exposeProxy = Boolean.valueOf(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE));
		if (exposeProxy) {
			// 设置AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition的exposeProxy属性为true
			AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
		}
	}
}

/** AopConfigUtils.java */
public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
	if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
		BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
		definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
	}
}

static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
	if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
		BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
		definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
	}
}


/** AopNamespaceUtils.java */
private static void registerComponentIfNecessary(BeanDefinition beanDefinition, ParserContext parserContext) {
	if (beanDefinition != null) {
		BeanComponentDefinition componentDefinition =
				new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME);
		parserContext.registerComponent(componentDefinition);
	}
}


/** AspectJAutoProxyBeanDefinitionParser.java */
private void extendBeanDefinition(Element element, ParserContext parserContext) {
	BeanDefinition beanDef =
			parserContext.getRegistry().getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME);
	if (element.hasChildNodes()) {
		addIncludePatterns(element, parserContext, beanDef);
	}
}

生成代理对象

         在<aop:aspectj-autoproxy/>解析过程中,主要工作就是注册AnnotationAwareAspectJAutoProxyCreator RootBeanDefinition。

AnnotationAwareAspectJAutoProxyCreator对象实例化

         与<aop:config>方式一样。

InstantiationModelAwarePointcutAdvisorImpl对象实例化

         <aop:aspectj-autoproxy/>配置下所有@Aspect中Advice注解最终都会实例为InstantiationModelAwarePointcutAdvisorImpl对象,以进行AOP拦截。与<aop:config>方式一样,在生成业务userAction代理对象过程中,InstantiationModelAwarePointcutAdvisorImpl的实例化发生在resolveBeforeInstantiation环节,与<aop:config>方式不同的是,AnnotationAwareAspectJAutoProxyCreator覆盖了AspectJAwareAdvisorAutoProxyCreator的findCandidateAdvisors方法,自行实例化注解的PointcutAdvisor。下面解析只涉及AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors,其他过程忽略。

AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors基本流程


相关源码

/** AnnotationAwareAspectJAutoProxyCreator.java */
// <aop:aspectj-autoproxy/>注解方式下的PointcutAdvisor即InstantiationModelAwarePointcutAdvisorImpl对象实例化
protected List<Advisor> findCandidateAdvisors() {
	// 实例化<aop:config>的AspectJPointcutAdvisor
	List<Advisor> advisors = super.findCandidateAdvisors();
	// 实例化<aop:aspectj-autoproxy/>的InstantiationModelAwarePointcutAdvisorImpl
	advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
	return advisors;
}


/** BeanFactoryAspectJAdvisorsBuilder.java */
public List<Advisor> buildAspectJAdvisors() {
	List<String> aspectNames = null;

	synchronized (this) {
		aspectNames = this.aspectBeanNames;
		if (aspectNames == null) {
			List<Advisor> advisors = new LinkedList<Advisor>();
			aspectNames = new LinkedList<String>();
			// 获取beanFactory及其循环上级的所有bean
			String[] beanNames =
					BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false);
			for (String beanName : beanNames) {
				if (!isEligibleBean(beanName)) {
					continue;
				}
				// We must be careful not to instantiate beans eagerly as in this
				// case they would be cached by the Spring container but would not
				// have been weaved
				Class beanType = this.beanFactory.getType(beanName);
				if (beanType == null) {
					continue;
				}
				// 判断beanType及其循环super Class\interface是否带@Aspect
				// 采用(clazz.getAnnotation(annotationType)!= null)判断
				if (this.advisorFactory.isAspect(beanType)) {
					aspectNames.add(beanName);
					AspectMetadata amd = new AspectMetadata(beanType, beanName);
					if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
						MetadataAwareAspectInstanceFactory factory =
								new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
						// 获取Advisors
						List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
						// 缓存classAdvisors
						if (this.beanFactory.isSingleton(beanName)) {
							this.advisorsCache.put(beanName, classAdvisors);
						}
						else {
							this.aspectFactoryCache.put(beanName, factory);
						}
						advisors.addAll(classAdvisors);
					}
					else {
						// Per target or per this.
						if (this.beanFactory.isSingleton(beanName)) {
							throw new IllegalArgumentException("Bean with name '" + beanName +
									"' is a singleton, but aspect instantiation model is not singleton");
						}
						MetadataAwareAspectInstanceFactory factory =
								new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
						this.aspectFactoryCache.put(beanName, factory);
						advisors.addAll(this.advisorFactory.getAdvisors(factory));
					}
				}
			}
			this.aspectBeanNames = aspectNames;
			return advisors;
		}
	}

	if (aspectNames.isEmpty()) {
		return Collections.EMPTY_LIST;
	}
	// 从缓存中获取advisors
	List<Advisor> advisors = new LinkedList<Advisor>();
	for (String aspectName : aspectNames) {
		List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
		if (cachedAdvisors != null) {
			advisors.addAll(cachedAdvisors);
		}
		else {
			MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
			advisors.addAll(this.advisorFactory.getAdvisors(factory));
		}
	}
	return advisors;
}

/** ReflectiveAspectJAdvisorFactory */
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory maaif) {
	final Class<?> aspectClass = maaif.getAspectMetadata().getAspectClass();
	final String aspectName = maaif.getAspectMetadata().getAspectName();
	validate(aspectClass);

	// We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
	// so that it will only instantiate once.
	final MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
			new LazySingletonAspectInstanceFactoryDecorator(maaif);

	final List<Advisor> advisors = new LinkedList<Advisor>();
	for (Method method : getAdvisorMethods(aspectClass)) {
		// 实例化<aop:aspectj-autoproxy/>注解方式下的PointcutAdvisor即InstantiationModelAwarePointcutAdvisorImpl
		Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
		if (advisor != null) {
			advisors.add(advisor);
		}
	}

	// If it's a per target aspect, emit the dummy instantiating aspect.
	if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
		Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
		advisors.add(0, instantiationAdvisor);
	}

	// Find introduction fields.
	for (Field field : aspectClass.getDeclaredFields()) {
		Advisor advisor = getDeclareParentsAdvisor(field);
		if (advisor != null) {
			advisors.add(advisor);
		}
	}

	return advisors;
}

// 获取aspectClass及其循环super Class\interface的不带@Pointcut的方法
private List<Method> getAdvisorMethods(Class<?> aspectClass) {
	final List<Method> methods = new LinkedList<Method>();
	// 回调doWith
	ReflectionUtils.doWithMethods(aspectClass, new ReflectionUtils.MethodCallback() {
		public void doWith(Method method) throws IllegalArgumentException {
			// 不包括pointcuts
			if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
				methods.add(method);
			}
		}
	});
	// 排序
	Collections.sort(methods, METHOD_COMPARATOR);
	return methods;
}

/** ReflectionUtils.java */
public static void doWithMethods(Class<?> clazz, MethodCallback mc) throws IllegalArgumentException {
	doWithMethods(clazz, mc, null);
}
public static void doWithMethods(Class<?> clazz, MethodCallback mc, MethodFilter mf)
		throws IllegalArgumentException {

	// 获取Declared Methods
	Method[] methods = clazz.getDeclaredMethods();
	for (Method method : methods) {
		if (mf != null && !mf.matches(method)) {
			continue;
		}
		try {
			mc.doWith(method);
		}
		catch (IllegalAccessException ex) {
			throw new IllegalStateException("Shouldn't be illegal to access method '" + method.getName()
					+ "': " + ex);
		}
	}
	if (clazz.getSuperclass() != null) {
		// 循环super Class
		doWithMethods(clazz.getSuperclass(), mc, mf);
	}
	else if (clazz.isInterface()) {
		for (Class<?> superIfc : clazz.getInterfaces()) {
			// 循环interface
			doWithMethods(superIfc, mc, mf);
		}
	}
}

/** ReflectiveAspectJAdvisorFactory.java */
public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aif,
		int declarationOrderInAspect, String aspectName) {

	validate(aif.getAspectMetadata().getAspectClass());

	AspectJExpressionPointcut ajexp =
			getPointcut(candidateAdviceMethod, aif.getAspectMetadata().getAspectClass());
	if (ajexp == null) {
		return null;
	}
	// PointcutAdvisor对象实例化,<aop:aspectj-autoproxy/>注解方式下的PointcutAdvisor
	return new InstantiationModelAwarePointcutAdvisorImpl(
			this, ajexp, aif, candidateAdviceMethod, declarationOrderInAspect, aspectName);
}

// 获取candidateAdviceMethod的advice注解,提取Pointcut,返回AspectJExpressionPointcut
private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
	AspectJAnnotation<?> aspectJAnnotation =
			AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
	if (aspectJAnnotation == null) {
		return null;
	}
	AspectJExpressionPointcut ajexp =
			new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]);
	ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
	return ajexp;
}

/** AbstractAspectJAdvisorFactory.java */
protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) {
	//所有Advice注解
	Class<? extends Annotation>[] classesToLookFor = new Class[] {
			Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class};
	// 循环查找匹配Advice的AspectJAnnotation
	for (Class<? extends Annotation> c : classesToLookFor) {
		AspectJAnnotation foundAnnotation = findAnnotation(method, c);
		if (foundAnnotation != null) {
			return foundAnnotation;
		}
	}
	return null;
}

private static <A extends Annotation> AspectJAnnotation<A> findAnnotation(Method method, Class<A> toLookFor) {
	A result = AnnotationUtils.findAnnotation(method, toLookFor);
	if (result != null) {
		// 获取到annotation则直接返回AspectJAnnotation
		return new AspectJAnnotation<A>(result);
	}
	else {
		return null;
	}
}

/** AnnotationUtils.java */
public static <A extends Annotation> A findAnnotation(Method method, Class<A> annotationType) {
	// 获取到annotation则直接返回
	A annotation = getAnnotation(method, annotationType);
	Class<?> clazz = method.getDeclaringClass();
	if (annotation == null) {
		// 在clazz的interface中查找带annotationType的method
		annotation = searchOnInterfaces(method, annotationType, clazz.getInterfaces());
	}
	while (annotation == null) {
		// 在clazz的super Class中查找带annotationType的method
		clazz = clazz.getSuperclass();
		if (clazz == null || clazz.equals(Object.class)) {
			break;
		}
		try {
			Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
			annotation = getAnnotation(equivalentMethod, annotationType);
		}
		catch (NoSuchMethodException ex) {
			// No equivalent method found
		}
		if (annotation == null) {
			annotation = searchOnInterfaces(method, annotationType, clazz.getInterfaces());
		}
	}
	return annotation;
}

public static <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
	Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
	// 利用Class.getAnnotation方法获取Annotation Class
	A ann = resolvedMethod.getAnnotation(annotationType);
	if (ann == null) {
		for (Annotation metaAnn : resolvedMethod.getAnnotations()) {
			ann = metaAnn.annotationType().getAnnotation(annotationType);
			if (ann != null) {
				break;
			}
		}
	}
	return ann;
}

/** InstantiationModelAwarePointcutAdvisorImpl.java */
// new InstantiationModelAwarePointcutAdvisorImpl对象
public InstantiationModelAwarePointcutAdvisorImpl(AspectJAdvisorFactory af, AspectJExpressionPointcut ajexp,
		MetadataAwareAspectInstanceFactory aif, Method method, int declarationOrderInAspect, String aspectName) {

	this.declaredPointcut = ajexp;
	this.method = method;
	this.atAspectJAdvisorFactory = af;
	this.aspectInstanceFactory = aif;
	this.declarationOrder = declarationOrderInAspect;
	this.aspectName = aspectName;

	if (aif.getAspectMetadata().isLazilyInstantiated()) {
		// Static part of the pointcut is a lazy type.
		Pointcut preInstantiationPointcut =
				Pointcuts.union(aif.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

		// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
		// If it's not a dynamic pointcut, it may be optimized out
		// by the Spring AOP infrastructure after the first evaluation.
		this.pointcut = new PerTargetInstantiationModelPointcut(this.declaredPointcut, preInstantiationPointcut, aif);
		this.lazy = true;
	}
	else {
		// singleton aspect.
		// 实例化的Advice
		this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
		// pointcut对象
		this.pointcut = declaredPointcut;
		this.lazy = false;
	}
}

// 实例化Advice
private Advice instantiateAdvice(AspectJExpressionPointcut pcut) {
	return this.atAspectJAdvisorFactory.getAdvice(
			this.method, pcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
}

/** ReflectiveAspectJAdvisorFactory.java */
// 获取candidateAdviceMethod上的Advice对象
public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut ajexp,
		MetadataAwareAspectInstanceFactory aif, int declarationOrderInAspect, String aspectName) {

	Class<?> candidateAspectClass = aif.getAspectMetadata().getAspectClass();
	validate(candidateAspectClass);

	// candidateAdviceMethod上的注解对象
	AspectJAnnotation<?> aspectJAnnotation =
			AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
	if (aspectJAnnotation == null) {
		return null;
	}

	// If we get here, we know we have an AspectJ method.
	// Check that it's an AspectJ-annotated class
	if (!isAspect(candidateAspectClass)) {
		throw new AopConfigException("Advice must be declared inside an aspect type: " +
				"Offending method '" + candidateAdviceMethod + "' in class [" +
				candidateAspectClass.getName() + "]");
	}

	if (logger.isDebugEnabled()) {
		logger.debug("Found AspectJ method: " + candidateAdviceMethod);
	}

	AbstractAspectJAdvice springAdvice;

	// 根据Advice注解类型,实例化相应的Advice,用candidateAdviceMethod、ajexp、aif进行构造
	switch (aspectJAnnotation.getAnnotationType()) {
		case AtBefore: // @Before
			springAdvice = new AspectJMethodBeforeAdvice(candidateAdviceMethod, ajexp, aif);
			break;
		case AtAfter: // @After
			springAdvice = new AspectJAfterAdvice(candidateAdviceMethod, ajexp, aif);
			break;
		case AtAfterReturning: // @AfterReturning
			springAdvice = new AspectJAfterReturningAdvice(candidateAdviceMethod, ajexp, aif);
			AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
			if (StringUtils.hasText(afterReturningAnnotation.returning())) {
				springAdvice.setReturningName(afterReturningAnnotation.returning());
			}
			break;
		case AtAfterThrowing: // @AfterThrowing
			springAdvice = new AspectJAfterThrowingAdvice(candidateAdviceMethod, ajexp, aif);
			AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
			if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
				springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
			}
			break;
		case AtAround: // @Around
			springAdvice = new AspectJAroundAdvice(candidateAdviceMethod, ajexp, aif);
			break;
		case AtPointcut: // @Pointcut
			if (logger.isDebugEnabled()) {
				logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
			}
			return null;
		default:
			throw new UnsupportedOperationException(
					"Unsupported advice type on method " + candidateAdviceMethod);
	}

	// 配置Advice对象
	springAdvice.setAspectName(aspectName);
	springAdvice.setDeclarationOrder(declarationOrderInAspect);
	String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
	if (argNames != null) {
		// 设置参数名称
		springAdvice.setArgumentNamesFromStringArray(argNames);
	}
	springAdvice.calculateArgumentBindings();
	return springAdvice;
}

/** ReflectiveAspectJAdvisorFactory.java */
// 获取@DeclareParents Annotation对象
private Advisor getDeclareParentsAdvisor(Field introductionField) {
	DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class);
	if (declareParents == null) {
		// Not an introduction field
		return null;
	}

	if (DeclareParents.class.equals(declareParents.defaultImpl())) {
		// This is what comes back if it wasn't set. This seems bizarre...
		// TODO this restriction possibly should be relaxed
		throw new IllegalStateException("defaultImpl must be set on DeclareParents");
	}

	return new DeclareParentsAdvisor(
			introductionField.getType(), declareParents.value(), declareParents.defaultImpl());
}

AnnotationAwareAspectJAutoProxyCreator生成代理类实例

          与<aop:config>方式一样。

调用代理对象

          与<aop:config>方式一样。

         <aop:aspectj-autoproxy/>与<aop:config>方式本质区别是PointcutAdvisor对象实例化方式不一样:<aop:aspectj-autoproxy/>采用注解方式,而<aop:config>则采用显示的xml配置方式,集中体现在findCandidateAdvisors。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值