Spring AOP系列学习笔记四:AOP具体调用流程分析

原文地址程序员囧辉大佬



AOP系列文章链接地址

Spring AOP系列学习笔记一:AOP简介
Spring AOP系列学习笔记二:AOP入口分析
Spring AOP系列学习笔记三:AOP注解原理分析
Spring AOP系列学习笔记四:AOP具体调用流程分析

aop简单流程代码如下:
aop代理类:

@Component
@Aspect
public class LogInterceptor {

    @Pointcut("execution(*   com.zgf.aop.*.*(..)) && args(price,discount)")
    public void point(double price,double discount) {
    }

    @Around(value = "point(price,discount)")
    public Object around(ProceedingJoinPoint pjp,double price,double discount) throws Throwable {
        if (price>=100){
            return (Double)pjp.proceed()*0.5;
        }
        return pjp.proceed();
    }
}

服务代码:

public interface ShopService {
    public double getPrice(double price,double discount);
}

@Service("shopService")
public class ShopServiceImpl implements ShopService{

    public double getPrice(double price,double discount){
        return price*discount;
    }

}

测试代码:

public class TestAopDemo {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        //jdk代理
        ShopService shopService = (ShopService) ac.getBean("shopService");
        double price = shopService.getPrice(100, 0.6);
        //CGLB代理
/*        ShopServiceImpl shopService = (ShopServiceImpl) ac.getBean("shopService");
        double price = shopService.getPrice(100, 0.6);*/
        System.out.println(price);
    }
}

applicationContext.xml:

    <context:component-scan base-package="com.zgf"></context:component-scan>

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

反编译之后的源码:

package com.sun.proxy;

import com.zgf.service.CloudDiskShareInfoDetailService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.core.DecoratingProxy;

public final class $Proxy62
extends Proxy
implements CloudDiskShareInfoDetailService,
SpringProxy,
Advised,
DecoratingProxy {
    private static Method m1;
    private static Method m10;
    private static Method m13;
    private static Method m25;
    private static Method m21;
    private static Method m3;
    private static Method m23;
    private static Method m4;
    private static Method m8;
    private static Method m15;
    private static Method m16;
    private static Method m0;
    private static Method m24;
    private static Method m17;
    private static Method m7;
    private static Method m12;
    private static Method m22;
    private static Method m2;
    private static Method m27;
    private static Method m11;
    private static Method m28;
    private static Method m20;
    private static Method m5;
    private static Method m6;
    private static Method m14;
    private static Method m19;
    private static Method m26;
    private static Method m18;
    private static Method m9;

    public $Proxy62(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }
	1、这儿是我后期补的,并不是上面的例子,源码找不到了,随便生成了个代理类源码看  嘿嘿
	不过意思都差不多
    public final void aaa() {
        try {
        	1、h就是JdkDynamicAopProxy调用invoke,开始执行aop调用链
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }


    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m10 = Class.forName("org.springframework.aop.framework.Advised").getMethod("addAdvisor", Class.forName("org.springframework.aop.Advisor"));
            m13 = Class.forName("org.springframework.aop.framework.Advised").getMethod("isExposeProxy", new Class[0]);
            m25 = Class.forName("org.springframework.aop.framework.Advised").getMethod("isProxyTargetClass", new Class[0]);
            m21 = Class.forName("org.springframework.aop.framework.Advised").getMethod("removeAdvisor", Integer.TYPE);
            m3 = Class.forName("com.zgf.service.CloudDiskShareInfoDetailService").getMethod("aaa", new Class[0]);
            m23 = Class.forName("org.springframework.aop.framework.Advised").getMethod("getProxiedInterfaces", new Class[0]);
            m4 = Class.forName("org.springframework.aop.framework.Advised").getMethod("indexOf", Class.forName("org.springframework.aop.Advisor"));
            m8 = Class.forName("org.springframework.aop.framework.Advised").getMethod("getTargetSource", new Class[0]);
            m15 = Class.forName("org.springframework.aop.framework.Advised").getMethod("addAdvice", Integer.TYPE, Class.forName("org.aopalliance.aop.Advice"));
            m16 = Class.forName("org.springframework.aop.framework.Advised").getMethod("addAdvice", Class.forName("org.aopalliance.aop.Advice"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m24 = Class.forName("org.springframework.aop.framework.Advised").getMethod("isInterfaceProxied", Class.forName("java.lang.Class"));
            m17 = Class.forName("org.springframework.aop.framework.Advised").getMethod("removeAdvice", Class.forName("org.aopalliance.aop.Advice"));
            m7 = Class.forName("org.springframework.aop.framework.Advised").getMethod("setExposeProxy", Boolean.TYPE);
            m12 = Class.forName("org.springframework.aop.framework.Advised").getMethod("getAdvisorCount", new Class[0]);
            m22 = Class.forName("org.springframework.aop.framework.Advised").getMethod("setTargetSource", Class.forName("org.springframework.aop.TargetSource"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m27 = Class.forName("org.springframework.aop.framework.Advised").getMethod("getTargetClass", new Class[0]);
            m11 = Class.forName("org.springframework.aop.framework.Advised").getMethod("addAdvisor", Integer.TYPE, Class.forName("org.springframework.aop.Advisor"));
            m28 = Class.forName("org.springframework.core.DecoratingProxy").getMethod("getDecoratedClass", new Class[0]);
            m20 = Class.forName("org.springframework.aop.framework.Advised").getMethod("removeAdvisor", Class.forName("org.springframework.aop.Advisor"));
            m5 = Class.forName("org.springframework.aop.framework.Advised").getMethod("indexOf", Class.forName("org.aopalliance.aop.Advice"));
            m6 = Class.forName("org.springframework.aop.framework.Advised").getMethod("isFrozen", new Class[0]);
            m14 = Class.forName("org.springframework.aop.framework.Advised").getMethod("replaceAdvisor", Class.forName("org.springframework.aop.Advisor"), Class.forName("org.springframework.aop.Advisor"));
            m19 = Class.forName("org.springframework.aop.framework.Advised").getMethod("setPreFiltered", Boolean.TYPE);
            m26 = Class.forName("org.springframework.aop.framework.Advised").getMethod("toProxyConfigString", new Class[0]);
            m18 = Class.forName("org.springframework.aop.framework.Advised").getMethod("getAdvisors", new Class[0]);
            m9 = Class.forName("org.springframework.aop.framework.Advised").getMethod("isPreFiltered", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }
}


注意看代理类$Proxy62实现了被代理方法的接口CloudDiskShareInfoDetailServiceSpringProxyAdvised,并且继承了Proxy类,正是jdk动态代理,所以为什么JDK动态代理只支持接口,而不支持实现类,就是因为java是单继承的,JDK动态代理需要继承Proxy类,而无法再去继承实现类。
spring-boot 2+直接默认修改成了CGLIB动态代理,避免了一些错误。
本次主要分析JdkDynamicAopProxy的调用流程,CGLB大同小异。下面进入正文

JdkDynamicAopProxy#invoke

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    MethodInvocation invocation;
    Object oldProxy = null;
    boolean setProxyContext = false;
 
    // 1.advised就是proxyFactory,而targetSource持有被代理对象的引用
    TargetSource targetSource = this.advised.targetSource;
    Class<?> targetClass = null;
    Object target = null;
 
    try {
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            // The target does not implement the equals(Object) method itself.
            // 目标不实现equals(Object)方法本身。
            return equals(args[0]);
        }
        else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            // The target does not implement the hashCode() method itself.
            return hashCode();
        } else if (method.getDeclaringClass() == DecoratingProxy.class) {
            // There is only getDecoratedClass() declared -> dispatch to proxy config.
            // 只有getDecoratedClass()声明 - > dispatch到代理配置。
            return AopProxyUtils.ultimateTargetClass(this.advised);
        } else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                method.getDeclaringClass().isAssignableFrom(Advised.class)) {
            // Service invocations on ProxyConfig with the proxy config...
            // ProxyConfig上的服务调用与代理配置...
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }
 
        Object retVal;
 
        // 有时候目标对象内部的自我调用将无法实施切面中的增强则需要通过此属性暴露代理
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
        
        // 2.拿到我们被代理的对象实例
        target = targetSource.getTarget();
        if (target != null) {
            targetClass = target.getClass();
        }
 
        // Get the interception chain for this method.
        // 3.获取拦截器链:例如使用@Around注解时会找到AspectJAroundAdvice,还有ExposeInvocationInterceptor
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        
        // 4.检查我们是否有任何拦截器(advice)。 如果没有,直接反射调用目标,并避免创建MethodInvocation。
        if (chain.isEmpty()) {
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            // 5.不存在拦截器链,则直接进行反射调用
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        } else {
            // We need to create a method invocation...
            // 6.如果存在拦截器,则创建一个ReflectiveMethodInvocation:代理对象、被代理对象、方法、参数、
            // 被代理对象的Class、拦截器链作为参数创建ReflectiveMethodInvocation
            invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // Proceed to the joinpoint through the interceptor chain.
            // 7.触发ReflectiveMethodInvocation的执行方法
            retVal = invocation.proceed();
        }
 
        // Massage return value if necessary.
        // 8.必要时转换返回值
        Class<?> returnType = method.getReturnType();
        if (retVal != null && retVal == target &&
                returnType != Object.class && returnType.isInstance(proxy) &&
                !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this" and the return type of the method
            // is type-compatible. Note that we can't help if the target sets
            // a reference to itself in another returned object.
            retVal = proxy;
        } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    } finally {
        if (target != null && !targetSource.isStatic()) {
            // Must have come from TargetSource.
            targetSource.releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

3.获取拦截器链:使用 @Around 注解时会找到 AspectJAroundAdvice,还有 ExposeInvocationInterceptor,其中 ExposeInvocationInterceptor 在前,AspectJAroundAdvice 在后。

6.如果存在拦截器,则创建一个 ReflectiveMethodInvocation,代理对象、被代理对象、方法、参数、被代理对象的 Class、拦截器链作为参数。这边 ReflectiveMethodInvocation 已经持有了被代理对象、方法、参数,后续就可以直接使用反射来调用被代理的方法了,见代码块1

7.触发 ReflectiveMethodInvocation 的执行方法,见代码块2

代码块1:ReflectiveMethodInvocation 构造函数

	protected ReflectiveMethodInvocation(
			Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments,
			@Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {

		this.proxy = proxy;
		this.target = target;
		this.targetClass = targetClass;
		this.method = BridgeMethodResolver.findBridgedMethod(method);
		this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
		this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
	}

代码块2:ReflectiveMethodInvocation#proceed

	public Object proceed() throws Throwable {
		//	We start with an index of -1 and increment early.
		1、遍历代理类,依次进行代理
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}
		2、每次调用的时候递增,拿到相应的拦截器,并做一个标记
		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			// Evaluate dynamic method matcher here: static part will already have
			// been evaluated and found to match.
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			3、进行判断是否动态匹配,就是判断当前代理和被代理是否相匹配
			if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				4、不匹配,直接跳过该代理,进行下一个
				// Dynamic matching failed.
				// Skip this interceptor and invoke the next in the chain.
				return proceed();
			}
		}
		else {
			// It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.
			5、只是一个普通的拦截器,则触发拦截器链责任链的调用,并且参数为ReflectiveMethodInvocation本身
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

该方法是一个责任链的方法,会按索引执行所有的拦截器。

1.如果所有拦截器都执行完毕(index是从-1开始,所以跟size - 1比较),则直接使用反射调用连接点(也就是我们原本的方法),见代码块3

4.只是一个普通的拦截器,则直接调用它,参数为自己本身,在本文的例子,interceptorsAndDynamicMethodMatchers 有两个拦截器:ExposeInvocationInterceptor 在前,AspectJAroundAdvice 在后,因此首先会触发 ExposeInvocationInterceptor 的 invoke 方法,见代码块4

代码块3:invokeJoinpoint

	protected Object invokeJoinpoint() throws Throwable {
		return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
	}


	public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
			throws Throwable {

		// Use reflection to invoke the method.
		try {
			ReflectionUtils.makeAccessible(method);
			1、直接执行被代理方法
			return method.invoke(target, args);
		}
		catch (InvocationTargetException ex) {
			// Invoked method threw a checked exception.
			// We must rethrow it. The client won't see the interceptor.
			throw ex.getTargetException();
		}
		catch (IllegalArgumentException ex) {
			throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
					method + "] on target [" + target + "]", ex);
		}
		catch (IllegalAccessException ex) {
			throw new AopInvocationException("Could not access method [" + method + "]", ex);
		}
	}

代码块4:ExposeInvocationInterceptor # invoke

	@Override
	public Object invoke(MethodInvocation mi) throws Throwable {
		MethodInvocation oldInvocation = invocation.get();
		invocation.set(mi);
		try {
			1、在这儿又会回到责任链中,进行下一个代理类的
			return mi.proceed();
		}
		finally {
			invocation.set(oldInvocation);
		}
	}

本例子中用的是@Around注解,所以下一个invoke方法会进入AspectJAroundAdvice # invoke

代码块5:AspectJAroundAdvice # invoke

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    // 1.这边的mi就是我们的ReflectiveMethodInvocation,
    // ReflectiveMethodInvocation实现了ProxyMethodInvocation接口,所以这边肯定通过校验
    if (!(mi instanceof ProxyMethodInvocation)) {
        throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
    }
    // 2.将mi直接强转成ProxyMethodInvocation,mi持有代理类实例proxy、被代理类实例target、被代理的方法method等
    ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
    // 3.将pmi封装层MethodInvocationProceedingJoinPoint(直接持有入参mi,也就是ReflectiveMethodInvocation的引用)
    ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
    // 4.拿到pointcut的表达式
    JoinPointMatch jpm = getJoinPointMatch(pmi);
    // 5.调用增强方法
    return invokeAdviceMethod(pjp, jpm, null, null);
}

5.调用增强方法,见代码块6

代码块6:invokeAdviceMethod

protected Object invokeAdviceMethod(JoinPoint jp, JoinPointMatch jpMatch, Object returnValue, Throwable t)
        throws Throwable {
    // 1.argBinding:获取方法执行连接点处的参数
    // 2.invokeAdviceMethodWithGivenArgs:使用给定的参数调用增强方法
    return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}

1.获取方法执行连接点处的参数,见代码块7

2.使用 argBinding 方法返回的参数调用增强方法,在本文给出的例子中,也就是 LogInterceptor 中被 @Around 修饰的 around(ProceedingJoinPoint pjp) 方法,见代码块8

代码块7:argBinding

protected Object[] argBinding(JoinPoint jp, JoinPointMatch jpMatch, Object returnValue, Throwable ex) {
    calculateArgumentBindings();
 
    // AMC start
    Object[] adviceInvocationArgs = new Object[this.parameterTypes.length];
    int numBound = 0;
 
    if (this.joinPointArgumentIndex != -1) {
        // 1.如果存在连接点参数,则将jp添加到调用参数
        // 当使用@Around时就有参数;使用@Before、@After时就没有参数
        adviceInvocationArgs[this.joinPointArgumentIndex] = jp;
        numBound++;
    } else if (this.joinPointStaticPartArgumentIndex != -1) {
        adviceInvocationArgs[this.joinPointStaticPartArgumentIndex] = jp.getStaticPart();
        numBound++;
    }
 
    if (!CollectionUtils.isEmpty(this.argumentBindings)) {
        // binding from pointcut match
        // 2.使用pointcut匹配绑定
        if (jpMatch != null) {
            PointcutParameter[] parameterBindings = jpMatch.getParameterBindings();
            for (PointcutParameter parameter : parameterBindings) {
                String name = parameter.getName();
                Integer index = this.argumentBindings.get(name);
                adviceInvocationArgs[index] = parameter.getBinding();
                numBound++;
            }
        }
        // binding from returning clause
        // 3.用于绑定@AfterReturing中的returning参数
        if (this.returningName != null) {
            Integer index = this.argumentBindings.get(this.returningName);
            adviceInvocationArgs[index] = returnValue;
            numBound++;
        }
        // binding from thrown exception
        // 4.用于绑定@AfterThrowing中的throwing参数
        if (this.throwingName != null) {
            Integer index = this.argumentBindings.get(this.throwingName);
            adviceInvocationArgs[index] = ex;
            numBound++;
        }
    }
 
    if (numBound != this.parameterTypes.length) {
        throw new IllegalStateException("Required to bind " + this.parameterTypes.length +
                " arguments, but only bound " + numBound + " (JoinPointMatch " +
                (jpMatch == null ? "was NOT" : "WAS") + " bound in invocation)");
    }
 
    return adviceInvocationArgs;
}

1.如果存在连接点参数,则将 jp 添加到增强方法的参数数组,对于 @Around 来说,这边的 jp 就是代码块5中的入参 mi,也就是我们之前创建的 ReflectiveMethodInvocation 对象。所以,当使用 @Around 时,这边返回的增强方法的参数数组持有的是 ReflectiveMethodInvocation 对象。

代码块8:invokeAdviceMethodWithGivenArgs

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
    Object[] actualArgs = args;
    // 1.如果增强方法没有参数,则将actualArgs赋值为null
    if (this.aspectJAdviceMethod.getParameterTypes().length == 0) {
        actualArgs = null;
    }
    try {
        ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
        // TODO AopUtils.invokeJoinpointUsingReflection
        // 2.反射执行增强方法
        return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
    } catch (IllegalArgumentException ex) {
        throw new AopInvocationException("Mismatch on arguments to advice method [" +
                this.aspectJAdviceMethod + "]; pointcut expression [" +
                this.pointcut.getPointcutExpression() + "]", ex);
    } catch (InvocationTargetException ex) {
        throw ex.getTargetException();
    }
}

2.反射执行增强方法,对于本文给出的例子,这边会直接走到 LogInterceptor 中被 @Around 修饰的 around(ProceedingJoinPoint pjp) 方法,该方法会执行一些增强逻辑,最终执行 “Object result = pjp.proceed()”。

通过代码块6和代码块7我们知道,这边的 pjp 就是我们之前创建的 ReflectiveMethodInvocation 对象,所以这边会再次调用 ReflectiveMethodInvocation 对象的 proceed() 方法,也就是回到代码块2。此时我们的拦截器都已经执行完毕,因此会走到 invokeJoinpoint() 方法,通过反射执行我们被代理的方法,也就是 getName(String name) 方法。

总结

AspectJ 方式的 AOP 内容到此就介绍完毕了,核心流程如下。

1)解析 AOP 的注解,并注册对应的内部管理的自动代理创建者的 bean,对于本次介绍是:AnnotationAwareAspectJAutoProxyCreator,其他的还有 InfrastructureAdvisorAutoProxyCreator 和 AspectJAwareAdvisorAutoProxyCreator。

2)当我们的 bean 初始化完毕后,会触发所有 BeanPostProcessor 的 postProcessAfterInitialization 方法,此时就会调用我们的 AnnotationAwareAspectJAutoProxyCreator 的 postProcessAfterInitialization 方法。该方法会查找我们定义的切面类(使用 @Aspect 注解),创建切面类中定义的增强器(使用 @Before、@After、@Around 等注解),并根据 @Pointcut 的 execution 表达式筛选出适用于当前遍历的 bean 的增强器, 将适用于当前遍历的 bean 的增强器作为参数之一创建对应的 AOP 代理。

3)当调用到被 AOP 代理的方法时,会走到对应的代理方法:JdkDynamicAopProxy#invoke 或 DynamicAdvisedInterceptor#intercept,该方法会创建 ReflectiveMethodInvocation,通过责任链的方式来执行所有的增强器和被代理的方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值