spring-第十七篇之spring AOP基于注解的零配置方式

1、基于注解的零配置方式

     Aspect允许使用注解定义切面、切入点和增强处理,spring框架可以识别并根据这些注解来生成AOP代理。spring只是用了和AspectJ 5一样的注解,但并没有使用AspectJ的编译器或者织入器,底层依然使用的是spring AOP。

    为了启用spring对@Aspect切面配置的支持,并保证spring容器中的目标bean被一个或多个切面自动增强,必须在spring配置文件中加入以下代码:

    引入命名空间:xmlns:aop="http://www.springframework.org/schema/aop"

    引入元素:

           http://www.springframework.org/schema/aop 
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

     启用对@Aspect注解的支持:<aop:aspectj-autoproxy/>

     如果不打算使用spring的XML Schema配置方式,则应该在spring配置文件中增加如下代码来启用@AspectJ的支持。

<!-- 完全不打算使用spring的xml schema配置方式使用spring AOP,即bean的注册也使用注解的形式。
                                   应该加入如下代码 ,AnnotationAwareAspectJAutoProxyCreator是一个bean后处理器,该bean
                                   后处理器将会为容器中的所有bean生成AOP代理
       -->
       <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/> 

     为了在spring应用中启动@AspectJ的支持,还需要在应用的类加载路径下增加两个AspectJ库:aspectjweaver.jar和aspectjrt.jar,除此之外,spring AOP还依赖aopalliance.jar。

2、定义切面bean

     使用@Aspect注解切面bean。

3、定义切入点

     1》切入点的定义包含两部分:

        1>定义切入点表达式:指定与哪些方法匹配。切入点表达式使用@Pointcut注解标注。

        2>定义名字和任意参数的方法签名:方法签名方法体通常为空,返回值必须为void。签名方法有访问控制符,如private只能是本切入面中使用该切入点,与Java的访问控制符意义差不多。

        切入点的定义:

package com.lfy.aspect;

import org.aspectj.lang.annotation.Pointcut;

/**
 * 切入点组合连接符有&&、||、!
 * @author lfy
 *
 */
public class PointcutUtil {

    //使用@Pointcut注解定义切入点,public控制着切入点的访问权限
    @Pointcut("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..))")
    public void myPointcut() {}
}

       使用举例:

@Before("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void authority(JoinPoint jp) {
        
        System.out.println("FourAdviceGetParamerAspect.Before模拟执行权限检查");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }

     2》切入点指示符

          定义切入点的时候,execution就是一个切入点指示符。spring AOP仅支持部分AspectJ的切入点指示符。spring AOP支持的切入点指示符有:

         1>excution:用于匹配执行方法的连接点。其表达式格式如下。

           excution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?),对各部分的解析如下。

              modifiers-pattern:指定方法的修饰符,支持通配符,该部分可以省略。

              ret-type-pattern:指定方法的返回值类型,支持通配符,可以使用“*”通配符来匹配所有的返回值类型。

              declaring-type-pattern:指定方法所属的类,支持通配符,该部分可以省略。

              name-pattern:指定匹配指定的方法名,支持通配符,可以使用“*”通配符来匹配所有方法。

              param-pattern:指定方法声明中的形参列表,支持两个通配符,即“*”和“..”,其中“*”代表一个任意类型的参数,“..”代表0个或多个任意类型的参数。

              throws-pattern:指定目标方法声明抛出的异常类型,支持通配符,该部分可以省略。

         2>within:用于限定匹配特定类型的连接点,当使用spring AOP的时候,只匹配方法执行的连接点(spring AOP支持方法连接)。

//在com.lfy.bean包中的任意连接点
within(com.lfy.bean.*)
//在com.lfy.bean包或其子包中的任意连接点
within(com.lfy.bean..*)

         3>this:用于限定AOP代理必须是指定类型的实例,匹配该对象的所有连接点。当使用spring AOP的时候,只能匹配方法执行的连接点(spring AOP支持方法连接)。

//匹配实现了com.lfy.bean.Person接口的AOP代理的所有连接点
//在spring AOP中只是方法执行的连接点
this(com.lfy.bean.Person)

         4>target:用于限定目标对象必须是指定类型的实例,匹配该对象的所有连接点。当使用spring AOP的时候,只能匹配方法执行的连接点(spring AOP支持方法连接)。

//匹配实现了com.lfy.bean.Person接口的目标对象的所有连接点
//在spring AOP中只是方法执行的连接点
target(com.lfy.bean.Person)

         5>args:用于对连接点的参数类型和数量进行限制,要求参数类型和数量是指定的数量和类型的实例。当使用spring AOP的时候,只能匹配方法执行的连接点(spring AOP支持方法连接)。

//匹配只接受一个参数,且传入的参数类型是Serializable的所有连接点
//在spring AOP中只是方法执行的连接点
args(java.io.Serializable)

          6>bean:spring AOP提供的限制只匹配指定bean实例内的连接点(spring AOP支持方法连接)。定义bean表达式时需要传入bean的id或name,表示只匹配该bean实例内的连接点,支持使用“*”通配符。

//匹配com.lfy.impl.Chinese bean实例内方法执行的连接点
bean(com.lfy.impl.Chinese)
//匹配名字以Impl结尾的bean实例内方法执行的连接点
bean(*impl)

     3》组合切入点表达式

         spring支持使用如下3个逻辑运算符来组合切入点表达式:

             1>&&:要求连接点同时匹配两个切入点表达式,即切入点表达式做逻辑与。

             2>||:连接点匹配任意一个表达式,即表达式逻辑或。

             3>!:连接点不匹配指定的切入点表达式,即切入点表达式逻辑非。

    @Before("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void authority(JoinPoint jp) {
        
        System.out.println("FourAdviceGetParamerAspect.Before模拟执行权限检查");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }

4、定义增强处理

    1》Before增强处理

        使用@Before注解一个切面方法,该方法作为Before增强处理,可以指定一个value属性,value属性的值就是切入点(可以是一个已有的切入点,也可以是表达式)。Before增强处理在目标方法前执行,不会影响目标方法的执行,除非Before增强抛异常了,则会阻止目标方法的执行

    @Before("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void authority(JoinPoint jp) {
        
        System.out.println("FourAdviceGetParamerAspect.Before模拟执行权限检查");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }

    2》AfterReturning增强处理

        使用@AfterReturning注解一个切面方法,AfterReturning增强处理将在目标方法正常完成后被织入。可以访问到目标方法的返回值,但不可以修改目标方法的返回值。

/**
     * 该注解可以指定pointcut/value,returning这两个常用的属性:<br>
     * 1>>pointcut/value:用于指定切入点表达式,可以是已有的切入点,也
     * 可以直接指定表达式。pointcut指定的值会覆盖value指定的值。<br>
     * 2>>returning:指定一个形参名,用于表示增强处理方法中可定义与此同名
     * 的形参,该形参可用于访问目标方法的返回值。除此之外,在增强处理方法上定义
     * 的该形参(代表目标方法的返回值)指定的类型,将限制目标方法必须返回该类型
     * 的值或没有返回值。
     * @param rvt
     * 匹配com.lfy.impl包下的所有类所有方法的执行作为切入点
     * 声明rvt时指定的类型会限制目标方法必须返回指定类型的值或没有返回值
     * 此处将rvt的类型声明为Object,意味着对目标方法的返回值不加限制,
     * 即目标方法可以是void返回类型,也可以是Object的所有子类,即无限制
     */
    @AfterReturning(returning="rvt",pointcut="execution(* com.lfy.impl.*.*(..))")
    public void log(Object rvt) {
        
        System.out.println("AfterReturning获取目标方法返回值: "+rvt);
        System.out.println("AfterReturning模拟记录日志功能...");
    }

    3》AfterThrowing增强处理

        使用@AfterThrowing注解一个切面方法,AfterThrowing用于处理未处理的异常。

/**
     * 
     * 该注解可以指定pointcut/value,returning这两个常用的属性:<br>
     * 1>>pointcut/value:用于指定切入点表达式,可以是已有的切入点,也
     * 可以直接指定表达式。pointcut指定的值会覆盖value指定的值。<br>
     * 2>>returning:指定一个形参名,用于表示增强处理方法中可定义与此同名
     * 的形参,该形参可用于访问目标方法抛出的异常。除此之外,在增强处理方法上定义
     * 的该形参(代表目标方法抛出的异常)指定的类型,将限制目标方法必须抛出该类型
     * 的异常。
     * @param ex
     * 匹配com.lfy.impl包下的所有类所有方法的抛异常作为切入点
     * 声明ex时指定的类型会限制目标方法必须抛出指定类型的异常
     * 此处将ex的类型声明为Throwable,意味着对目标方法抛出的异常不加限制。<br>
     * 
     * @说明:该增强处理能够处理该异常,但不能完全处理该异常,异常还将在目标函数往父级
     * 传递。catch异常捕获则是完全处理。<br>
     */
    @AfterThrowing(throwing="ex",pointcut="execution(* com.lfy.impl.*.*(..))")
    public void doRecoveryActions(Throwable ex) {
        
        System.out.println("AfterThrowing目标方法中抛出的异常:"+ex);
        System.out.println("AfterThrowing模拟增强处理对异常的修复业务...");
    }

    4》After增强处理

        使用@After注解切面方法

package com.lfy.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;

/**
 * After增强处理
 * @author lfy
 * 与AfterReturning的区别:AfterReturning增强处理只有在目标方法成功完成后才会被织入,
 * After增强处理无论目标方法如何结束(包括成功完成和遇到异常中止),它都会被织入。
 * After需要处理正常返回和异常返回两种情况。After增强处理需要指定一个value属性,用于指定已
 * 有的切入点,或者切入点表达式。
 */
@Aspect
public class AfterAspect {

    @After("execution(* com.lfy.impl.*.*(..))")
    public void release() {
        
        System.out.println("After模拟方法结束后的释放资源...");
    }
}

    5》Around增强处理

       使用@Around注解切面方法。Around的功能近似是Before增强处理和AfterReturning增强处理的总和,它既可以在目标方法前织入增强处理,也可以在执行目标方法后织入增强处理。

       目标方法作为它的一个回调,不是必须的,也就是增强处理完全可以阻止目标方法的执行。

       还可以修改目标方法的入参,或者改变目标方法执行后的返回值

       它不是一个线程安全的增强处理,通常需要保证它在线程安全的环境下执行。

       使用时指定的value值可以是一个已有的切入点,也可以是一个切入点表达式。

       当定义一个Around增强处理方法时,该方法的第一个参数必须是ProceedingJoinPoint类型(至少包含一个参数),在增强处理方法体内,调用ProceedingJoinPoint参数的proceed()方法才会执行目标方法,proceed()相当于就是目标方法,proceed()方法没有被调用,则目标方法不会被执行。调用proceed()方法还可以传入一个Object[]对象作为参数,相当于给目标方法传递实参。

package com.lfy.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

/**
 * Around增强处理
 * @author lfy
 *
 */
@Aspect
public class AroundAspect {

    /**
     * 
     * @return Object
     * @throws Throwable 
     * 
     */
    @Around("execution(* com.lfy.impl.*.*(..))")
    public Object processTx(ProceedingJoinPoint jp) throws Throwable {
        
        System.out.println("Around执行目标方法之前,模拟开始事务...");
        //获取目标方法原始的调用参数
        Object[] args=jp.getArgs();
        if(args!=null&&args.length>1) {
            //修改目标方法调用参数的第一个参数
            args[0]="[增强的前缀]"+args[0];
        }
        //改变后的参数去执行目标方法,并保存目标方法执行后的返回值
        Object rvt=jp.proceed(args);
        System.out.println("Around执行目标方法之后,模拟结束事务...");
        //如果rvt的类型是Integer,将rvt改为它的平方
        if(rvt!=null&&rvt instanceof Integer) {
            rvt=(Integer)rvt*(Integer)rvt;
        }
        return rvt;
    }
}

      传入的参数Object[]数组长度与目标方法所需要参数的个数不相等,或者类型不匹配,都会引发异常。为了能够获取目标方法的参数的个数和类型,需要增强处理方法能够访问目标方法的参数。

      6》访问目标方法的参数

        访问目标方法的参数,最简单的做法是定义增强处理方法第一个参数定义为JoinPoint类型(ProceedingJoinPoint继承于JoinPoint),当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点,JonPoint包含了如下常用方法:

        1>Object[] getArgs():返回执行目标方法的参数。

        2>Signature getSignature():返回被增强的方法的相关信息。

        3>Object getTarget():返回被织入增强处理的目标对象。

        4>Object getThis():返回AOP框架为目标对象生成的代理对象。

      举个例子:

      

     FourAdviceGetParamerAspect.java

package com.lfy.aspect;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Around、Before、After、AfterReturning四种增强处理<br>
 * 访问目标方法最简单的做法是定义增强处理方法时将第一个参数定义为JoinPoint类型,
 * 当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点。JoinPoint
 * 提供了如下的常用访问方法:<br>
 *   1>Objeact[] getArgs():返回目标方法的参数<br>
 *   2>Signature getSignature():返回目标方法的相关信息<br>
 *   3>Object getTarget():返回被增强处理的对象<br>
 *   4>Object getThis():返回AOP框架为目标对象生成的代理对象。<br>
 * 在这些方法中,只有Around增强处理中可以对目标方法的参数进行修改!<br>
 * @author lfy
 *
 * 切入点组合连接符有&&、||、!
 */
@Aspect
public class FourAdviceGetParamerAspect {

    @Around("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public Object processTx(ProceedingJoinPoint jp) throws Throwable {
        
        System.out.println("FourAdviceGetParamerAspect.Around执行目标方法之前,模拟开始事务...");
        //获取目标方法原始的调用参数
        Object[] args=jp.getArgs();
        if(args!=null && args.length>0 && args[0].getClass() == String.class) {
            //修改目标方法调用参数的第一个参数
            args[0]="[增强的前缀]"+args[0];
        }
        //改变后的参数去执行目标方法,并保存目标方法执行后的返回值
        Object rvt=jp.proceed(args);
        System.out.println("FourAdviceGetParamerAspect.Around执行目标方法之后,模拟结束事务...");
        System.out.println("<---------------->");
        //如果rvt的类型是Integer,将rvt改为它的平方
        if(rvt!=null&&rvt instanceof Integer) {
            rvt=(Integer)rvt*(Integer)rvt;
        }
        return rvt;
    }
    
    @Before("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void authority(JoinPoint jp) {
        
        System.out.println("FourAdviceGetParamerAspect.Before模拟执行权限检查");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }
    
    @After("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void release(JoinPoint jp) {
        
        System.out.println("FourAdviceGetParamerAspect.After模拟方法结束后的释放资源...");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }
    
    @AfterReturning(returning="rvt",pointcut="execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..)) || PointcutUtil.myPointcut()")
    public void log(JoinPoint jp,Object rvt) {
        
        System.out.println("FourAdviceGetParamerAspect.AfterReturning获取目标方法返回值: "+rvt);
        System.out.println("FourAdviceGetParamerAspect.AfterReturning模拟记录日志功能...");
        //返回被织入增强的目标方法
        System.out.println("被织入的目标方法是: "+jp.getSignature().getName());
        System.out.println("被织入的目标方法的参数: "+Arrays.toString(jp.getArgs()));
        System.out.println("被织入增强处理的目标对象为: "+jp.getTarget());
        System.out.println("<---------------->");
    }
}

     PointcutUtil.java

package com.lfy.aspect;

import org.aspectj.lang.annotation.Pointcut;

/**
 * 切入点组合连接符有&&、||、!
 * @author lfy
 *
 */
public class PointcutUtil {

    //使用@Pointcut注解定义切入点,public控制着切入点的访问权限
    @Pointcut("execution(int com.lfy.impl.HelloImpl.fourAdviceGetParamer(..))")
    public void myPointcut() {}
}

     beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- spring配置文件的根元素,使用spring-beans-4.0.xsd语义约束 -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd  
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
      
       <!-- 指定自动搜索bean组件、自动搜索切面类 -->
       <context:component-scan base-package="com.lfy.aspect,com.lfy.impl">
           <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
       </context:component-scan>
       
       
       <!-- 或者不打算使用spring的xml schema配置方式,则应该在spring配置文件中增加如下片段
                                来启动@AspectJ的支持  
           <bean class="org.springframework.aop.aspectj.annotation.
                        AnnotationAwareAspectJAutoProxyCreator"/>
                                为了在spring应用中启动@AspectJ的支持,还需要在应用的类加载路径下增加两个AspectJ库:
           aspectjweaver.jar和aspectjrt.jar,除此之外,spring AOP还依赖aopalliance.jar
       -->
       <!-- 启动@AspectJ支持  -->
       <aop:aspectj-autoproxy/>
       
       
       <!-- 完全不打算使用spring的xml schema配置方式使用spring AOP,即bean的注册也使用注解的形式。
                                   应该加入如下代码 ,AnnotationAwareAspectJAutoProxyCreator是一个bean后处理器,该bean
                                   后处理器将会为容器中的所有bean生成AOP代理
       -->
       <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>
</beans>

    HelloImpl.java

package com.lfy.impl;

import org.springframework.stereotype.Component;

import com.lfy.bean.Hello;

/**
 * 被增强的目标类
 * @author lfy
 *
 */
@Component("hello")
public class HelloImpl implements Hello {

    @Override
    public void foo() {
        
        System.out.println("执行Hello组件的foo()方法");
    }

    @Override
    public void addUser(String name, String pass) {
        
        System.out.println("执行Hello组件的addUser()添加用户: "+name+",pass为"+pass);
    }

    @Override
    public int addGroup(String groupName, int groupMemberNumber) {
        
        System.out.println("执行Hello组件的addGroup()添加群组: "+groupName);
        if(groupMemberNumber<0||groupMemberNumber>100) {
            throw new IllegalArgumentException("群组成员数在0~100之间");
        }
        return 0;
    }

    @Override
    public int fourAdviceGetParamer(String param) {
        
        System.out.println("执行Hello组件的fourAdviceGetParamer()方法,param:"+param);
        return 5;
    }

}

     SpringAOPTest.java

package com.lfy.main;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.lfy.bean.Hello;

/**
 * 1、基于注解的“零配置”方式,演示获取切入点信息
 * @author lfy
 * 未登记的知识点:
 *   1>指定增强处理的优先级,@Order注解及Orderd接口
 *   2>切入点指示符
 */
public class SpringAOPTest {

    public static void main(String[] args) {
        
        ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
        Hello hello=ctx.getBean("hello", Hello.class);
        hello.addUser("孙悟空", "7788");
        System.out.println("****************\n");
        System.out.println("fourAdviceGetParamer默认只返回5,经过平方后:"+hello.fourAdviceGetParamer("唐僧"));
    }

}

    运行结果:各方法内成功的获取到了切入点相关信息。Around增强处理修改了目标方法的入参、返回值,其实也只有Around增强可以修改并生效(影响)到目标方法。

     7》如果只需要访问目标方法的参数

          可以在程序中用args切入点表达式来绑定目标方法的参数。如果一个args表达式中指定了一个或多个参数,则该切入点将只匹配具有对应形参的方法(包括参数个数以及参数类型),且目标方法的参数值将被传入增强处理方法。

@AfterReturning(returning="rvt",pointcut="execution(* com.lfy.impl.*.*(..)) && args(arg0,arg1)")
public void log(Object rvt,String arg0,String arg1) {
    ...
}

      上面的切入点表达式增加了&&args(arg0,arg1)部分,且log()方法还指定了它们是String类型,则该增强处理只匹配参数是两个个且均为String类型的目标方法。

      如果表达式args部分为&&args(name,age, .. ),注意最后面是两点,这说明最少匹配两个参数(包括增强处理指定的类型),第三个参数可以是0到无限多个,且类型由增强处理决定,如:

@AfterReturning(returning="rvt",pointcut="execution(* com.lfy.impl.*.*(..)) && args(arg0,arg1,..)")
public void log(Object rvt,String arg0,String arg1,Date date) {
    ...
}

 

转载于:https://www.cnblogs.com/ZeroMZ/p/11335839.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值