2.3 Spring系列教程3-Spring AOP

Spring的AOP模块是Spring框架体系中十分重要的内容,该模块一般适用于具有横切逻辑的场景,如访问控制、事务管理和性能监控等,本章将对Spring AOP的相关知识进行详细讲解。

一 Spring AOP介绍

1.1 AOP概述

​ AOP的全称是Aspect Oriented Programming,即面向切面编程。和OOP不同,AOP主张将程序中相同的业务逻辑进行横向隔离,并将重复的业务逻辑抽取到一个独立的模块中,以达到提高程序可重用性和开发效率的目的。

​ 在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),同样的代码仍然会分散到各个方法中。

例如,订单系统中有添加订单信息、更新订单信息和删除订单信息3个方法,这3个方法中都包含事务管理业务代码,订单系统的逻辑如图所示。

image-20240930173948819

  • AOP面向切面编程的优势

    ​ 由订单系统可知,添加订单信息、修改订单信息、删除订单信息的方法体中都包含事务管理的业务逻辑,这就带来了一定数量的重复代码并使程序的维护成本增加。基于AOP的面向切面编程,可以为此类问题提供解决方案,AOP可以将事务管理的业务逻辑从这三个方法体中抽取到一个可重用的模块,进而降低横向业务逻辑之间的耦合,减少重复代码。AOP的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多地关注其他业务逻辑的实现,不但提高了开发效率,又增强了代码的可维护性。

1.2 Spring AOP术语

术语名称英文说明
切面Aspect切面是指关注点形成的类(关注点是指类中重复的代码),通常是指封装的、用于横向插入系统的功能类(如事务管理、日志记录等)。在实际开发中,该类被Spring容器识别为切面,需要在配置文件中通过<bean>元素指定
连接点Joinpoint连接点是程序执行过程中某个特定的节点,例如,某方法调用时或处理异常时。在Spring AOP中,一个连接点通常是一个方法的执行。
切入点Pointcut当某个连接点满足预先指定的条件时,AOP就能够定位到这个连接点,在连接点处插入切面,该连接点也就变成了切入点。
通知/增强处理Advice通知/增强处理就是插入的切面程序代码。可以将通知/增强处理理解为切面中的方法,它是切面的具体实现
目标对象Target目标对象是指被插入切面的方法,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象
织入Weaving将切面代码插入到目标对象上,从而生成代理对象的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。
代理Proxy将通知应用到目标对象之后,程序动态创建的通知对象,就称为代理。代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,可以采用调用原类相同的方式调用代理类。
引介Introduction引介是一种特殊的通知,它为目标对象添加一些属性和方法。这样,即使一个业务类原本没有实现某一个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

二 Spring AOP的实现机制

2.1 JDK动态代理

默认情况下,Spring AOP使用JDK动态代理,JDK动态代理是通过java.lang.reflect.Proxy 类实现的,可以调用Proxy类的newProxyInstance()方法创建代理对象。JDK动态代理可以实现无侵入式的代码扩展,并且可以在不修改源代码的情况下,增强某些方法。

2.1.1 示例代码

Java代码位于SpringLearn/src/main/java/site/icefox/javaeelearn/Learn3/target21

  • UserDao.java

    package site.icefox.javaeelearn.Learn3.target21;
    
    public interface UserDao {
        void addUser();
    
        void deleteUser();
    }
    
  • UserDaoImpl.java

    package site.icefox.javaeelearn.Learn3.target21;
    
    import lombok.Data;
    
    @Data
    public class UserDaoImpl implements UserDao {
        public void addUser() {
            System.out.println("添加用户");
        }
    
        public void deleteUser() {
            System.out.println("删除用户");
        }
    }
    
  • MyProxy.java

    package site.icefox.javaeelearn.Learn3.target21;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class MyProxy implements InvocationHandler {
        private UserDao userDao;
    
        public Object createProxy(UserDao userDao) {
            this.userDao = userDao;
            ClassLoader classLoader = MyProxy.class.getClassLoader(); // 1.类加载器
            Class[] classes = userDao.getClass().getInterfaces(); // 2.被代理对象实现的所有接口
            return Proxy.newProxyInstance(classLoader, classes, this); // 3.返回代理对象
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            MyAspect.check_Permissions();
            Object res = method.invoke(userDao, args);
            MyAspect.log();
            return res;
        }
    }
    
    
  • MyAspect.java

    package site.icefox.javaeelearn.Learn3.target21;
    
    public class MyAspect {// 切面类:存在多个通知Advice(增强的方法)
        public static void check_Permissions(){
            System.out.println("模拟检查权限...");		}
        public static void log(){
            System.out.println("模拟记录日志...");		}
    }
    
    
  • JDKTest.java

    package site.icefox.javaeelearn.Learn3.target21;
    
    public class JDKTest {
        public static void main(String[] args) {
            MyProxy jdkProxy = new MyProxy();// 创建代理对象
            UserDao userDao = new UserDaoImpl();// 创建目标对象
            // 从代理对象中获取增强后的目标对象
            UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao);
            // 执行方法
            userDao1.addUser();
            userDao1.deleteUser();
        }
    }
    
2.1.2 预期结果
模拟检查权限...
添加用户
模拟记录日志...
模拟检查权限...
删除用户
模拟记录日志...

进程已结束,退出代码为 0

2.2 CGLib动态代理

JDK动态代理存在缺陷,它只能为接口创建代理对象,当需要为类创建代理对象时,就需要使用CGLib(Code Generation Library)动态代理,CGLib动态代理不要求目标类实现接口,它采用底层的字节码技术,通过继承的方式动态创建代理对象。

2.2.1 示例代码

Java代码位于SpringLearn/src/main/java/site/icefox/javaeelearn/Learn3/target22

  • CglibProxy.java

    package site.icefox.javaeelearn.Learn3.target22;
    
    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    public class CglibProxy implements MethodInterceptor {
    
        public Object createProxy(Object target) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(target.getClass());
            enhancer.setCallback(this);
            return enhancer.create();
        }
    
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            //创建一个切面
            MyAspect myAspect = new MyAspect();
            //前增强
            myAspect.check_Permissions();
            Object obj = methodProxy.invokeSuper(proxy, args);
            //后增强
            myAspect.log();
            return obj;
        }
    }
    
  • CglibTest.java

    package site.icefox.javaeelearn.Learn3.target22;
    
    public class CglibTest {
        public static void main(String[] args) {
            CglibProxy cglibProxy = new CglibProxy();
            UserDao userDao = new UserDao(); // 创建目标对象
            UserDao userDaoProxy = (UserDao) cglibProxy.createProxy(userDao); // 获取代理对象
    
            // 执行方法
            userDaoProxy.addUser();
            userDaoProxy.deleteUser();
        }
    }
    
  • MyAspect.java

    package site.icefox.javaeelearn.Learn3.target22;
    
    public class MyAspect {// 切面类:存在多个通知Advice(增强的方法)
    
        public void check_Permissions() {
            System.out.println("模拟检查权限...");
        }
    
        public void log() {
            System.out.println("模拟记录日志...");
        }
    }
    
  • UserDao.java

    package site.icefox.javaeelearn.Learn3.target22;
    
    public class UserDao {
        public void addUser() {
            System.out.println("添加用户");
        }
    
        public void deleteUser() {
            System.out.println("删除用户");
        }
    }
    
2.2.2 预期结果
模拟检查权限...
添加用户
模拟记录日志...
模拟检查权限...
删除用户
模拟记录日志...

进程已结束,退出代码为 0

三 基于XML的AOP实现

使用AOP代理对象的好处

因为Spring AOP中的代理对象由IoC容器自动生成,所以开发者无须过多关注代理对象生成的过程,只需选择连接点、创建切面、定义切点并在XML文件中添加配置信息即可。Spring提供了一系列配置Spring AOP的XML元素。

3.1 配置Spring AOP的XML元素

元素描述
<aop:config>Spring AOP配置的根元素
<aop:aspect>配置切面
<aop:advisor>配置通知器
<aop:pointcut>配置切点
<aop:before>配置前置通知,在目标方法执行前实施增强,可以应用于权限管理等功能
<aop:after>配置后置通知,在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能
<aop:around>配置环绕方式,在目标方法执行前后实施增强,可以应用于日志、事务管理等功能
<aop:after-returning>配置返回通知,在目标方法成功执行之后调用通知
<aop:after-throwing>配置异常通知,在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能

3.2 配置说明

3.2.1 配置切面

在Spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,因此,在使用<aop:aspect>元素之前,要在配置文件中先定义一个普通的Spring Bean。Spring Bean定义完成后,通过<aop:aspect>元素的ref属性即可引用该Bean。配置<aop:aspect>元素时,通常会指定id和ref两个属性。

  • aop:aspect元素的id属性和ref属性的描述:

    属性说明
    id用于定义该切面的唯一标识
    ref用于引用普通的Spring Bean
3.2.2 配置切入点

在Spring的配置文件中,切入点是通过<aop:pointcut>元素来定义的。当<aop:pointcut>元素作为<aop:config>元素的子元素定义时,表示该切入点是全局的,它可被多个切面共享;当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。定义<aop:pointcut>元素时,通常会指定id、expression属性。

  • <aop:pointcut>元素的id属性和expression属性描述:

    属性说明
    id用于指定切入点的唯一标识
    expression用于指定切入点关联的切入点表达式
  • Spring AOP切入点表达式的基本格式

    execution(
        modifiers-pattern?
        ret-type-pattern
        declaring-type-pattern?
        name-pattern(param-pattern)
        throws-pattern?
    )
    
  • execution表达式各部分参数说明

    参数名称说明
    modifiers-pattern表示定义的目标方法的访问修饰符,如public、private等。
    ret-type-pattern表示定义的目标方法的返回值类型,如void、String等。
    declaring-type-pattern表示定义的目标方法的类路径,如com.itheima.jdk.UserDaoImpl。
    name-pattern表示具体需要被代理的目标方法,如add()方法。
    param-pattern表示需要被代理的目标方法包含的参数,本章示例中目标方法参数都为空。
    throws-pattern表示需要被代理的目标方法抛出的异常类型。
3.2.3 配置通知

在Spring的配置文件中,使用<aop:aspect>元素配置了5种常用通知,分别为前置通知、后置通知、环绕通知、返回通知和异常通知。

  • <aop:aspect>元素的常用属性

    属性描述
    pointcut该属性用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时织入该通知。
    pointcut-ref该属性指定一个已经存在的切入点名称,如配置代码中的myPointCut。通常pointcut和pointcut-ref两个属性只需要使用其中一个即可。
    method该属性指定一个方法名,指定将切面Bean中的该方法转换为增强处理。
    throwing该属性只对<after-throwing>元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常。
    returning该属性只对<after-returning>元素有效,它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值。

3.3 示例代码

Java代码位于SpringLearn/src/main/java/site/icefox/javaeelearn/Learn3/target30

xml代码位于SpringLearn/src/main/resources/learn3/target30

  • TestXml.java

    package site.icefox.javaeelearn.Learn3.target30;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TestXml {
        public static void main(String[] args) {
            ApplicationContext context = new
                    ClassPathXmlApplicationContext("learn3/target30/applicationContext.xml");
            UserDao userDao = context.getBean("userDao", UserDao.class);
            userDao.delete();
            System.out.println();
            userDao.insert();
            System.out.println();
            userDao.select();
            System.out.println();
            userDao.update();
        }
    }
    
  • UserDao.java

    package site.icefox.javaeelearn.Learn3.target30;
    
    public interface UserDao {
        void insert();
        void delete();
        void update();
        void select();
    }
    
  • UserDaoImpl.java

    package site.icefox.javaeelearn.Learn3.target30;
    
    public class UserDaoImpl implements UserDao {
        public void insert() {
            System.out.println("添加用户信息");
        }
    
        public void delete() {
            System.out.println("删除用户信息");
        }
    
        public void update() {
            System.out.println("更新用户信息");
        }
    
        public void select() {
            System.out.println("查询用户信息");
        }
    }
    
  • XmlAdvice.java

    package site.icefox.javaeelearn.Learn3.target30;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    public class XmlAdvice {
        //前置通知
        public void before(JoinPoint joinPoint) {        //使用JoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.print("这是前置通知!");
            System.out.print("目标类:" + joinPoint.getTarget());
            System.out.println(",被织入增强处理的目标方法为:" + joinPoint.getSignature().getName());
        }
    
        //返回通知
        public void afterReturning(JoinPoint joinPoint) {//使用JoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.print("这是返回通知(方法不出现异常时调用)!");
            System.out.println("被织入增强处理的目标方法为:" + joinPoint.getSignature().getName());
        }
    
        /**
         * 环绕通知
         * ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
         * 1.必须是Object类型的返回值
         * 2.必须接收一个参数,类型为ProceedingJoinPoint
         * 3.必须throws Throwable
         */
        public Object around(ProceedingJoinPoint point) throws Throwable {//使用ProceedingJoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.println("这是环绕通知之前的部分!");
            //调用目标方法
            Object object = point.proceed();
            System.out.println("这是环绕通知之前的部分!");
            return object;
        }
    
        //异常通知
        public void afterException() {
            System.out.println("异常通知!");
        }
    
        //后置通知
        public void after() {
            System.out.println("这是后置通知!");
        }
    }
    

3.4 预期结果

这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target30.UserDaoImpl@7adf16aa,被织入增强处理的目标方法为:delete
这是环绕通知之前的部分!
删除用户信息
这是后置通知!
这是环绕通知之前的部分!
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:delete

这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target30.UserDaoImpl@7adf16aa,被织入增强处理的目标方法为:insert
这是环绕通知之前的部分!
添加用户信息
这是后置通知!
这是环绕通知之前的部分!
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:insert

这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target30.UserDaoImpl@7adf16aa,被织入增强处理的目标方法为:select
这是环绕通知之前的部分!
查询用户信息
这是后置通知!
这是环绕通知之前的部分!
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:select

这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target30.UserDaoImpl@7adf16aa,被织入增强处理的目标方法为:update
这是环绕通知之前的部分!
更新用户信息
这是后置通知!
这是环绕通知之前的部分!
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:update

进程已结束,退出代码为 0

四 基于注解的AOP实现

4.1 Spring AOP的注解

元素描述
@Aspect配置切面
@Pointcut配置切点
@Before配置前置通知
@After置后置通知
@Around配置环绕方式
@AfterReturning配置返回通知
@AfterThrowing配置异常通知

4.2 示例代码

Java代码位于SpringLearn/src/main/java/site/icefox/javaeelearn/Learn3/target40

xml代码位于SpringLearn/src/main/resources/learn3/target40

  • AnnoAdvice.java

    package site.icefox.javaeelearn.Learn3.target40;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    
    @Aspect
    public class AnnoAdvice {
        //切点
        @Pointcut("execution( * site.icefox.javaeelearn.Learn3.target40.UserDaoImpl.*(..))")
        public void poincut() {
        }
    
        //前置通知
        @Before("poincut()")
        public void before(JoinPoint joinPoint) {        //使用JoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.print("这是前置通知!");
            System.out.print("目标类:" + joinPoint.getTarget());
            System.out.println(",被织入增强处理的目标方法为:" + joinPoint.getSignature().getName());
        }
    
        //返回通知
        @AfterReturning("poincut()")
        public void afterReturning(JoinPoint joinPoint) {//使用JoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.print("这是返回通知(方法不出现异常时调用)!");
            System.out.println("被织入增强处理的目标方法为:" + joinPoint.getSignature().getName());
        }
    
        /**
         * 环绕通知
         * ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
         * 1.必须是Object类型的返回值
         * 2.必须接收一个参数,类型为ProceedingJoinPoint
         * 3.必须throws Throwable
         */
        @Around("poincut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {//使用ProceedingJoinPoint接口实例作为参数获得目标对象的类名和方法名
            System.out.println("这是环绕通知之前的部分!");
            //调用目标方法
            Object object = point.proceed();
            System.out.println("这是环绕通知之前的部分!");
            return object;
        }
    
        //异常通知
        @AfterThrowing("poincut()")
        public void afterException() {
            System.out.println("异常通知!");
        }
    
        //后置通知
        @After("poincut()")
        public void after() {
            System.out.println("这是后置通知!");
        }
    }
    
  • TestAnnotation.java

    package site.icefox.javaeelearn.Learn3.target40;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TestAnnotation {
        public static void main(String[] args) {
            ApplicationContext context = new
                    ClassPathXmlApplicationContext("learn3/target40/applicationContext-Anno.xml");
            UserDao userDao = context.getBean("userDao", UserDao.class);
            userDao.delete();
            System.out.println();
            userDao.insert();
            System.out.println();
            userDao.select();
            System.out.println();
            userDao.update();
        }
    }
    
    
  • UserDao.java

    package site.icefox.javaeelearn.Learn3.target40;
    
    public interface UserDao {
        void insert();
    
        void delete();
    
        void update();
    
        void select();
    }
    
  • UserDaoImpl.java

    package site.icefox.javaeelearn.Learn3.target40;
    
    public class UserDaoImpl implements UserDao {
        public void insert() {
            System.out.println("添加用户信息");
        }
    
        public void delete() {
            System.out.println("删除用户信息");
        }
    
        public void update() {
            System.out.println("更新用户信息");
        }
    
        public void select() {
            System.out.println("查询用户信息");
        }
    }
    
  • applicationContext-Anno.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 -->
        <bean name="userDao" class="site.icefox.javaeelearn.Learn3.target40.UserDaoImpl"/>
        <bean name="AnnoAdvice" class="site.icefox.javaeelearn.Learn3.target40.AnnoAdvice"/>
        <!-- 开启@aspectj的自动代理支持 -->
        <aop:aspectj-autoproxy/>
    </beans>
    

4.3 预期结果

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:delete
删除用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:delete
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:insert
添加用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:insert
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:select
查询用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:select
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:update
更新用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:update
这是后置通知!
这是环绕通知之前的部分!

进程已结束,退出代码为 0

五 结语

a88,被织入增强处理的目标方法为:delete
删除用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:delete
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:insert
添加用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:insert
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:select
查询用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:select
这是后置通知!
这是环绕通知之前的部分!

这是环绕通知之前的部分!
这是前置通知!目标类:site.icefox.javaeelearn.Learn3.target40.UserDaoImpl@24faea88,被织入增强处理的目标方法为:update
更新用户信息
这是返回通知(方法不出现异常时调用)!被织入增强处理的目标方法为:update
这是后置通知!
这是环绕通知之前的部分!

进程已结束,退出代码为 0


## 五 结语

本章主要讲解了Spring中的AOP。首先介绍了Spring AOP,包括Spring AOP的概述和Spring AOP的术语;然后讲解了Spring AOP的实现机制,包括JDK动态代理和CGLib动态代理;接着讲解了基于XML的AOP实现,并使用案例的方式实现了基于XML文件的AOP;最后讲解了基于注解的AOP实现。通过本章的学习,读者可以对Spring AOP有基础的了解,为框架开发奠定基础。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

喝Java也加唐~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值