Spring教程02-面向切面编程(AOP)

2 篇文章 0 订阅

AOP

3.1 动态代理

3.1.1实现方式

  • JDK动态代理
    • 使用JDK中的Proxy, Method, InvocationHander创建代理对象
    • JDK动态代理要求目标类必须实现接口
  • cglib动态代理
    • 第三方的工具库,创建代理对象,原理是继承,通过继承目标类,创建子类,子类就是代理对象
    • 要求目标类不能是final的,方法也不能是final的

3.1.2 作用

  • 在目标类源码不改变的情况下,增加功能
  • 减少代码重复
  • 专注业务逻辑,没有干扰的日志或事务功能
  • 解耦合,业务功能和日志,事务非业务功能分离

3.1.3 未使用AOP开发代码

​ 目标:对业务类增加非业务方法

​ 实现:先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑。

定义业务接口:

public interface SomeService {
    void doSome();
    void doOther();
}

业务接口实现类:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        System.out.println("执行了业务方法doSome");
    }

    @Override
    public void doOther() {
        System.out.println("执行了业务方法doOther");
    }
}

非业务方法:

​ ➢ doTransaction():用于事务处理
​ ➢ doLog():用于日志处理 然后,再使接口方法调用它们。接口方法也称为主业务逻辑。

    public void doLog(){
        System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
    }
    
    public void doTrans(){
        System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
    }
  1. 方式一:在业务实现类中,直接调用非业务方法
public class SomeServiceImpl2 implements SomeService {
    @Override
    public void doSome() {
        doLog();            //直接调用
        System.out.println("执行了业务方法doSome");
        doTrans();          //直接调用
    }

    @Override
    public void doOther() {
        doLog();            //直接调用
        System.out.println("执行了业务方法doOther");
        doTrans();          //直接调用
    }

    public void doLog(){
        System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
    }

    public void doTrans(){
        System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
    }
}
  1. 方式二:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。

ServiceTools代码:

public class ServiceTools {
    public static void doLog(){
        System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
    }

    public static void doTrans(){
        System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
    }
}

实现类代码:

public class SomeServiceImpl3 implements SomeService {
    @Override
    public void doSome() {
        ServiceTools.doLog();            //通过工具类调用
        System.out.println("执行了业务方法doSome");
        ServiceTools.doTrans();          //通过工具类调用
    }

    @Override
    public void doOther() {
        ServiceTools.doLog();            //通过工具类调用
        System.out.println("执行了业务方法doOther");
        ServiceTools.doTrans();          //通过工具类调用
    }
}
  1. 方式三:动态代理

​ 以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能

​ MyInvocationHandler 继承 InvocationHandler接口:

public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    //无参构造
    public  MyInvocationHandler() {
        super();
    }
    public  MyInvocationHandler(Object target) {
        super();
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object obj = null;
        //在方法之前,输出日志
        ServiceTools.doLog();
        //执行目标方法,执行target对象的方法
        obj = method.invoke(target,args);
        //在方法之后,执行事务
        ServiceTools.doTrans();
        return obj;
    }
}

​ 动态代理测试代码:

public class MyTest {
    public static void main(String[] args) {
        //创建代理对象
        SomeServiceImpl target = new SomeServiceImpl();
        MyInvocationHandler handler = new MyInvocationHandler(target);

        SomeService proxy = (SomeService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                handler);

        //通过代理对象执行业务方法,实现日志  实现事务
        proxy.doSome();
        System.out.println("===========================");
        proxy.doOther();
    }
}

3.2 AOP概述

3.2.1 AOP概念

AOP(Aspect Orient Programming)面向切面编程

  • Aspect: 切面,给目标类增加的功能是切面,像上面用的日志,事务都是切面。切面的特点: 一般都是非业务方法,独立使用的。
  • Orient:面向
  • Programming:编程

3.2.2 AOP实现方式

​ AOP:面向切面编程,基于动态代理,可以使用JDK,cglib两种代理方式

​ AOP就是动态代理的规范化,把动态代理对实现步骤和方式都定义好,开发人员用一种统一的方式,就是动态代理。

3.2.3 如何理解切面编程

​ (1)需要在分析项目功能时,找出切面

​ (2)合理的安排切面的执行时间(在目标方法前, 还是目标方法后)

​ (3)合理的安全切面执行的位置,在哪个类,哪个方法增加增强功能

3.2.4 面向切面编程对有什么好处?

  1. 减少重复
  2. 专注业务

:面向切面编程知识面向对象的一种补充

image-20220619153730949

3.3 AOP术语

3.3.1 AOP术语

(1)**切面(Aspect):**切面,表示增强的功能,非业务方法。泛指交叉业务逻辑,常见的事务处理、日志处理、统计信息、权限验证就可以理解为切面。

(2)**连接点(JoinPoint) :**连接点,连接业务方法和切面的位置,就是某个类中的业务方法。例如在动态代理中,doSome方法中加入了非业务方法,称该方法为连接点

(3)**切入点(Pointcut):**切入点,指多个连接点方法的集合。

(4)**目标对象:**给那个类的方法增加功能,这个类就是目标对象。

(5)**通知(Advice):**通知,通知表示切面功能执行的时间。通知定义了增强代码切入到目标代码的时间点,是目标方 法执行之前执行,还是之后执行等。

3.3.2 前面的三要素

一个切面有三个关键要素:

(1)切面的功能代码,切面干什么

(2)切面的执行位置,使用Pointcut表示切面的执行位置

(3)切面的执行时间,使用Advice表示时间,在目标方法之前还是之后

3.4 AOP的实现

AOP是一个规范是对动态代理的一个规范化,一个标注。

​ AOP技术实现框架:

  1. spring:spring在内部实现了AOP规范,能做AOP的工作。spring主要在事务处理时使用AOP。
    日常项目开发中,很少使用spring的AOP实现,因为spring的AOP比较笨重

  2. aspectJ:一个开源专门做AOP的框架。spring框架中集成了aspectJ框架

    aspectJ框架实现aop的两种方式

    ​ 1)使用xml的配置文件

    ​ 2)使用注解,我们在项目中使用aop功能,一般都使用注解,aspectJ一共有5个注解

3.5 学习 aspectJ的使用(基于注解)

3.5.1 切面的执行时间

​ 切面的执行时间,在规范中叫做Advice(通知,增强)。在asepctJ框架中使用注解表示,也可以使用xml配置文件中的标签。在aspectJ中常用的物种类型:

(1)前置通知

(2)后置通知

(3)环绕通知

(4)异常通知

(5)最终通知

3.5.2 AspectJ 的切入点表达式

​ 切面的位置,使用的是切入点表达式。表达式的原型是:

execution(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) 方法名(参数类型和参数个数) 
throws-pattern 							抛出异常类型
?表示可选的部分

​ 简要表示为:

execution(「访问权限」 方法返回值 方法声明(参数) 「异常类型」) =======>「」表示可选

切入点表达式要匹配的对象就是目标方法的方法名,所以,execution 表达式是方法的签名,可以使用以下符号简写:

image-20220619155827518

举例:

execution(public * *(..)):指定切入点为:任意公共方法。

execution(* set*(..)):指定切入点为:任何一个以“set”开始的方法。

execution(* com.xxx.service.*.*(..)): 指定切入点为:定义在 service 包里的任意类的任意方法。

execution(* com.xyz.service..*.*(..)):指定切入点为:定义在 service 包或者子包里的任意类的任意方法。 “…”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。

execution(* *..service.*.*(..)):指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

3.5.3 使用AspectJ框架实现aop

3.5.3.1 使用步骤
  1. 新建maven项目
  2. 加入依赖:①spring依赖 ②aspect依赖
  3. 创建目标类:接口和实现类, 要做的就是给类中的方法增加功能
  4. 创建切面类:
    • 在类的上面加入@Aspect
    • 在类中定义方法,也就是切面要执行的功能代码。 在方法的上面加入aspectJ中的注解,例如:@Before 有需要指定切入点表达式execution()
  5. 创建spring配置文件:声明对象,把对象交给容器统一管理,声明对象可以使用注解或者xml配置文件的标签
    • 声明目标类对象
    • 声明切面类对象
    • 声明aspectJ框架中的自动代理生成器标签
      • 自动代理生成器:用来完成代理对象的自动创建功能的
  6. 创建测试类,从spring容器中获取目标对象(实际就是代理对象),通过代理执行方法,实现aop功能增强。

举例:

(1)新建maven项目

(2)加入依赖:①spring依赖 ②aspect依赖

<!--spring依赖-->
<dependency>
	<groupId>org.springframework</groupId> 
  <artifactId>spring-context</artifactId> 
  <version>5.2.5.RELEASE</version>
</dependency>

<!--aspectJ依赖-->
<dependency> 
  <groupId>org.springframework</groupId> 
  <artifactId>spring-aspects</artifactId> 
  <version>5.2.5.RELEASE</version>
</dependency>

(3)创建目标类:接口和实现类, 要做的就是给类中的方法增加功能

创建业务接口类SomeService

public interface SomeService {
    void doSome(String name, Integer age);
}

创建业务实现类SomeServiceImpl:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        //给doSome方法增加一个功能,在doSome方法执行之前,输出方法的执行时间
        System.out.println("=====目标方法doSome()=====");
    }
}

(4)创建切面类:

  • 在类的上面加入@Aspect

  • 在类中定义方法,也就是切面要执行的功能代码。 在方法的上面加入aspectJ中的注解,例如:@Before 有需要指定切入点表达式execution()

创建MyAspect类:

/**
 * @Aspect :是aspectJ框架中的注解
 *      作用:表示当前类是切面类
 *      切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *      位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 定义方法:方法是实现切面功能的
     * 方法的定义要求:
     *  1.公共方法public
     *  2.方法没有返回值
     *  3.方法名称自定义
     *  4.可以有参数 也可以没有
     *      若有参数,参数不是自定义的,有几个参数类型可以使用
     */

    /**
     * @Before: 前置通知注解
     *  属性:value,是切入点表达式,表示切面的功能执行的位置
     *  特点:
     *      1.在目标方法之前执行的
     *      2.不会改变目标方法的执行结果
     *      3.不会影响目标方法的执行
     */
    @Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        //就是切面要执行的功能代码
        System.out.println("前置通知,切面功能,在目标方法之前输出时间" + new Date());
    }
}

(5)创建spring配置文件:声明对象,把对象交给容器统一管理,声明对象可以使用注解或者xml配置文件的标签

  • 声明目标类对象
  • 声明切面类对象
  • 声明aspectJ框架中的自动代理生成器标签
    • 自动代理生成器:用来完成代理对象的自动创建功能的

在resources目录下新建applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--①声明目标对象-->
    <bean id="someService" class="com.st.ba01.SomeServiceImpl"/>

    <!--②声明切面类对象-->
    <bean id="myAspect" class="com.st.ba01.MyAspect"/>

    <!--③声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构。
        <aop:aspectj-autoproxy/>会把spring容器中所有目标对象,一次性生成代理对象
    -->
    <aop:aspectj-autoproxy/>


</beans>

(6)创建测试类,从spring容器中获取目标对象(实际就是代理对象),通过代理执行方法,实现aop功能增强。

public class MyTest {

    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext app = new ClassPathXmlApplicationContext(config);
        SomeService someService = (SomeService) app.getBean("someService");

        //jdk动态代理生成的对象名称
        System.out.println(someService.getClass().getName());

        //通过代理对象执行方法,实现目标方法执行时,增强了功能
        someService.doSome("李四", 20);
    }
}

(7)输出结果:

image-20220619180617905

3.5.3.2 注意事项

注意:

  1. 对于一个业务方法,可以配置多个切面

  2. 一个切面也可以配置到多个业务方法中

  3. 切面声明的@Before(value=“xxxx”),xxxx中如果写错,则目标类对象不会受影响

  4. @Aspect:是aspectJ框架中的注解

    • 作用:表示当前类是切面类
    • 切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
    • 位置:在类定义的上面
  5. 方法的定义要求:

    • 公共方法public
    • 方法没有返回值
    • 方法名称自定义
    • 可以有参数 也可以没有;若有参数,参数不是自定义的,有几个参数类型可以使用

3.6 常用注解

3.6.1 @Before前置通知 - 方法有JoinPoint 参数

  • 作用:在目标方法执行之前执行,被注解为前置通知的方法
  • 语法:@Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))")
  • 注解属性:
    • value :切入点表达式
  • 方法参数:
    • JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
  • 代码:
@Aspect
public class MyAspect {
    @Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore4(JoinPoint jp){ //参数位置
        //获取方法的完整定义
        System.out.println("方法的签名:" + jp.getSignature());
        System.out.println("方法的名称:" + jp.getSignature().getName());

        Object[] args = jp.getArgs();
        for(Object arg:args){
            System.out.println("参数包含:"+ arg);
        }

        System.out.println("前置通知,切面功能,在目标方法之前输出时间" + new Date());
    }
}
  • 测试代码
@Test
public void test01(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");


  someService.doSome("李四", 20);
}

运行结果:

image-20220619211619242

### 3.6.2 @AfterReturning 后置通知 - 注解有 returning 属性
  • 作用:在目标方法执行之后执行,被注解为后置通知的方法,由于是目标方法之后执行,所以可获取目标方法返回值。
  • 语法:@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res")
  • 注解属性:
    • value :切入点表达式
    • returning:自定义的变量,表示目标方法的返回值,自定义变量名必须和通知方法的形参名一样
  • 方法参数:
    • JoinPoint 【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
    • Object res【可选】:是目标方法执行后的返回值,根据返回值做切面的功能处理,推荐使用Object
  • 代码:

MyAspect类:

```java

@AfterReturning(value = “execution(* *…SomeServiceImpl.doOther2(…))”, returning = “student”)
public void myAfterReturningStudent(Object student){
System.out.println(“后置通知,获取的返回值是:”+student.toString());
Student student1 = (Student) student;
student1.setName(“王五”); //对返回的对象 进行修改
}
```

SomeServiceImpl类:

public class SomeServiceImpl implements SomeService {
    public Student doOther2() {
        Student student = new Student();
        student.setName("李四");
        student.setAge(20);
        return student;
    }
}
  • 测试代码
@Test
public void test02(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");
  Student student = someService.doOther2();
  System.out.println(student.toString());
}

运行结果:

image-20220619215349584

3.6.3 @Around 环绕通知-有 ProceedingJoinPoint 参数

  • 作用:功能最强的通知,在目标方法执行之前之后执行,被注解为环绕增强的方法要有返回值
  • 语法:@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
  • 注解属性:
    • value :切入点表达式
  • 方法参数:
    • ProceedingJoinPoint pjp【必选】:等同于Method,执行目标方法的。 pjp.proceed == method.invoke()
  • 方法返回值:Object,目标方法的执行结果,可以被修改
  • 代码:

MyAspect类:

@Aspect
public class MyAspect {
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround2(ProceedingJoinPoint pjp) throws Throwable {
        String name="";
        //获取第一个参数值
        Object[] args = pjp.getArgs();
        if(args!=null && args.length > 1){
            Object arg = args[0];
            name = (String)arg;
        }
        //实现环绕通知,前置通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间" + new Date());
      
        //1.目标方法调用,并获取返回值result (这里是根据返回值进行判断)
        if("张三".equals(name)){ 
            result = pjp.proceed();// == method.invoke
        }
        //2. 实现环绕通知,后置通知
        System.out.println("环绕通知:在目标方法之后,输出时间" + new Date());
        //修改目标方法执行结果,影响方法的最后的调用结果
        if(result != null){
            result = "新的返回值:hello aspectJ aop";
        }
        return result;
    }
}

SomeServiceImpl类:

public class SomeServiceImpl implements SomeService {
    @Override
    public String doFirst(String name, Integer age) {
        System.out.println("=====业务方法doFirst()=====");
        return "doFirst";
    }
}
  • 测试代码
@Test
public void test02(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");
  //        String str = someService.doFirst("李四", 20);
  String str = someService.doFirst("张三", 20);
  System.out.println(str);
}

运行结果:

image-20220619221523418

3.6.4 @AfterThrowing 异常通知-注解中有 throwing 属性

  • 作用:在目标方法抛出异常后执行。可以做异常的监控程序,监控目标方法执行时是不是有异常,若存在异常,可以发送邮件,短信进行通知
  • 语法:@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
  • 注解属性:
    • value :切入点表达式
    • throwing:自定义变量,表示目标方法抛出的异常对象,变量名必须和方法的参数名一样
  • 方法参数:
    • JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
    • Throwable ex【必选】:参数名称为 throwing 指定的 名称,表示发生的异常对象。(Throwable 实现类即可)
  • 方法返回值:void
  • 代码:

MyAspect类:

@Aspect
public class MyAspect {
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
    public void myAfterThrowing(Exception ex){
        System.out.println("异常通知" + ex.getMessage());
    }
}

SomeServiceImpl类,设置一个错误:

@Override
public void doSecond() {
  System.out.println("业务方法doSecond" + 10/0);   //会发生错误
}
  • 测试代码
@Test
public void test01(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");
  someService.doSecond();
}

运行结果:

image-20220619222433964

3.6.5 @After 最终通知

  • 作用:无论目标方法是否抛出异常,该增强均会被执行在目标方法之后执行的
  • 语法:@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
  • 注解属性:
    • value :切入点表达式
  • 方法参数:
    • JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
  • 方法返回值:void
  • 代码:

MyAspect类:

@Aspect
public class MyAspect {
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public void myAfter(){
        System.out.println("执行最终通知的");
    }
}

SomeServiceImpl类:

@Override
public void doThird() {
  System.out.println("业务方法doThird"+ 10/0);
}
  • 测试代码:
@Test
public void test01(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");
  someService.doThird();
}

运行结果:

image-20220619222935603

3.6.6 @Pointcut 定义切入点

  • 作用:当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。 AspectJ 提供了**@Pointcut 注解,用于定义 execution 切入点表达式**
  • 语法:@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
  • 注解属性:
    • value :切入点表达式
  • 方法参数:无
  • 方法返回值:void
  • 代码:

MyAspect类:

@Aspect
public class MyAspect {
    @After(value = "myPt()")
    public void myAfter(){
        System.out.println("执行最终通知的");
    }

    @Before(value = "myPt()")
    public void myBefore(){
        System.out.println("前置通知");
    }

    @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
    private void myPt(){
        //无需代码
    }
}

SomeServiceImpl类:

@Override
public void doThird() {
  System.out.println("业务方法doThird");
}
  • 测试代码
@Test
public void test01(){
  String config = "applicationContext.xml";
  ApplicationContext app = new ClassPathXmlApplicationContext(config);
  SomeService someService = (SomeService) app.getBean("someService");
  someService.doThird();
}

运行结果:

image-20220619223634976
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

St_up

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值