Spring创建及使用切面过程

AspectJ和Spring aop

AspectJ是一个强大的aop框架,可以单独实现静态字节码代理,故而可以用于代理final,abstract方法;而Spring aop是基于动态代理,并且也涵盖了AspectJ实现代理。普通情况下,为了实现方便,都是直接使用spring aop实现动态代理。

Spring创建切面

Spring是在创建bean的时候整理其对应的切面,主要过程如下。

创建入口

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
      throws BeanCreationException {

   // 创建bean的实例
   BeanWrapper instanceWrapper = null;
   if (mbd.isSingleton()) {
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
   }
   if (instanceWrapper == null) {
      instanceWrapper = createBeanInstance(beanName, mbd, args);
   }
   final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
   Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
   mbd.resolvedTargetType = beanType;

   // 前置处理器修改beandefinition
   synchronized (mbd.postProcessingLock) {
      if (!mbd.postProcessed) {
         try {
            applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
         }
         catch (Throwable ex) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                  "Post-processing of merged bean definition failed", ex);
         }
         mbd.postProcessed = true;
      }
   }

   // 更早的暴露防止循环引用
   boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
         isSingletonCurrentlyInCreation(beanName));
   if (earlySingletonExposure) {
      if (logger.isDebugEnabled()) {
         logger.debug("Eagerly caching bean '" + beanName +
               "' to allow for resolving potential circular references");
      }
      addSingletonFactory(beanName, new ObjectFactory<Object>() {
         @Override
         public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
         }
      });
   }

   Object exposedObject = bean;
   try {
      // 填充bean的属性
      populateBean(beanName, mbd, instanceWrapper);
      if (exposedObject != null) {
          // !!!初始化bean,这里就是织入切面
         exposedObject = initializeBean(beanName, exposedObject, mbd);
      }
   }
   catch (Throwable ex) {
      if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
         throw (BeanCreationException) ex;
      }
      else {
         throw new BeanCreationException(
               mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
      }
   }

   if (earlySingletonExposure) {
       // 获取单例
      Object earlySingletonReference = getSingleton(beanName, false);
      if (earlySingletonReference != null) {
         if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
         }
         else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
             //获取相关依赖的bean
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
               if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                  actualDependentBeans.add(dependentBean);
               }
            }
            if (!actualDependentBeans.isEmpty()) {
               throw new BeanCurrentlyInCreationException(beanName,
                     "Bean with name '" + beanName + "' has been injected into other beans [" +
                     StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                     "] in its raw version as part of a circular reference, but has eventually been " +
                     "wrapped. This means that said other beans do not use the final version of the " +
                     "bean. This is often the result of over-eager type matching - consider using " +
                     "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
            }
         }
      }
   }

   // Register bean as disposable.
   try {
      registerDisposableBeanIfNecessary(beanName, bean, mbd);
   }
   catch (BeanDefinitionValidationException ex) {
      throw new BeanCreationException(
            mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
   }

   return exposedObject;
}

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
   if (System.getSecurityManager() != null) {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
         @Override
         public Object run() {
            invokeAwareMethods(beanName, bean);
            return null;
         }
      }, getAccessControlContext());
   }
   else {
       //如果实现了BeanNameAware,BeanClassLoaderAware或者BeanFactoryAware,则调用对应的特殊方法
       //setBeanName/setBeanClassLoader/setBeanFactory
      invokeAwareMethods(beanName, bean);
   }

   Object wrappedBean = bean;
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   }

   try {
       //调用init方法,比如实现了InitializingBean则调用afterPropertiesSet方法;如果自定义了init方法,则反射调用
      invokeInitMethods(beanName, wrappedBean, mbd);
   }
   catch (Throwable ex) {
      throw new BeanCreationException(
            (mbd != null ? mbd.getResourceDescription() : null),
            beanName, "Invocation of init method failed", ex);
   }
   if (mbd == null || !mbd.isSynthetic()) {
       //这里执行各类bean前置处理器,此处就含有创建代理
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
   }
   return wrappedBean;
}

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization->
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
      return bean;
   }
   if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
      return bean;
   }
    //如果是基础类(Advice、Pointcut、Advisor、AopInfrastructureBean)则不需要代理
    //如果是Aspect类则不需要代理直接跳过
   if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
      this.advisedBeans.put(cacheKey, Boolean.FALSE);
      return bean;
   }

   //!!!创建代理
   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;
}

1.普通场景代理

org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean->
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
         //1.先找到所有的切面,主要是a.找到所有advisor定义b.创建advisorc.创建AspectJ的advisor
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
         //2.再找出匹配上的切面
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    	//3.看看是不是需要其他辅助切面【AspectJ】
		extendAdvisors(eligibleAdvisors);
    	//4.切面排序(@Order/@Priority)
		if (!eligibleAdvisors.isEmpty()) {
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}
1-c.为AspectJ创建对应的advisor

org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

protected List<Advisor> findCandidateAdvisors() {
   // a.找到所有advisor定义b.创建advisor
   List<Advisor> advisors = super.findCandidateAdvisors();
   // c.创建AspectJ的advisor
   advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
   return advisors;
}
c-I.先找到使用了@Aspect注解的类或者使用ajc编译的类

org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

c-II.创建普通Advisor及IntroductionAdvisor

org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisors

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
   Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
   String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
   validate(aspectClass);

   MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
         new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

   List<Advisor> advisors = new ArrayList<>();
    //找出该类的所有方法,包含其父类或接口的方法
   for (Method method : getAdvisorMethods(aspectClass)) {
      //为通知方法创建一个InstantiationModelAwarePointcutAdvisorImpl,如果是普通方法则返回null
      Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
      if (advisor != null) {
         advisors.add(advisor);
      }
   }

   // 如果切面需要懒加载,则在首位加入一个SyntheticInstantiationAdvisor
   if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
      Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
      advisors.add(0, instantiationAdvisor);
   }

   // 寻找类中使用@DeclareParents的字段,为之创建DeclareParentsAdvisor
   // @DeclareParents可用于AspectJ类的字段上,表示哪些方法需要整合其他方法代理【此处就是将两个毫无关系的类的方法建立关系】
   for (Field field : aspectClass.getDeclaredFields()) {
      Advisor advisor = getDeclareParentsAdvisor(field);
      if (advisor != null) {
         advisors.add(advisor);
      }
   }

   return advisors;
}
2.再找出匹配上的切面

org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findAdvisorsThatCanApply->

org.springframework.aop.support.AopUtils#findAdvisorsThatCanApply->

org.springframework.aop.support.AopUtils#canApply(org.springframework.aop.Advisor, java.lang.Class<?>, boolean)

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
   if (advisor instanceof IntroductionAdvisor) {
       // a.IntroductionAdvisor则根据类过滤器匹配
      return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
   }
   else if (advisor instanceof PointcutAdvisor) {
      PointcutAdvisor pca = (PointcutAdvisor) advisor;
       // b.PointcutAdvisor匹配切点
      return canApply(pca.getPointcut(), targetClass, hasIntroductions);
   }
   else {
      // c.其他则直接匹配成功
      return true;
   }
}
3.看看是不是需要其他辅助切面【AspectJ】

org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#extendAdvisors->

org.springframework.aop.aspectj.AspectJProxyUtils#makeAdvisorChainAspectJCapableIfNecessary

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) {
         //看是否包含AspectJ通知
         if (isAspectJAdvice(advisor)) {
            foundAspectJAdvice = true;
         }
      }
      if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
          //添加ExposeInvocationInterceptor,暴露当前的methodInvocation
         advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
         return true;
      }
   }
   return false;
}

2.根据名字代理

org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator#getAdvicesAndAdvisorsForBean
通过bean的名字来进行匹配,来创建特定代理。

切面执行代理

执行代理的入口

org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept

因为spring在高版本后,统一使用的Cglib代理,所以在调用方法时,会先为这个方法找到代理链路。然后每个链路依次嵌套调用,实现代理。

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
   Object oldProxy = null;
   boolean setProxyContext = false;
   Object target = null;
   TargetSource targetSource = this.advised.getTargetSource();
   try {
      if (this.advised.exposeProxy) {
         // Make invocation available if necessary.
         oldProxy = AopContext.setCurrentProxy(proxy);
         setProxyContext = true;
      }
      // 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);
       //1. 获取切面链路
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
      Object retVal;
      if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
          //没有切面则直接执行方法
         Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
         retVal = methodProxy.invoke(target, argsToUse);
      }
      else {
         //2. 创建MethodInvocation,并执行切面
         retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
      }
       //处理返回值
      retVal = processReturnType(proxy, target, method, retVal);
      return retVal;
   }
   finally {
      if (target != null && !targetSource.isStatic()) {
         targetSource.releaseTarget(target);
      }
      if (setProxyContext) {
         // Restore old proxy.
         AopContext.setCurrentProxy(oldProxy);
      }
   }
}

执行切面

@Override
@Nullable
public Object proceed() throws Throwable {
   // 最后一个切面的时候,如果是public方法(记录的methodProxy),则直接执行方法,否则通过反射的方式执行方法
   if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
      return invokeJoinpoint();
   }
   //每次递增使用的切面下标
   Object interceptorOrInterceptionAdvice =
         this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
   if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
      // 动态匹配【比如参数切点】
      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 {
         // 动态匹配失败,则略过本切面,执行下一个切面
         return proceed();
      }
   }
   else {
      // 依次调用切面
      return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
   }
}

代理顺序

切面的顺序是在创建切面的时候就已经给每个切面排好序了,但是每个Aspect的不同时期的通知是运行时拼接的。

拼接原理

不同的通知,invoke方法不一样,通过调整执行proceed的顺序,保证了各个通知最终能按照一定顺序执行。

AspectJAroundAdvice

直接执行通知方法,所以around在before之前

public Object invoke(MethodInvocation mi) throws Throwable {
   if (!(mi instanceof ProxyMethodInvocation)) {
      throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
   }
   ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
   ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
   JoinPointMatch jpm = getJoinPointMatch(pmi);
   return invokeAdviceMethod(pjp, jpm, null, null);
}
MethodBeforeAdviceInterceptor

在MethodInvocation执行proceed之前执行before通知

public Object invoke(MethodInvocation mi) throws Throwable {
   this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
   return mi.proceed();
}
AspectJAfterThrowingAdvice

在catch块执行afterThrowing通知

public Object invoke(MethodInvocation mi) throws Throwable {
   try {
      return mi.proceed();
   }
   catch (Throwable ex) {
      if (shouldInvokeOnThrowing(ex)) {
         invokeAdviceMethod(getJoinPointMatch(), null, ex);
      }
      throw ex;
   }
}
AfterReturningAdviceInterceptor

在执行MethodInvocation之后,返回结果前,执行afterReturning通知

public Object invoke(MethodInvocation mi) throws Throwable {
   Object retVal = mi.proceed();
   this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
   return retVal;
}
AspectJAfterAdvice

在finally块执行after通知

public Object invoke(MethodInvocation mi) throws Throwable {
   try {
      return mi.proceed();
   }
   finally {
      invokeAdviceMethod(getJoinPointMatch(), null, null);
   }
}
通知时机顺序总结

按照上述5个通知的执行原理,可以得到以下顺序图。
在这里插入图片描述

相关知识点

ExposeInvocationInterceptor

在spring寻找切面的时候,会经过org.springframework.aop.aspectj.AspectJProxyUtils#makeAdvisorChainAspectJCapableIfNecessary,看是否需要为AspectJ添加一个辅助类——ExposeInvocationInterceptor。

这个特殊切面是用于存储当前的invocation,供AspectJ使用,比如在使用before方法切入,则此时不会直接传入一个invocation,需要通过ExposeInvocationInterceptor来获取当前的invocation。当切面优先级为最高级,则ExposeInvocationInterceptor的会报错,因为没有过这个切面,没有往invocation放入过当前线程的值;由于invocation是threadlocal变量,故而不允许跨线程调用,一旦跨线程也会报错。

那为什么需要ExposeInvocationInterceptor?

因为AspectJ有很多切的时机,除了around的入参是ProceedingJoinPoint以外,其他方法入参都是JointPoint,ProceedingJoinPoint和JoinPoint的区别是前者暴露了MethodInvocation的proceed方法,可以执行方法。所以对于其他时机的切面来说,无法直接获取当前的MethodInvocation,需要通过一个辅助类来帮忙。

PointcutAdvisor和IntroductionAdvisor

Advisor有许多实现,其中PointcutAdvisor和IntroductionAdvisor是主要的两个实现

  • PointcutAdvisor是基于切点的,适用范围很广,并且切点很精确
  • IntroductionAdvisor是使用于类的,目的是在不修改java文件的情况下,使得某个类具有特殊接口

Pointcut表达式

pointcut表达式十分丰富,从对参数、方法、类、包到注解等都有对应的表达式。多个pointcut之间可以用|| , &&, !进行组合。

args和@args

args:匹配参数;@args匹配含有某些注解的参数

案例:

场景表达式
没有参数args()
第一个参数是User类型args(test.User,…)
任意参数args(…)
含有具有@RequestBody的参数@args(org.springframework.web.bind.annotation.RequestBody)

execution和@annotation

execution:匹配方法,格式:访问标识符 返回值类型 包路径.类名.方法名(参数类型)

@annotation:匹配具有某些注解的方法

案例:

场景表达式
所有方法execution(* *(…))
User类的方法execution(public * test.User.*(…))
User类最后一个参数是int类型的方法execution(public * test.User.*(…,int))

within和@within

within:指定包含的类范围;@within:指定具有某些注解的类

案例:

场景表达式
某个包下所有类方法within(test.*)
具体类的所有方法within(test.User)
使用@Aspect的类的所有方法@within(org.aspectj.lang.annotation.Aspect)

this

匹配代理后的对象的所有方法

案例:

场景表达式
代理后的对象是User类型this(test.User)

target和@target

target:匹配代理前的对象的所有方法;@target:匹配代理前的对象的类得使用某些注解

案例:

场景表达式
代理前的对象是User类型target(test.User)
代理前的对象的类使用了@Aspect注解@target(org.aspectj.lang.annotation.Aspect)

Spring aop

Spring的动态代理最终是通过 Cglib和jdk动态代理实现的,分别是为类、接口产生代理类。两者均是在运行时产生代理类,**所以如果是有大量类需要代理时,要考虑下生成的代理类所占空间大小。**因为Cglib是通过字节码技术实现,所以在调用方法时,效率会更高。

jdk代理示例

代理类实现InvocationHandler
总接口:Person

public interface Person {
    void printName();
}

实现类:User

@Data
public class User implements Person{
    String city;
    String name;

    @Override
    public void printName() {
        System.out.println(name);;
    }
}

代理类:UserProxy

@Slf4j
public class UserProxy implements InvocationHandler {
    User user;

    public UserProxy(User user) {
        this.user = user;
    }

    public static Object newProxy(User user) {
        UserProxy proxy = new UserProxy(user);
        return Proxy.newProxyInstance(UserProxy.class.getClassLoader(), User.class.getInterfaces(), proxy);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("准备执行{}方法", method.getName());
        return method.invoke(this.user, args);
    }
}

测试代码:

    public static void main(String[] args) {
        User user = new User();
        user.setName("jdk代理");
        Person person = (Person) UserProxy.newProxy(user);
        person.printName();
    }

测试结果

18:56:18.166 [main] INFO cn.dotfashion.soa.CacheDemo.jdk.UserProxy - 准备执行printName方法
jdk代理

cglib代理示例

代理类实现
实体类:User。注意,此类可以不实现接口,因为Cglib是用继承该类User的方式实现。

@Data
public class User implements Person {
    String city;
    String name;

    @Override
    public void printName() {
        System.out.println(name);;
    }
}

代理类:UserProxy

@Slf4j
public class UserProxy implements MethodInterceptor {
    User user;

    public UserProxy(User user) {
        this.user = user;
    }

    public static Object newProxy(User user) {
        UserProxy userProxy = new UserProxy(user);
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(user.getClass());
        enhancer.setCallback(userProxy);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log.info("准备执行{}方法", method.getName());
        return method.invoke(this.user, args);
    }
}

测试代码:


    public static void main(String[] args) {
        User user = new User();
        user.setName("cglib代理");
        User pp = (User) UserProxy.newProxy(user);
        pp.printName();
    }

测试结果:

19:00:30.452 [main] INFO cn.dotfashion.soa.CacheDemo.cglib.UserProxy - 准备执行printName方法
cglib代理
JDK代理Cglib代理
代理类必须实现接口无要求
实现方式通过实现接口,只会对接口的方法进行代理(public/default),生成代理类,然后生成代理对象(Proxy.newProxyInstance),多接口场景更倾向使用jdk代理通过继承代理类,生成子类,覆盖被代理类的方法(非private),通过子类代理对象实现(Enhancer.create)
代理类实现的接口InvocationHandlerMethodInterceptor
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值