spring源码分析-springAOP

一.实现的原理简介

1.我们都只知道AOP是基于代理模式来实现的,这里我们来思考spring是如何基于动态代理来实现AOP?通过分析,他实现的大概的流程如下图:
在这里插入图片描述
接着,我们用代码来实现一个基于注解模式的简单的spring AOP,代码如下:

import org.springframework.stereotype.Component;

/**
 * @author zhouyongquan
 * @date 2019/10/21 上午11:15
 * @description
 */
@Component
public class MicAop {

    public void micWork() {
        System.out.println("I do something");
    }
}
/**
 * @author zhouyongquan
 * @date 2019/10/21 上午11:07
 * @description
 */
@Component
@Aspect
public class MicAopConfig {

    /**
     * 配置切入点,理解:@Pointcut里面配置类被拦截的代理的对象的方法
     */
    @Pointcut("execution(* mic.com.MicAop..*(..))")
    public void micPointcut() {

    }

    /**
     * 配置切面,理解:在这些切入点被调用前,我们统一做了什么的操作,一个切面
     */
    @Before("micPointcut()")
    public void micDoBefore() {
        System.out.println("micDoBefore");
    }
}

这样,在MicAop#micWork之前,就会执行切面micDoBefore的逻辑。

二.基于上面的流程,分析具体的实现原理

1.首先,我们需要报MicAopConfig里面配置的内容保存到BeanFactory里面,这个过程涉及到切面的解析。
(1)实现代理的关键的源头是AnnotionAwareAspactAutoProxy这类,我们先来看看他的继承结构:
在这里插入图片描述
AnnotionAwareAspactAutoProxyCreator 继承了BeanPostProcessor,BeanPostProcessor这个里的实现类类里面有很多执行处理节点,其中一个节点就是关于Bean实例化之后的,AnnotionAwareAspactAutoProxyCreator 做的就是在Bean初始化的时候进行拦截,根据提前解析好的切面信息,对bean里面的方法进行匹配,如果匹配成功,则进行代理对象的创建。
(2)我们首先来分析AnnotionAwareAspactAutoProxyCreator ,在bean第一次实例化的时候,就会解析保存AOP的信息到实例中。
跟踪下面两个方法:
AbstractApplicationContext#refresh(bean初始化上下文的主干方法)、AbstractApplicationContext#registerBeanPostProcessors(执行实例化并保存所有实现BeanPostProcessor接口的类)
AnnotationAwareAspectJAutoProxyCreator是继承了BeanfactoryAware接口,所以在实例化时,会执行setFactory方法。而所有切面信息解析的执行者BeanFactoryAspectJAdvisorsBuilderAdapter初始化的时机也是在setFactory方法。
跟踪方法:
AnnotationAwareAspectJAutoProxyCreator#initBeanFactory
源码:

@Override
	protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		super.initBeanFactory(beanFactory);
		if (this.aspectJAdvisorFactory == null) {
			this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory);
		}
		this.aspectJAdvisorsBuilder =
				new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory);
	}

BeanFactoryAspectJAdvisorsBuilderAdapter这个实例会根据BeanFactory里面保存的AOP的信息进行解析保存,但是马上执行,他是在第一次初始化bean实力的时候,才会解析保存AOP配置信息。解析BeanFactory里面保存的AOP信息的方法是:BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors,他会在初次执行AnnotationAwareAspectJAutoProxyCreator调用postProcessBeforeInitialization时开始执行。
(2)解析保存AOP配置信息,需要解析缓存切面
InstantiationAwareBeanPostProcessor接口定义的postProcessBeforeInitialization方法是一个可以对已经注入依赖属性的bean对象实例进行编辑操作的接口,我们会在AbstractAutowireCapableBeanFactory#doCreateBean、AbstractAutowireCapableBeanFactory#initializeBean(String, Object, RootBeanDefinition)、AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInstantiation这几个方法里面调用到。在postProcessBeforeInitialization里面,会进行初次缓存切面信息。
进入InstantiationAwareBeanPostProcessor#postProcessBeforeInitialization方法分析代码。
AbstractAutoProxyCreator#postProcessBeforeInstantiation、
AspectJAwareAdvisorAutoProxyCreator#shouldSkip
进入如下代码AbstractAutoProxyCreator,这个实例也就是之前一开始初始化的AnnotationAwareAspectJAutoProxyCreator实例,进入实例的shouldSkip 方法。

@Override
	protected boolean shouldSkip(Class<?> beanClass, String beanName) {
		// TODO: Consider optimization by caching the list of the aspect names
		//预先解析好缓存好的切面信息
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		//遍历切面信息
		for (Advisor advisor : candidateAdvisors) {
			if (advisor instanceof AspectJPointcutAdvisor &&
					((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
				return true;
			}
		}
		return super.shouldSkip(beanClass, beanName);
	}

进入预先解析好缓存好的切面信息的findCandidateAdvisors()方法

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

findCandidateAdvisors()方法负责预想解析切面信息,并且会进行缓存。
那么问题来了,首先它是如何进行解析的?
我们来看看这个调用:aspectJAdvisorsBuilder.buildAspectJAdvisors()
他的源码如下:

/**
	 * Look for AspectJ-annotated aspect beans in the current bean factory,
	 * and return to a list of Spring AOP Advisors representing them.
	 * <p>Creates a Spring Advisor for each AspectJ advice method.
	 * @return the list of {@link org.springframework.aop.Advisor} beans
	 * @see #isEligibleBean
	 */
	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<>();
					//1.首先获取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;
						}
						//2.判断bean的类型是不是切面类型
						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);
								//3.如果是切面,就解析AOP配置,返回Advisor对象集合
								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;
	}

判断是不是切面的方法源码:

@Override
	public boolean isAspect(Class<?> clazz) {
	   //就是判断右面@Aspect这个注解
		return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
	}

接着,我们关心他是如何解析等到Advisor对象集合的?
我们关注这两个过程

MetadataAwareAspectInstanceFactory factory =new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
//获取Advisor对象集合
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;
	}
@Override
	@Nullable
	public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {

		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
       ///解析判断候选方法是否有@Before,@After,@Around等注解,如果有,就继续执行新建Advisor对象。
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		if (expressionPointcut == null) {
			return null;
		}

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

getAdvisor 遍历所有没被@PointCut注解标注的方法,获取有@Before,@After,@Around等注解的方法。
新建的Advisor都会保存在BeanFactoryAspectJAdvisorsBuilder#advisorsCache这个缓存里面,AnnotionAwareAspactAutoProxy在bean初始化的时候,需要在这个预先设置的切面缓存里面,进行匹配,匹配中了,才会进行代理对象的创建。
到目前为止的Advisor里面并没有对@PointCut注解进行处理。
追中代码,我们发现AnnotationAwareAspectJAutoProxyCreator#postProcessAfterInstantiation第一次执行时解析拦截表达式。
(3)接下来就是选择适配的切面了
在AbstractAutoProxyCreator#postProcessAfterInitialization时,会找到在缓存好的所有切面的信息,会进行切面适配,适配成功会创建代理对象,分析以下两个方法
AbstractAutoProxyCreator#postProcessAfterInitialization、
AbstractAutoProxyCreator#wrapIfNecessary

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

分析查找适配切面的方法:Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

源码如下:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
       //从缓存里面取出所有的切面信息
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		//根据advisor信息中的表达式进行方法对class的匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

findEligibleAdvisors方法会取出切面缓存,然后根据advisor信息中的表达式进行方法对class的匹配,如果是第一次findAdvisorsThatCanApply方法的话,他的@Pointcut里面的信息并没有保存初始化,这时候,就需要初始化@Pointcut的信息,在findAdvisorsThatCanApply的时候,他就会初始化@Pointcut的值
方法调用链如下:

AopUtils#findAdvisorsThatCanApply(candidateAdvisors, beanClass);
AopUtils#canApply(Advisor advisor, Class<?> targetClass);
AspectJExpressionPointcut#getClassFilter
AspectJExpressionPointcut#checkReadyToMatch

源码如下:

private void checkReadyToMatch() {
   if (getExpression() == null) {
      throw new IllegalStateException("Must set property 'expression' before attempting to match");
   }
   if (this.pointcutExpression == null) {
      this.pointcutClassLoader = (this.beanFactory instanceof ConfigurableBeanFactory ?
            ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader() :
            ClassUtils.getDefaultClassLoader());
       //解析得到拦截表达式,例如根据@Before的value来关联查询出对应的表达式
      this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
   }
}

匹配时就根据pointcutExpression循环进行匹配class的方法
(4)创建代理对象
如果发现有适配的切面,那么,我们需要创建代理对象,创建代理对象,我们还是回到wrapIfNecessary这个方法
部门源码如下:

// Create proxy if we have advice.
       //1.首先查找匹配的切面
		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;

查看createProxy方法

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

		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}
		//新建代理对象工厂
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);
       //设置工厂代理类
		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(getProxyClassLoader());方法,他是如何创建代理对象的:

protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
		return getAopProxyFactory().createAopProxy(this);
	}
@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
			//基于JDK的方式来创建动态代理对象
				return new JdkDynamicAopProxy(config);
			}
			//基于cglib方式来创建动态代理对象。
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

使用cglib代理,代理如下:

@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isTraceEnabled()) {
			logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
		}

		try {
			Class<?> rootClass = this.advised.getTargetClass();
			Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

			Class<?> proxySuperClass = rootClass;
			if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
				proxySuperClass = rootClass.getSuperclass();
				Class<?>[] additionalInterfaces = rootClass.getInterfaces();
				for (Class<?> additionalInterface : additionalInterfaces) {
					this.advised.addInterface(additionalInterface);
				}
			}

			// Validate the class, writing log messages as necessary.
			validateClassIfNecessary(proxySuperClass, classLoader);

			// Configure CGLIB Enhancer...
			Enhancer enhancer = createEnhancer();
			if (classLoader != null) {
				enhancer.setClassLoader(classLoader);
				if (classLoader instanceof SmartClassLoader &&
						((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
					enhancer.setUseCache(false);
				}
			}
			enhancer.setSuperclass(proxySuperClass);
			enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
			enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
			enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));
			//获取拦截回调函数
			Callback[] callbacks = getCallbacks(rootClass);
			Class<?>[] types = new Class<?>[callbacks.length];
			for (int x = 0; x < types.length; x++) {
				types[x] = callbacks[x].getClass();
			}
			// fixedInterceptorMap only populated at this point, after getCallbacks call above
			enhancer.setCallbackFilter(new ProxyCallbackFilter(
					this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
			enhancer.setCallbackTypes(types);

			// Generate the proxy class and create a proxy instance.
			//返回一个cglib代理对象
			return createProxyClassAndInstance(enhancer, callbacks);
		}
		catch (CodeGenerationException | IllegalArgumentException ex) {
			throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
					": Common causes of this problem include using a final class or a non-visible class",
					ex);
		}
		catch (Throwable ex) {
			// TargetSource.getTarget() failed
			throw new AopConfigException("Unexpected AOP exception", ex);
		}
	}

普通AOP采用的回调函数

Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

cglib 的aop回调函数如下

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
   Object oldProxy = null;
   boolean setProxyContext = false;
   Class<?> targetClass = null;
   Object target = null;
   try {
       //这里注入的advised就是之前创建的ProxyFactory对象
      if (this.advised.exposeProxy) {
         // Make invocation available if necessary.
         oldProxy = AopContext.setCurrentProxy(proxy);
         setProxyContext = true;
      }
      // May be null. Get as late as possible to minimize the time we
      // "own" the target, in case it comes from a pool...
      target = getTarget();
      if (target != null) {
         targetClass = target.getClass();
      }
       //根据切面信息创建切面内容调用链
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
      Object retVal;
      // Check whether we only have one InvokerInterceptor: that is,
      // no real advice, but just reflective invocation of the target.
      if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
         // We can skip creating a MethodInvocation: just invoke the target directly.
         // Note that the final invoker must be an InvokerInterceptor, so we know
         // it does nothing but a reflective operation on the target, and no hot
         // swapping or fancy proxying.
         Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
         retVal = methodProxy.invoke(target, argsToUse);
      }
      else {
         // We need to create a method invocation...
          //创建一个方法调用对象,具体调用实现没分析,Before逻辑大概是先调用切面,在反射调用目标方法
         retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
      }
      retVal = processReturnType(proxy, target, method, retVal);
      return retVal;
   }
   finally {
      if (target != null) {
         releaseTarget(target);
      }
      if (setProxyContext) {
         // Restore old proxy.
         AopContext.setCurrentProxy(oldProxy);
      }
   }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值