你不知道的Spring之AOP的那些秘密

注意:这个笔记很少提到 Spring 源码,基本上都是基于源码对功能的模拟实现

AOP 实现

ajc 编译器

  1. 使用编译器修改 class 源码实现增强,不依赖于 Spring
  2. 编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强

注意

  • 需要添加 aspectj-maven-plugin 插件依赖
  • 要用 maven 的 compile 来编译再运行,因为idea 不会调用 ajc 编译器

agent 类加载

  1. 运行时通过类加载的 agent 修改 class 源码实现增强,不依赖于 Spring
  2. 运行时需要在 VM options 里加入 -javaagent:本地maven仓库地址/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar

proxy

jdk 动态代理使用

示例代码如下:

public class JdkProxyDemo {
    
    interface Foo {
        void foo();
    }
    
    static class Target implements Foo {
        public void foo() {
            System.out.println("target foo");
        }
    }
    
    // jkd 代理:只能针对接口代理
    // cglib 代理
    public static void main(String[] param) {

        //目标对象
        Target target = new Target();

        //类加载器:加载运行时代理类生成的字节码
        ClassLoader loader = JdkProxyDemo.class.getClassLoader();
        Foo proxy = (Foo) Proxy.newProxyInstance(loader, new Class[]{Foo.class}, (p, method, args) -> {
            System.out.println("before....");
            Object result = method.invoke(target, args);
            System.out.println("after....");
            return result;
        });
        proxy.foo();
    }
}

总结:jdk 动态代理要求目标类必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系

cglib 动态代理使用

示例代码如下:

public class CglibProxyDemo {

    static class Target {
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) {
        Target target = new Target();

        Target proxy = (Target) Enhancer.create(Target.class, new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("before...");
                Object result = method.invoke(target, args); //方法反射调用目标
                Object result1 = methodProxy.invoke(target, args);// 内部没有用反射,需要目标对象 (Spring)
                Object result2 = methodProxy.invokeSuper(proxy, args);// 内部没有用反射,需要代理对象
                System.out.println("after...");
                return result;
            }
        });

        proxy.foo();
    }
}

总结:

  • 代理是子类型,目标是父类型
  • 目标类不能为 final,因为 final 类不能被继承
  • 目标类方法不能为 final,因为 final 方法不能被重写
  • methodProxy 可以避免反射调用

jdk 动态代理原理

模拟 jdk 动态代理

流程如下:

  1. 定义代理类实现和目标类相同的接口
  2. 定义 InvocationHandler 接口
  3. InvocationHandler 接口作为参数,创建代理类对象
  4. 执行代理类对象的方法,拿到 Method 对象后回调 InvocationHandler 接口
  5. InvocationHandler 匿名内部类中通过 Method 对象调用目标对象方法,并做一些增强

模拟代码如下:

模拟 InvocationHandler 接口:

interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

模拟创建代理对象过程:

public class A13 {

    interface Foo {
        void foo();
        int bar();
    }

    //目标类
    static class Target implements Foo {
        public void foo() {
            System.out.println("target foo");
        }

        @Override
        public int bar() {
            System.out.println("target bar");
            return 100;
        }
    }
	
    public static void main(String[] args) {
        //模拟创建代理对象过程
        Foo proxy = new $Proxy0(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
                // 1.功能增强
                System.out.println("before...");
                // 2.调用目标
                return method.invoke(new Target(), args);
            }
        });
        proxy.foo();
        proxy.bar();
    }
}

模拟代理类:

// 模拟代理类
public class $Proxy0 implements Foo {

    private InvocationHandler h;

    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    static Method foo;
    static Method bar;

    static {
        try {
            foo = Foo.class.getMethod("foo");
            bar = Foo.class.getMethod("bar");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    @Override
    public void foo() {
        try {
            h.invoke(this, foo, new Object[0]);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public int bar() {
        try {
            Object result = h.invoke(this, bar, new Object[0]);
            return (int) result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

🍭扩展

ASM 是一个 Java 字节码操控框架,可以修改现有的字节码,或是在运行期间动态生成字节码(.class),没有源码(.java)

先做了解,后面学到 JVM 后再来深入。

cglib 动态代理原理

模拟 cglib 动态代理

和 jdk 动态代理原理类似,流程如下:

  1. 在代理类定义接口属性 methodInterceptor
  2. 在外部类设置 methodInterceptor 匿名内部类,即回调方法(增强)
  3. 执行代理对象的方法,拿到 Method 对象后回调 methodInterceptor 接口
  4. methodInterceptor 匿名内部类中通过 Method 对象 或 MethodProxy 对象 调用目标对象方法,并做一些增强

模拟代码在👇(MethodProxy 应用)

区别:

  • 回调的接口换了,InvocationHandler 改成了 MethodInterceptor
  • 调用目标接口时有所改进:
    • method.invoke反射调用,必须调用到足够次数才会进行优化
    • methodProxy.invoke不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)
    • methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象

MethodProxy

创建 MethodProxy
MethodProxy.create(Class c1, Class c2, String desc, String name1, String name2) //创建一个 MethodProxy 对象
  1. c1:目标类型
  2. c2:代理类型
  3. desc:参数和返回值,例如 ()V 代表无参无返回值
  4. name1:带增强功能的方法名
  5. name2:带原始功能的方法名
MethodProxy 应用

模拟代码如下:

模拟创建代理对象过程:

//目标类
static class Target{
    public void save() {
        System.out.println("save()");
    }
    public void save(int i) {
        System.out.println("save(int)");
    }
    public void save(long j) {
        System.out.println("save(long)");
    }
}

public static void main(String[] args) {
        Proxy proxy = new Proxy();
        Target target = new Target();

        proxy.setMethodInterceptor(new MethodInterceptor() {
            @Override
            public Object intercept(Object p, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("before....");
//                return method.invoke(target, args); //反射调用
//                return methodProxy.invoke(target, args); //内部无反射,结合目标对象使用
                return methodProxy.invokeSuper(p, args); //内部无反射,结合代理使用
            }
        });
        proxy.save();
        proxy.save(1);
        proxy.save(2L);
    }

模拟代理类:

public class Proxy extends Target{

    private MethodInterceptor methodInterceptor;

    public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
        this.methodInterceptor = methodInterceptor;
    }

    static Method save0;
    static Method save1;
    static Method save2;
    static MethodProxy save0Proxy;
    static MethodProxy save1Proxy;
    static MethodProxy save2Proxy;
    static {
        try {
            save0 = Target.class.getMethod("save");
            save1 = Target.class.getMethod("save", int.class);
            save2 = Target.class.getMethod("save", long.class);
            save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V", "save", "savaSuper");
            save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V", "save", "savaSuper");
            save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V", "save", "savaSuper");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>带原始功能的方法
    public void savaSuper(){
        super.save();
    }
    public void savaSuper(int i){
        super.save(i);
    }
    public void savaSuper(long j){
        super.save(j);
    }

    // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>带增强功能的方法
    @Override
    public void save() {
        try {
            methodInterceptor.intercept(this, save0, new Object[0], save0Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public void save(int i) {
        try {
            methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public void save(long j) {
        try {
            methodInterceptor.intercept(this, save2, new Object[]{j}, save2Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}
MethodProxy 原理
methodProxy.invoke(target, args); //结合目标对象使用
methodProxy.invokeSuper(proxy, args); //结合代理对象使用

通过两个方法避免反射调用

methodProxy 产生的两个代理类和之前的代理类的区别:

  • 之前的代理类是给目标类做增强
  • 产生的两个代理类是为了避免两个方法的反射调用
  • 产生的两个代理类的父类都是 FastClass

所以以下把产生的两个代理类称之为 FastClass,避免混淆

产生的 FastClass ,一个配合目标对象使用,另一个配合代理对象使用

模拟两个 FastClass(没有去继承 FastClass 抽象类是因为里面方法太多了,这里模拟两个核心方法):

调用 TargetFastClass 对象的流程如下:

  1. MethodProxy.create() 时,底层会创建 TargetFastClass 对象,并确定好每个增强方法对应的编号
  2. TargetFastClass 类中通过 getIndex 得到每一个方法的编号
  3. 调用 methodProxy.invoke(target, args) 时,底层的是调用 TargetFastClassinvoke 方法

模拟 TargetFastClass 类:

public class TargetFastClass {
    static Signature s0 = new Signature("save", "()V");
    static Signature s1 = new Signature("save", "(I)V");
    static Signature s2 = new Signature("save", "(J)V");

    //获取目标方法的编号
    /*
        Target
            save()         0
            save(int)      1
            save(long)     2
         signature:包含方法名字,返回参数
     */
    public int getIndex(Signature signature) {
        if (s0.equals(signature)) {
            return 0;
        } else if (s1.equals(signature)) {
            return 1;
        } else if (s2.equals(signature)) {
            return 2;
        } else {
            return 0;
        }
    }

    // 根据方法编号,正常(不走反射)调用目标对象方法
    public Object invoke(int index, Object target, Object[] args) {
        if (index == 0) {
            ((Target) target).save();
            return null;
        } else if (index == 1) {
            ((Target) target).save((int) args[0]);
            return null;
        } else if (index == 2) {
            ((Target) target).save((long) args[0]);
            return null;
        }  else {
            throw new RuntimeException("无此方法");
        }
    }
}

模拟调用 TargetFastClass 对象流程:

@Test
public void test(){
    TargetFastClass fastClass = new TargetFastClass();
    int index = fastClass.getIndex(new Signature("save", "(I)V"));
    System.out.println(index); //输出1
    fastClass.invoke(index, new Target(), new Object[]{100});
}

调用 ProxyFastClass 对象的流程和 TargetFastClass 大体一致,代码如下:

模拟 ProxyFastClass 类:

public class ProxyFastClass {

    static Signature s0 = new Signature("savaSuper", "()V");
    static Signature s1 = new Signature("savaSuper", "(I)V");
    static Signature s2 = new Signature("savaSuper", "(J)V");

    //获取代理方法的编号
    /*
        Proxy
            savaSuper()         0
            savaSuper(int)      1
            savaSuper(long)     2
         signature:包含方法名字,返回参数
     */
    public int getIndex(Signature signature) {
        if (s0.equals(signature)) {
            return 0;
        } else if (s1.equals(signature)) {
            return 1;
        } else if (s2.equals(signature)) {
            return 2;
        } else {
            return 0;
        }
    }

    // 根据方法编号,正常(不走反射)调用目标对象方法
    public Object invoke(int index, Object proxy, Object[] args) {
        if (index == 0) {
            ((Proxy) proxy).savaSuper();
            return null;
        } else if (index == 1) {
            ((Proxy) proxy).savaSuper((int) args[0]);
            return null;
        } else if (index == 2) {
            ((Proxy) proxy).savaSuper((long) args[0]);
            return null;
        }  else {
            throw new RuntimeException("无此方法");
        }
    }
}

模拟调用 ProxyFastClass 对象流程:

@Test
public void test(){
    ProxyFastClass fastClass = new ProxyFastClass();
    int index = fastClass.getIndex(new Signature("saveSuper", "()V"));
    System.out.println(index); //输出0
    fastClass.invoke(index, new Proxy(), new Object[0]);
}

总结:

methodProxy.invoke(target, args); 
methodProxy.invokeSuper(proxy, args);

两个方法内部都是调用原始方法,如果是调用增强方法就死循环了,jdk 也是调用的原始方法,但是是通过反射。

所以 FastClassinvoke 方法都是调用原始方法,只不过是用目标类调用和用代理类调用的区别

Spring 选择代理

模拟 Spring 底层 AOP

流程如下:

  1. 定义切点
  2. 定义通知
  3. 定义切面(封装切点和通知)
  4. 创建代理

代码如下:

//接口
interface I1 {
    void foo();

    void bar();
}
//目标类
static class Target1 implements I1 {
    public void foo() {
        System.out.println("target1 foo");
    }

    public void bar() {
        System.out.println("target1 bar");
    }
}

public static void main(String[] args) {

    // 1. 定义切点
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* foo())");
    // 2. 定义通知
    MethodInterceptor advice = invocation -> {
        System.out.println("before...");
        Object result = invocation.proceed();//调用目标
        System.out.println("after...");
        return result;
    };
    // 3. 定义切面(封装切点和通知)
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

    // 4. 创建代理
    Target1 target = new Target1();
    // ProxyFactory 是 Spring 提供的代理工厂
    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(target);
    factory.addAdvisor(advisor);
    factory.setInterfaces(target.getClass().getInterfaces());
    I1 proxy = (I1) factory.getProxy();
    System.out.println(proxy.getClass()); //jdk实现代理
    proxy.foo();
    proxy.bar();
}

注意:

定义通知的 MethodInterceptor 接口是 Spring 的,不同于之前 cglib 中的 MethodInterceptor

选择代理规则

ProxyFactory 代理工厂用来创建代理,他的父类 ProxyConfig 中的 proxyTargetClass 属性决定选用哪个代理:

  • proxyTargetClass = false,目标实现了接口,,用 jdk 实现
  • proxyTargetClass = false, 目标没有实现接口,用 cglib 实现
  • proxyTargetClass = true,总是使用 cglib 实现

默认 false

Spring 切点匹配

AspectJ 匹配方式

模拟 AspectJ 进行切点匹配:

AspectJExpressionPointcut pt1 = new AspectJExpressionPointcut();
pt1.setExpression("execution(* bar())"); //只能匹配方法
boolean foo = pt1.matches(T1.class.getMethod("foo"), T1.class); //检查是否匹配

setExpression 底层通过 matches 匹配:

boolean matches(Method method, Class<?> targetClass)
  • method:目标方法
  • targetClass:目标类

@Transactional

@Transactional 底层并不是通过 AspectJExpressionPointcutsetExpression 完成匹配,因为 AspectJExpressionPointcut 匹配的局限性,只能匹配方法,而@Transactional 有3种用法:

  1. 加在方法
  2. 加在上,代表类中所有方法都要进行事务增强
  3. 加在接口上,代表所有实现方法都要进行事务增强

模拟匹配 @Transactional

StaticMethodMatcherPointcut pt3 = new StaticMethodMatcherPointcut() {
    @Override
    //重写匹配规则
    public boolean matches(Method method, Class<?> targetClass) {
        //检查方法上是否加了 Transactional 注解
        MergedAnnotations annotations = MergedAnnotations.from(method);
        if (annotations.isPresent(Transactional.class)){
            return true;
        }
        //查看类和实现接口是是否加了 Transactional 注解
        annotations = MergedAnnotations
            .from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
        if (annotations.isPresent(Transactional.class)) {
            return true;
        }
        return false;
    }
};
boolean foo = pt3.matches(T1.class.getMethod("foo"), T1.class);//检查是否匹配

高低级切面

高级切面类 Aspect

示例代码:

@Aspect //高级切面类
static class Aspect1{

    @Before("execution(* foo())")
    public void before(){
        System.out.println("aspect1 before...");
    }

    @After("execution(* foo())")
    public void after(){
        System.out.println("aspect1 after...");
    }
}
//目标类
static class Target1 {
    public void foo() {
        System.out.println("target1 foo");
    }
}

⛽知识加油站

切入点表达式可以直接写在通知方法上,也可以单独写在一个方法上,用 @Pointcut 注解,避免代码重复

在这里插入图片描述

低级切面类 Advisor

示例代码:

@Configuration
static class Config{
    @Bean //低级切面
    public Advisor advisor3(MethodInterceptor advice3){
        //创建切点
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* foo())");
        //把切点和通知封装为切面
        return new DefaultPointcutAdvisor(pointcut, advice3);
    }
    //创建通知
    @Bean
    public MethodInterceptor advice3(){
        return invocation -> {
            System.out.println("aspect3 before...");
            Object result = invocation.proceed(); //调用目标方法
            System.out.println("aspect3 after...");
            return result;
        };
    }
}

切面执行顺序

默认情况下低级切面先执行。

自定义执行顺序:

  • 在高级切面类上面添加 @Order
  • 在低级切面类中调用 DefaultPointcutAdvisor 对象的 setOrder 方法

@Order 局限性:

  1. 无法作用于低级切面类
  2. 无法作用于高级切面类的方法

Bean 后处理器

bean后处理器 AnnotationAwareAspectJAutoProxyCreator 的两个作用:

  1. 找到容器中的所有切面方法,并把高级切面转换为低级切面

  2. 创建代理对象

分别对应 AnnotationAwareAspectJAutoProxyCreator 中两个方法:

List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName)
Object wrapIfNecessary(Object bean, String beanName, Object cacheKey)

findEligibleAdvisors

List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName)
  • beanClass:指定一个类,查找该类中的方法是否匹配容器中的切面类的切点,简单来说就是查找切面方法
  • beanName:beanClass 类在容器中的名字,还没注册到容器的话可以随便起名

作用:把高级切面转换为低级切面,返回所有低级切面

高级切面的每个通知方法都会转换为一个低级切面

wrapIfNecessary

Object wrapIfNecessary(Object bean, String beanName, Object cacheKey)

只用关心第一个参数:

bean:目标对象

判断是否有必要为目标创建代理,内部会调用 findEligibleAdvisors只要返回集合不空, 则表示需要创建代理

代理创建时机

循环依赖:A 依赖 B,B 依赖 A(你中有我,我中有你)

创建原始类 bean 实例 -> (*) 依赖注入 -> 初始化 (*)

代理对象创建的时机在 “*”,二选一:

  • 没有循环依赖时:初始化之后
  • 有循环依赖时:创建原始类 bean 实例之后,并暂存于二级缓存

高级转低级切面

@Before 为例,@Before 会被转换为原始的 AspectJMethodBeforeAdvice ,流程如下:

  1. 获取 @Before 注解的方法
  2. 获取切点表达式
  3. 创建切点对象,设置其表达式
  4. 创建通知对象(每种通知都有自己的通知类
  5. 创建切面对象,封装切点和通知

模拟代码如下:

//创建切面实例工厂
AspectInstanceFactory factory = new SingletonAspectInstanceFactory(new Aspect());

for (Method method: Aspect.class.getDeclaredMethods()){
    // 1.获取 @Before 注解的方法
    if (method.isAnnotationPresent(Before.class)){
        // 2.获取切点表达式
        String espression = method.getAnnotation(Before.class).value();
        // 3.创建切点对象,设置其表达式
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(espression);
        // 4.创建通知
        AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(method, pointcut, factory);
        // 5.创建切面,封装切点和通知
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
        list.add(advisor);
    }
}

静态通知调用

统一转换为环绕通知

在这里插入图片描述

有多个通知的话,由外至内,再由内至外调用,所以需要把通知统一转换为环绕通知

环绕通知需要实现 MethodInterceptor 接口

5种通知对象:

  1. 前置通知:AspectJMethodBeforeAdvice
  2. 后置通知:AspectJAfterAdvice
  3. 返回后通知:AspectJAfterReturningAdvice
  4. 抛出异常后通知:AspectJAfterThrowingAdvice
  5. 环绕通知:AspectJAroundAdvice

其中,后置通知、抛出异常后通知、环绕通知已经实现了 MethodInterceptor 接口,无需转换

通过 ProxyFactorygetInterceptorsAndDynamicInterceptionAdvice 进行转换:

List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass)
  • method:目标方法对象
  • targetClass:目标类型
  • List<Object>:返回转换后的环绕通知集合

适配器模式

把一套接口转换为另一套接口,以便适合某种场景的使用,中间做转换的对象叫做适配器。

例如:

  • 适配器 MethodBeforeAdviceAdapter:将 AspectJMethodBeforeAdvice 适配为 MethodBeforeAdviceInterceptor
  • 适配器 AfterReturningAdviceAdapter:将 AspectJAfterReturningAdvice 适配为 AfterReturningAdviceInterceptor

调用链对象

MethodInvocation:调用链对象,调用每一个环绕通知(即实现 MethodInterceptor 接口)和目标

MethodInvocation proceed()方法 :调用每一个环绕通知和目标

在某些通知内部需要用到调用链对象,所以在最外层需要将 MethodInvocation 放入 ThreadLocal(当前线程)

在这里插入图片描述

模拟调用链执行

执行流程如下:

在这里插入图片描述

由外至内,再由内至外

模拟 proceed() 方法完成递归调用:

在这里插入图片描述

proceed() 不是直接递归调用 proceed(),而是通过 methodInterceptor.invoke(this) 间接调用 proceed()

动态通知调用

静态通知调用:

@Before("execution(* foo(..))") // 静态通知调用,不带参数绑定,执行时不需要切点对象
public void before1() {
    System.out.println("before1");
}

动态通知调用:

@Before("execution(* foo(..)) && args(x)") // 动态通知调用,需要参数绑定,执行时还需要切点对象
public void before2(int x) {
    System.out.printf("before2(%d)", x);
}

args(x) 中的 x 表示目标方法的第一个参数


List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass)

返回的不仅有环绕通知集合,还有 InterceptorAndDynamicMethodMatcher,即动态通知,内部有两个属性:

final MethodInterceptor interceptor;//环绕通知
final MethodMatcher methodMatcher; //切点对象
  • 有参数绑定的通知调用时需要切点对象,对参数进行匹配及绑定
  • 复杂程度高, 性能比没有参数绑定的通知调用低
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值