SpringAop源码(三)- EnableAspectJAutoProxy实现原理(中)- getAdvicesAndAdvisorsForBean

目录

一、findAdvisorBeans获取所有可用的切面

二、findAdvisorsThatCanApply从所有切面中获取可用的切面

1)、父类中 findAdvisorsThatCanApply

2)、本类中 buildAspectJAdvisors

1、BeanFactory中获取Object的子类

2、遍历,根据名称获取Class 

3、判断是否是AspectJ注解

4、根据Bean名称获取Aspect

4-1、 getAdvisorMethods(aspectClass)

4-2、getAdvisor

4-2-1、getPointcut

4-2-2、InstantiationModelAwarePointcutAdvisorImpl

4-3、DeclareParentsAdvisor(引介增强)

四、获取继承关系的切面

五、对切面进行排序

总结:


我们现在基本都会使用@Aspect方式进行Aop,为了理解方便比如当前注解类如下,类上注解为@Aspect,方法上注解为:

 @Pointcut("")、@Around("timeConsumeAspect()")、@After("timeConsumeAspect()") ,如下:

@Aspect
@Slf4j
public class TimeConsumeAction {

    /**
     * 只切面 TimeConsume 注解标注的方法
     */
    @Pointcut("@annotation(com.kevin.tool.timeconsume.TimeConsume)")
    private void timeConsumeAspect() {
    }

    /**
     *  Aop环绕
     * @param pjp 切入点信息
     * @return 代理对象
     * @throws Throwable 执行异常
     */
    @Around("timeConsumeAspect()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        
        return pjp.proceed();
    }

    /**
     *  Aop后置方法
     * @param pjp 切入点信息
     * @throws Throwable
     */
    @After("timeConsumeAspect()")
    public void after(JoinPoint pjp) {
       
    }

    private TimeConsume getTimeConsume(JoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        return method.getAnnotation(TimeConsume.class);
    }

}

 比如添加切面的目标类如下:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    @TimeConsume(taskName = "UserService.getInit")
    @Async("createOrder")
    public Integer getInit() throws InterruptedException {
        Thread.sleep(3000);
        userDao.getInit();
        return 2;
    }

    @TimeConsume(taskName = "UserService.getInr", print = true)
    @Async("createOrder")
    public Integer getInr(int i) throws InterruptedException {
        Thread.sleep(2000);
        return userDao.getInr(i);
    }
}

  获取切面:getAdvicesAndAdvisorsForBean方法,如下:

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

    List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
    if (advisors.isEmpty()) {
        return DO_NOT_PROXY;
    }
    return advisors.toArray();
}

继续findEligibleAdvisors方法:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    List<Advisor> candidateAdvisors = findCandidateAdvisors();
    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    extendAdvisors(eligibleAdvisors);
    if (!eligibleAdvisors.isEmpty()) {
        eligibleAdvisors = sortAdvisors(eligibleAdvisors);
    }
    return eligibleAdvisors;
}

一、findAdvisorBeans获取所有可用的切面

// 从Bean工厂中获取所有Advisor(切面)的子类
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
					this.beanFactory, Advisor.class, true, false);
// 根据名称从BeanFactory中获取切面的Bean实例
List<Advisor> advisors = new ArrayList<>();
advisors.add(this.beanFactory.getBean(name, Advisor.class));

二、findAdvisorsThatCanApply从所有切面中获取可用的切面

AnnotationAwareAspectJAutoProxyCreator中实现了该方法:

@Override
protected List<Advisor> findCandidateAdvisors() {
    // Add all the Spring advisors found according to superclass rules.
    List<Advisor> advisors = super.findCandidateAdvisors();
    // Build Advisors for all AspectJ aspects in the bean factory.
    if (this.aspectJAdvisorsBuilder != null) {
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
    }
    return advisors;
}

1)、父类中 findAdvisorsThatCanApply

执行相当于加了一个线程锁,在判断,主要方法是在AopUtils中完成的:

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
    if (candidateAdvisors.isEmpty()) {
        return candidateAdvisors;
    }
    List<Advisor> eligibleAdvisors = new ArrayList<>();
    for (Advisor candidate : candidateAdvisors) {
        if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
            eligibleAdvisors.add(candidate);
        }
    }
    boolean hasIntroductions = !eligibleAdvisors.isEmpty();
    for (Advisor candidate : candidateAdvisors) {
        if (candidate instanceof IntroductionAdvisor) {
            // already processed
            continue;
        }
        if (canApply(candidate, clazz, hasIntroductions)) {
            eligibleAdvisors.add(candidate);
        }
    }
    return eligibleAdvisors;
}

先判断是否是引介切面,后在判断put切面,但是最终都会进入canApply方法:

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;
    }
}

引介切面则获取内部的ClassFilter的matches方法进行判断,普通切面在调用canApply方法,方法内容太多就不进行分析了,主要是调用match方法进行判断。

 

2)、本类中 buildAspectJAdvisors

advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());

之前看过,初始化的aspectJAdvisorsBuilder 为 BeanFactoryAspectJAdvisorsBuilderAdapter类型,其内部属性AspectJAdvisorFactory为 ReflectiveAspectJAdvisorFactory类型。

public List<Advisor> buildAspectJAdvisors() {
    List<String> aspectNames = this.aspectBeanNames;

    if (aspectNames == null) {
        synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
                List<Advisor> advisors = new ArrayList<>();
                aspectNames = new ArrayList<>();
                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;
                    }
                    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);
                            List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                            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.emptyList();
    }
    List<Advisor> advisors = new ArrayList<>();
    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;
}

1、BeanFactory中获取Object的子类

第一次进来都是空的(我debug好几千个beanNames)

String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
      this.beanFactory, Object.class, true, false);

2、遍历,根据名称获取Class<?> 

 Class<?> beanType = this.beanFactory.getType(beanName);

3、判断是否是AspectJ注解

if (this.advisorFactory.isAspect(beanType)) 当前的advisorFactory为:ReflectiveAspectJAdvisorFactory,compiledByAjc判断所有的字段是否有以 "ajc$" 开头的。就会拿到我们上面标有@Aspect的类。

public boolean isAspect(Class<?> clazz) {
    return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
}
private boolean hasAspectAnnotation(Class<?> clazz) {
    return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
}

4、根据Bean名称获取AspectJ增强

MetadataAwareAspectInstanceFactory factory =
    new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
@Override
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
    Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
    String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
    validate(aspectClass);

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

    List<Advisor> advisors = new ArrayList<>();
    for (Method method : getAdvisorMethods(aspectClass)) {
        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;
}

4-1、 getAdvisorMethods(aspectClass)

private List<Method> getAdvisorMethods(Class<?> aspectClass) {
    final List<Method> methods = new ArrayList<>();
    ReflectionUtils.doWithMethods(aspectClass, method -> {
        // Exclude pointcuts
        if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
            methods.add(method);
        }
    }, ReflectionUtils.USER_DECLARED_METHODS);
    methods.sort(METHOD_COMPARATOR);
    return methods;
}

判断我们自己标记的@Aspect的类的所以方法,只有不含有@Pointcut的方法才会添加到List中,那么上面的方法带有@Pointcut的方法就没有被添加进去,但是我们的private方法getTimeConsume却被添加进去了。

顺序依次为:after、getTimeConsume、around

再执行排序:methods.sort(METHOD_COMPARATOR);

排序后的顺序为:around、after、getTimeConsume

 

排序的比较器在static静态代码块中定义,根据方法名称按照定义的顺序进行排序:

4-2、getAdvisor

public Advisor getAdvisor(Method candidateAdviceMethod, 
    MetadataAwareAspectInstanceFactory aspectInstanceFactory,
    int declarationOrderInAspect, String aspectName) {

    validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());

    AspectJExpressionPointcut expressionPointcut = getPointcut(candidateAdviceMethod, 
        aspectInstanceFactory.getAspectMetadata().getAspectClass());
    if (expressionPointcut == null) {
        return null;
    }

    return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, 
        candidateAdviceMethod,
        this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

4-2-1、getPointcut

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());
    if (this.beanFactory != null) {
        ajexp.setBeanFactory(this.beanFactory);
    }
    return ajexp;
}

根据方法信息,调用findAspectJAnnotationOnMethod方法进行判断,就是循环遍历是否为下面的类型,排除掉getTimeConsume方法:

再去new 一个AspectJExpressionPointcut类型返回

4-2-2、InstantiationModelAwarePointcutAdvisorImpl

    根据AspectJExpressionPointcut的切点,new一个InstantiationModelAwarePointcutAdvisorImpl类型的切面,进行返回。

public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
                                                  Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
                                                  MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

    this.declaredPointcut = declaredPointcut;
    this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
    this.methodName = aspectJAdviceMethod.getName();
    this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
    this.aspectJAdviceMethod = aspectJAdviceMethod;
    this.aspectJAdvisorFactory = aspectJAdvisorFactory;
    this.aspectInstanceFactory = aspectInstanceFactory;
    this.declarationOrder = declarationOrder;
    this.aspectName = aspectName;

    if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
        // Static part of the pointcut is a lazy type.
        Pointcut preInstantiationPointcut = Pointcuts.union(
                aspectInstanceFactory.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, aspectInstanceFactory);
        this.lazy = true;
    }
    else {
        // A singleton aspect.
        this.pointcut = this.declaredPointcut;
        this.lazy = false;
        this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
    }
}

    构造函数内部看似复杂,其实最主要的是初始化instantiatedAdvice = instantiateAdvice(this.declaredPointcut),即对当前切面内的增强的处理。之前获取到了增强信息,和增强上的Spring Expression字符串,但是还没有进行解析。

private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {
    Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod,  
         pointcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
    return (advice != null ? advice : EMPTY_ADVICE);
}
public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
						MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

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

	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;

	switch (aspectJAnnotation.getAnnotationType()) {
		case AtPointcut:
			if (logger.isDebugEnabled()) {
				logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
			}
			return null;
		case AtAround:
			springAdvice = new AspectJAroundAdvice(
					candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
			break;
		case AtBefore:
			springAdvice = new AspectJMethodBeforeAdvice(
					candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
			break;
		case AtAfter:
			springAdvice = new AspectJAfterAdvice(
					candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
			break;
		case AtAfterReturning:
			springAdvice = new AspectJAfterReturningAdvice(
					candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
			AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
			if (StringUtils.hasText(afterReturningAnnotation.returning())) {
				springAdvice.setReturningName(afterReturningAnnotation.returning());
			}
			break;
		case AtAfterThrowing:
			springAdvice = new AspectJAfterThrowingAdvice(
					candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
			AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
			if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
				springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
			}
			break;
		default:
			throw new UnsupportedOperationException(
					"Unsupported advice type on method: " + candidateAdviceMethod);
	}

	// Now to configure the advice...
	springAdvice.setAspectName(aspectName);
	springAdvice.setDeclarationOrder(declarationOrder);
	String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
	if (argNames != null) {
		springAdvice.setArgumentNamesFromStringArray(argNames);
	}
	springAdvice.calculateArgumentBindings();

	return springAdvice;
}

比较清楚了,根据不同的注解类型,初始化AspectJ类型的增强(Advice)。这样,增强初始化完成,切入点确认,切面也初始化完成。

4-3、DeclareParentsAdvisor(引介增强

判断所有字段(包括私有)是否有添加@DeclareParents注解,则返回DeclareParentsAdvisor类型的增强。

aspectClass.getDeclaredFields()
private Advisor getDeclareParentsAdvisor(Field introductionField) {
    DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class);
    if (declareParents == null) {
        // Not an introduction field
        return null;
    }

    if (DeclareParents.class == declareParents.defaultImpl()) {
        throw new IllegalStateException("'defaultImpl' attribute must be set on DeclareParents");
    }

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

 

四、获取继承关系的增强

在父类AspectJAwareAdvisorAutoProxyCreator中进行了重写,就是判断是否需要添加默认的拦截器。

protected void extendAdvisors(List<Advisor> candidateAdvisors) {
    AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}
public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
    // Don't add advisors to an empty list; may indicate that proxying is just not required
    if (!advisors.isEmpty()) {
        boolean foundAspectJAdvice = false;
        for (Advisor advisor : advisors) {
            // Be careful not to get the Advice without a guard, as this might eagerly
            // instantiate a non-singleton AspectJ aspect...
            if (isAspectJAdvice(advisor)) {
                foundAspectJAdvice = true;
                break;
            }
        }
        if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
            advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
            return true;
        }
    }
    return false;
}

当前Advisor中包含了AspectJ的拦截器,并且没有DefaultPointcutAdvisor(默认拦截器,并且Advice为ExposeInvocationInterceptor类型)则把默认拦截器加在第一位。当前目标类UserService则已经获取到了三个增强,并且顺序为:

org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR

InstantiationModelAwarePointcutAdvisorImpl类型的around增强(我们自己添加@Around的方法封装)

InstantiationModelAwarePointcutAdvisorImpl类型的after增强(我们自己添加@After的方法封装)

 

五、对增强进行排序

AnnotationAwareOrderComparator.sort(advisors);

总结:

    先回顾一下大前提,每个Bean的实例化 BeanFactory.getBean(。。。),都会调用该方法:

1、获取所有BeanFactory中Advisor的子类,进行缓存

2、从所有的前面中遍历匹配当前Class的增强,分为两部分

    1)、父类AbstractAdvisorAutoProxyCreator中,根据Class调用canApply进行判断,最后返回Advisor子类Bean

    2)、AnnotationAwareAspectJAutoProxyCreator自己中,是处理@Aspect的类。分为两部分,方法(普通增强)、字段(引介增强)

    处理普通切面:比较暴力,在BeanFactory中获取所有Object子类的Bean进行遍历,判断是否在类上添加@Aspect注解。有的话获取所有不包含@Pointcut的方法。在根据方法名称按照(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class)匹配和排序。就拿到了切点方法,new AspectJExpressionPointcut类型的切点,再new InstantiationModelAwarePointcutAdvisorImpl类型切面进行返回。

    进阶切面:获取方法的所有字段,判断是否有添加@DeclareParents注解,则new DeclareParentsAdvisor切面进行返回。

3、获取继承的增强

4、对增强进行排序

    这样就完成了根据Bean名称和类获取排序好的增强就完成了,等到AbstractApplicationContext的refresh的最后,加载单利非懒加载的Bean调用完。所有该处理的Aop类都处理完了。但是这只是获取到了增强,还应该根据当前的切面(this)去创建切面,获取代理等待被调用。

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值