AOP面向切面编程:Aspect J的使用

对于AOP面向切面编程,目前AOP框架有多种选型,具体可以参考文章:谈谈Android AOP技术方案

在这里会讲解其中的AOP框架:AspectJ的使用。

1 面向切面编程

1.1 编程范式

在目前的编程语言中,编程范式可以分为以下几种

  • 面向过程编程:比如C语言

  • 面向对象编程:比如Java、C++

  • 函数式编程

  • 事件驱动编程

  • 面向切面编程

1.2 什么是AOP

AOP 全称为 Aspect Oriented Programming 面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。

AOP既然称为面向切面编程,那切面是什么?

切面:在面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面。

上面描述的切面定义可能还是太抽象,简单理解就是在方法添加了业务代码,被添加了业务代码的方法不需要被修改,而是通过在一个切面类定义方法实现就可以将业务代码切入到那个方法去完成目的

对于AOP面向切面编程,需要留意几个要点:

  • 它是一种编程范式,不是编程语言

  • 它是解决特定问题的,不能解决所有的问题

  • 它是OOP面向对象编程的补充,不是替代竞争的关系

1.3 AOP的初衷

既然提出了AOP面向切面编程,AOP又作为OOP的补充,那它主要就是解决OOP无法解决的一些现象和问题。

AOP提出的初衷主要是解决以下问题:

  • DRY(Don’t Repeat Yourself):解决代码重复的问题

  • Soc(Separation Of Concerns):解决关注点的分离。分离可以分为三种:

    • 水平分离:比如MVC中的展示层->服务层->持久层

    • 垂直分离:对于业务需求角度进行的分离,模块划分

    • 切面分离:对于功能性角度进行的分离,分离功能性需求与非功能性需求

1.4 使用AOP的好处和应用场景

那在实际中AOP面向切面编程可以带来如下好处:

  • 集中处理某一关注点/横切逻辑

  • 可以很方便的添加/删除关注点

  • 侵入性少,增强代码可读性及可维护性

AOP作为OOP的补充,它有属于自己适合的使用场景,比如权限控制、缓存控制、事务控制、审计日志、性能监控、异常处理等等。

2 AOP的使用

2.1 AspectJ的接入

在Android中接入AspectJ有两种方式:

  • 参考JakeWharton的 Hugo 开源框架的写法接入

  • 接入 hujiang AspectJ插件

Hugo 框架其实也是AspectJ在Android中的实际使用,这种方式的接入参考文章:AOP埋点从入门到放弃(一),还有 Hugo 框架的github地址:Hugo

第二种插件接入参考github地址:hujiang AspectJ插件。需要注意的是,如果AspectJ切面的代码是写在另一个module中的,如果要切入其他module下的代码,被切入的module需要引入插件。

2.2 AspectJ

在Java语言的AOP中需要使用到 AspectJ,它是一个面向切面的框架,扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

先用一个简单的例子来说明一下一个简单的使用:

@Target(value = {ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Arithmetic {
}

@Aspect
public class AopHelper {
	
	// 切点1
	@Pointcut("@annotation(Arithmetic)")
	public void pointcut1() {
	}

	// 切点2
	@Pointcut("execution(* com.example.demo.MainActivity.onCreate(..))")
	public void pointcut2() {
	}

	// 在注解了@Arithmetic的方法执行前先执行该方法
	@Before("pointcut1()")
	public void execBeforeAnnotatedArithmeticMethod() {
		Log.i("AOP", "execute before annotated @Arithmetic method");
	}
}

public class MainActivity extends AppCompatActivity {
	
	@Arithmetic
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Log.i("AOP", "execute method onCreate");
	}
}

运行上面的程序就能够将注解了 @Arithmetic 的方法、类运行打印出日志。通过在切面类中添加切点,就可以在执行到指定的代码时切入,基本不需要添加任何代码,非常方便。

接下来详细的讲解AspectJ的使用。

2.3 切面表达式

expression 切面表达式由三个部分组成:
在这里插入图片描述

  • wildcards(通配符):有一些方法和类要一一列举出来太麻烦,就可以使用通配符处理。在切点表达式中使用的通配符主要有以下三种:
通配符描述
*匹配任意数量的字符
+匹配指定类及其子类
..一般用于匹配任意数的子包或参数
  • operators(操作符):指定符合的执行条件组合使用。在切点表达式中使用的操作符主要有以下三种:
操作符描述
&&与操作符,都满足条件即可执行
||或操作符,只要满足一个条件即可执行
!非操作符,不满足条件
  • designators(指示器):描述指定通过哪种方式匹配要切入的Java类或方法。
    在这里插入图片描述
    接下来讲解下在切点表达式中指示器的匹配。

2.3.1 within表达式

within 是匹配指定包下的所有方法和类的类型的方法,比较多是和其他指示器一起使用。

Arithmetic.java

package com.example.demo;

public interface Arithmetic {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
}

ArithmeticCalculator.java

package com.example.demo;

import android.util.Log;

public class ArithmeticCalculator implements Arithmetic {
    private static final String TAG = "AOP";
    
    @Override
    public int add(int i, int j) {
        Log.i(TAG, "call method add(i, j), i = " + i + ", j = " + j);
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        Log.i(TAG, "call method sub(i, j), i = " + i + ", j = " + j);
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        Log.i(TAG, "call method mul(i, j). i = " + i + ", j = " + j);
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        Log.i(TAG, "call method div(i, j), i = " + i + ", j = " + j);
        return i / j;
    }
}

AspectWithin.java

@Aspect
public class AspectWithin {
    private static final String TAG = "AOP";

    // 匹配ArithmeticCalculator类里头的所有方法
    // 如果指示器匹配的是一个接口,实际代码执行的是接口的实现类,切入点不会匹配执行
    // 需要注意的是如果切入点的方法内也有方法调用,同样也会被切入
    @Pointcut("within(com.example.demo.ArithmeticCalculator)")
    public void matchType() {
    }

    // 匹配com.example.demo包及子包下所有类的方法
    // 如果AOP切面类也在指定的包下会抛出NoAspectBoundException,需要排除该切面类
    // within(com.example.demo.*) && !within(com.example.aop.AspectWithin)
    @Pointcut("within(com.example.demo.*)")
    public void matchPackage() {
    }

    @Before("matchType()")
    public void withinMatchType() {
        Log.i(TAG, "withinMatchType");
    }

    @Before("matchPackage()")
    public void withinMatchPackage() {
        Log.i(TAG, "withinMatchPackage");
    }
}

// 调用匹配的方法
ArithmeticCalculator calculator = new ArithmeticCalculator();
calculator.add(10, 5);

2.3.2 对象匹配

对象匹配有三个方法:this()bean()target(),指定匹配的对象类型和对象抽象类型(包含接口)下的所有方法。

package com.example.aop;

import android.util.Log;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectObject {
    private static final String TAG = "AOP";

    // this和target在没有introduction时可以认为是一样的
    // introduction是可以对某个类去动态增加方法的
    // 在有introduction时,this就可以去切入到introduction生成的这些方法,而target不能

    // 匹配AOP对象的目标对象为指定类型的方法,即ArithmeticCalculator的AOP代理对象的方法
    // AOP是会生成代理对象的,而this就是指的代理对象
    // 没有introduction时和target一样,所以this(com.example.demo.Arithmetic)也是一样可以匹配到
    @Pointcut("this(com.example.demo.ArithmeticCalculator)")
    public void matchThis() {
    }

    // 匹配实现Arithmetic接口的目标对象(而不是AOP代理后的对象)的方法,这里即ArithmeticCalculator的方法
    // target就是指原始的对象
    // 没有introduction时和this一样
    @Pointcut("target(com.example.demo.Arithmetic)")
    public void matchTarget() {
    }

    @Before("matchThis()")
    public void thisMatch() {
        Log.i(TAG, "thisMatch");
    }

    @Before("matchTarget()")
    public void targetMatch() {
        Log.i(TAG, "targetMatch");
    }
}

// 调用匹配的方法
// ArithmeticCalculator和ArithmeticCalculator2都实现了Arithmetic接口
ArithmeticCalculator calculator = new ArithmeticCalculator();
calculator.add(10, 5);

ArithmeticCalculator2 calculator2 = new ArithmeticCalculator2();
calculator2.add(5, 10);

2.3.3 参数匹配

args 参数匹配指定一个或多个对应类型的方法。

Arithmetic.java

package com.example.demo;

public interface Arithmetic {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
    void test1(Long i);
    void test2(long i, String s);
}

ArithmeticCalculator.java

package com.example.demo;

import android.util.Log;

public class ArithmeticCalculator implements Arithmetic {
    private static final String TAG = "AOP";

    @Override
    public int add(int i, int j) {
        Log.i(TAG, "call method add(i, j), i = " + i + ", j = " + j);
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        Log.i(TAG, "call method sub(i, j), i = " + i + ", j = " + j);
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        Log.i(TAG, "call method mul(i, j). i = " + i + ", j = " + j);
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        Log.i(TAG, "call method div(i, j), i = " + i + ", j = " + j);
        return i / j;
    }

    @Override
    public void test1(Long i) {
        Log.i(TAG, "call method test1(i), i = " + i);
    }

    @Override
    public void test2(long i, String s) {
        Log.i(TAG, "call method test2(i), i = " + i + ", s = " + s);
    }
}

AspectArgs.java

package com.example.aop;

import android.util.Log;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectArgs {
    private static final String TAG = "AOP";

    // 匹配任何只有一个Long类型参数的方法
    @Pointcut("within(com.example.demo.*) && args(Long)")
    public void matchArgs1() {
    }

    // 匹配第一个参数为Long类型的方法
    @Pointcut("within(com.example.demo.*) && args(Long,..)")
    public void matchArgs2() {
    }

    @Pointcut("within(com.example.demo.*) && args(Long, String)")
    public void matchArgs3() {
    }

    @Before("matchArgs1()")
    public void args1Match() {
        Log.i(TAG, "arg1Match");
    }

    @Before("matchArgs2()")
    public void args2Match() {
        Log.i(TAG, "args2Match");
    }

    @Before("matchArgs3()")
    public void args3Match() {
        Log.i(TAG, "args3Match");
    }
}

// 调用匹配的方法
 ArithmeticCalculator calculator = new ArithmeticCalculator();
 calculator.test1(10L);
 calculator.test2(10L, "test");

2.3.4 注解匹配

注解匹配有四种:@annotation()@within@target@args

ArithmeticAnnotation.java

package com.example.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ArithmeticAnnotation {
}

ArithmeticAnnotationClass.java

package com.example.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS) // 指定CLASS级别,也可以指定RUNTIME
@Target(ElementType.TYPE)
@Inherited
public @interface ArithmeticAnnotationClass {
}

ArithmeticAnnotationRuntime.java

package com.example.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) // 指定为RUNTIME级别
@Target(ElementType.TYPE)
@Inherited
public @interface ArithmeticAnnotationRuntime {
}

AspectAnotation.java

package com.example.aop;

import android.util.Log;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectAnnotation {
    private static final String TAG = "AOP";

    // 匹配标注有@ArithmeticAnnotation注解的类、方法或成员变量等
    @Pointcut("@annotation(com.example.aop.ArithmeticAnnotation)")
    public void annotationMatch() {
    }

    // 匹配标注有@ArithmeticAnnotationClass注解的类底下的所有方法
    // 要求注解的RetentionPolicy级别为CLASS或以上级别,即CLASS和RUNTIME会执行,SOURCE级别就不会执行
    // 被注解的类及其子类的方法都会被拦截切入
    @Pointcut("@within(com.example.aop.ArithmeticAnnotationClass)")
    public void annotationMatchWithin() {
    }

    // 匹配标注有@ArithmeticAnnotationRuntime注解的类底下的所有方法
    // 要求注解的RetentionPolicy级别为RUNTIME,即SOURCE和CLASS都不会执行
    // 被注解的类及其子类的方法都会被拦截切入
    @Pointcut("@target(com.example.aop.ArithmeticAnnotationRuntime)")
    public void annotationMatchTarget() {
    }

    // 匹配传入的参数类标注有@ArithmeticAnnotation注解的方法
    @Pointcut("@args(com.example.aop.ArithmeticAnnotation) && within(com.example.demo..*)")
    public void annotationMatchArgs() {
    }

    @Before("annotationMatch()")
    public void matchAnnotation() {
        Log.i(TAG, "matchAnnotation");
    }

    @Before("annotationMatchWithin()")
    public void matchAnnotationWithin() {
        Log.i(TAG, "matchAnnotationWithin");
    }

    @Before("annotationMatchTarget()")
    public void matchAnnotationTarget() {
        Log.i(TAG, "matchAnnotationTarget");
    }

    @Before("annotationMatchArgs()")
    public void matchAnnotationArgs() {
        Log.i(TAG, "matchAnnotationArgs");
    }
}

ArithmeticCalculator.java

package com.example.demo;

import android.util.Log;

import com.example.aop.ArithmeticAnnotation;
import com.example.aop.ArithmeticAnnotationClass;
import com.example.aop.ArithmeticAnnotationRuntime;

@ArithmeticAnnotation // @annotation和@args注解匹配
@ArithmeticAnnotationClass // @within注解匹配
@ArithmeticAnnotationRuntime // @target注解匹配
public class ArithmeticCalculator implements Arithmetic {
    private static final String TAG = "AOP";

    @Override
    public int add(int i, int j) {
        Log.i(TAG, "call method add(i, j), i = " + i + ", j = " + j);
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        Log.i(TAG, "call method sub(i, j), i = " + i + ", j = " + j);
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        Log.i(TAG, "call method mul(i, j). i = " + i + ", j = " + j);
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        Log.i(TAG, "call method div(i, j), i = " + i + ", j = " + j);
        return i / j;
    }

    @Override
    public void test1(Long i) {
        Log.i(TAG, "call method test1(i), i = " + i);
    }

    @Override
    public void test2(long i, String s) {
        Log.i(TAG, "call method test2(i), i = " + i + ", s = " + s);
    }
}

// 调用匹配方法
ArithmeticCalculator calculator = new ArithmeticCalculator();
calculator.add(10, 5);

2.3.5 execution表达式

格式:

// 标注?的是可选的,没有?的是必须要声明的
execution(
	modifier-pattern? // 方法修饰符表达式
	ret-type-pattern // 返回类型表达式
	declaring-type-pattern? // 包名对象类型表达式
	name-pattern(param-pattern) // 方法名表达式(参数表达式)
	throws-pattern? // 抛出异常表达式
)

同样用一个例子来讲解 execution

package com.example.demo.demo2; // 创建一个demo的子包下的类,验证拦截子包

import android.util.Log;

public class Test {
    private static final String TAG = "AOP";

    public void test() {
        Log.i(TAG, "test");
    }
}

package com.example.aop;

import android.util.Log;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectExecution {
    private static final String TAG = "AOP";

    // execution(
    //      public  // 方法修饰符
    //      *       // 任意返回值
    //      *.      // 任意类名
    //      *(..)   // *为任意方法名,(..)为任意参数【如果想匹配无参的就是()】
    // )
    // 该切点表达式只能拦截对应的包,如果也想要拦截对应包子包下的方法,只需要在包名下用 .. 表示,即com.example.demo..
    // execution(public * com.example.demo..*.*(..))
    @Pointcut("execution(public * com.example.demo.*.*(..))")
    public void matchExecution() {
    }

    // execution(
    //      public                          // 方法修饰符
    //      int                             // 返回int类型
    //      com.example.demo.Arithmetic*.   // 前缀为Arithmetic的类名
    //      *(int,int)                      // *为任意方法名,(int,int)为传参都是int类型
    // )
    @Pointcut("execution(public int com.example.demo.Arithmetic*.*(int, int))")
    public void matchExecution2() {
    }

    // execution(
    //      public                                      // 方法修饰符
    //      *                                           // 返回int类型
    //      *.                                          // 任意类名
    //      *(..)                                       // *为任意方法名,(int,int)为传参都是int类型
    //      throws java.lang.IllegalArgumentException   // 匹配抛出的异常
    // )
    @Pointcut("execution(public * com.example.demo.*.*(..) throws java.lang.IllegalArgumentException)")
    public void matchExecution3() {
    }

    @Before("matchExecution()")
    public void executionMatch() {
        Log.i(TAG, "executionMatch");
    }

    @Before("matchExecution2()")
    public void execution2Match() {
        Log.i(TAG, "execution2Match");
    }

    @Before("matchExecution3()")
    public void execution3Match(JoinPoint joinPoint) {
        Object clazz = joinPoint.getThis();
        Log.i(TAG, "execution3Match, clazz = " + clazz);
    }
}

// 调用匹配方法
ArithmeticCalculator calculator = new ArithmeticCalculator();
calculator.add(10, 5);
// demo子包demo2包下的类,验证匹配com.example.demo..
Test test = new Test();
test.test();

2.3.6 advice注解

advice注解也可以认为是AOP中的通知,通知有以下五种类型:

  • 前置通知:在目标方法被调用之前调用通知功能

  • 后置通知:在目标方法完成之后调用通知,此时不会关心方法的输出是什么

  • 返回通知:在目标方法成功执行之后调用通知

  • 异常通知:在目标方法抛出异常后调用通知

  • 环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

五种通知都有对应的注解:

注解类型
@Pointcut定义一个切点,用于声明可重用的切点表达式
@Before前置通知,在目标方法开始之前执行
@After后置通知,在目标方法执行后执行(无论是否发生异常)
@AfterReturning返回通知,在方法返回后执行
@AfterThrowing异常通知,可以指定在出现特定异常时再执行通知
@Around环绕通知,需要携带 ProceedingJointPint 类型参数,环绕通知必须有返回值,返回值即目标方法的返回值
AopHelper.java

@Aspect
public class AopHelper {
    private static final String TAG = "AOP";

    // 声明可重用的切点表达式;一般该方法不需要添加其他代码
    @Pointcut("execution(* com.example.demo.ArithmeticCalculator.*(..))")
    public void declaredPointcutExpression() {
    }

    // 声明一个前置通知:在目标方法开始之前执行
    // JoinPoint可以作为参数获取参数,如方法名和方法参数
    @Before("declaredPointcutExpression()")
    public void pointcutBeforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        Log.i(TAG, "pointcut before, methodName = " + methodName + ", args = " + args);
    }

    // 后置通知:在目标方法执行后执行(无论是否发生异常)
    // 在后置通知中还不能访问目标方法执行的结果
    @After("declaredPointcutExpression()")
    public void pointcutAfterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Log.i(TAG, "pointcut after, methodName = " + methodName);
    }

    // 返回通知:在方法返回后执行
    // 可以访问到方法的返回值,指定的returning就是方法的返回值
    @AfterReturning(value = "declaredPointcutExpression()", returning = "result")
    public void pointcutAfterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        Log.i(TAG, "pointcut after returning, methodName = " + methodName + ", result = " + result);
    }

    // 异常通知,可以指定在出现特定异常时再执行通知
    // 如Exception改为NullPointException,而抛出的异常时ArithmeticException,则异常通知不会执行
    @AfterThrowing(value = "execution(* com.example.demo.ArithmeticCalculator.div(..))", throwing = "ex")
    public void pointcutAfterThrowing(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        Log.i(TAG, "pointcut after throwing, methodName = " + methodName + ", ex = " + ex);
    }

    @Around("declaredPointcutExpression()")
    public Object pointcutAroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
        Object result;
        String methodName = proceedingJoinPoint.getSignature().getName();

        try {
            // 前置通知
            Log.i(TAG, "pointcut around before, methodName = " + methodName);
            // 执行目标方法
            result = proceedingJoinPoint.proceed();
            // 返回通知
            Log.i(TAG, "pointcut around, methodName = " + methodName);
        } catch (Throwable throwable) {
            // 异常通知
            Log.i(TAG, "pointcut around exception, ex = " + throwable);
            throw new RuntimeException(throwable);
        }

        // 后置通知
        Log.i(TAG, "pointcut around after, methodName = " + methodName);

        return result;
    }
}

ArithmeticCalculator calculator = new ArithmeticCalculator();
calculator.add(10, 5);
try {
    calculator.div(5, 0);
} catch (Exception e) {
    e.printStackTrace();
}

输出:
// 前置、后置、返回、异常通知
pointcut before, methodName = add, args = [10, 5]
call method add(i, j), i = 10, j = 5
pointcut after, methodName = add
pointcut after returning, methodName = add, result = 15
pointcut before, methodName = div, args = [5, 0]
call method div(i, j), i = 5, j = 0
pointcut after, methodName = div
pointcut after throwing, methodName = div, ex = java.lang.ArithmeticException: divide by zero

// 环绕通知
pointcut around before, methodName = add
call method add(i, j), i = 10, j = 5
pointcut around, methodName = add
pointcut around after, methodName = add
pointcut around before, methodName = div
call method div(i, j), i = 5, j = 0
pointcut around exception, ex = java.lang.ArithmeticException: divide by zero
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值