Spring 事务源码(5)—TransactionInterceptor事务拦截器与事务的AOP增强实现

  基于最新Spring 5.x,详细介绍了Spring 事务源码,包括TransactionInterceptor事务拦截器与事务的AOP增强实现的总体流程。

  此前我们讲解了BeanFactoryTransactionAttributeSourceAdvisor 注解事务通知器的源码,现在我们来学习TransactionInterceptor事务拦截器的源码。这个类是Spring 事务的核心实现类,我们会重点讲解。

Spring 事务源码 系列文章

Spring 5.x 源码汇总

Spring 事务源码(1)—<tx:advice/>事务标签源码解析

Spring 事务源码(2)—<tx:annotation-driven/>事务标签源码解析

Spring 事务源码(3)—@EnableTransactionManagement事务注解源码解析

Spring 事务源码(4)—BeanFactoryTransactionAttributeSourceAdvisor注解事务通知器源码解析

Spring 事务源码(5)—TransactionInterceptor事务拦截器与事务的AOP增强实现

Spring 事务源码(6)—createTransactionIfNecessary处理事务属性并尝试创建事务【两万字】

Spring 事务源码(7)—事务的completeTransactionAfterThrowing回滚、commitTransactionAfterReturning提交以及事务源码总结【一万字】

1 TransactionInterceptor的概述

  创建了代理对象之后,在实际调用方法时,就会走代理对象的调用逻辑,对于JDK代理对象,JdkDynamicAopProxy.invoke方法就是调用的入口,对于CGLIB代理对象,DynamicAdvisedInterceptor.intercept方法就是调用入口。
  在调用代理方法时,首先会从通知器链中获取适合该方法的拦截器链集合(通过通知器获取拦截器),这些拦截器中就保存着对应方法的AOP增强的逻辑(比如各种通知方法的调用点)。随后会通过责任链模式和方法递归调用来实现方法的代理和增强的逻辑,这一点类似于Servlet中的Filter以及Spring MVC中的HandlerInterceptor(它和Spring AOP的拦截器Interceptor不是同一个体系)。调用链的最底层就是目标方法,此前调用的就是前置通知,此后调用的就是后置通知,异常调用的就是异常通知,最终调用的就是最终通知!
  以上,我们简单回顾了Spring AOP调用时的逻辑,Spring事务也是走的这一套逻辑,而这些逻辑的通用源码我们在此前就讲过了,在此不再赘述。我们主要是学习Spring事务的TransactionInterceptor拦截器源码。
  TransactionInterceptor拦截器的uml类图如下:
在这里插入图片描述

  TransactionAspectSupport是声明式事务切面的的基础实现,持有一个TransactionManager,提供了Spring声明式事务的核心实现,比如各种操作事务的模版方法!
  TransactionInterceptor继承了TransactionAspectSupport,因此具有处理事务的功能,同时还实现了MethodInterceptor接口,它还作为一个AOP拦截器。
  拦截器链中每个拦截器的调用入口就是内部的invoke方法,该方法就是对某个方法进行事务增强的入口,因此主要看invoke方法的实现逻辑!

2 invoke执行事务增强

  该方法就是TransactionInterceptor事务拦截器的执行入口,内部将会调用父类TransactionAspectSupport的invokeWithinTransaction方法。
  调用invokeWithinTransaction方法的时候,第三个参数类型为InvocationCallback的实例,实际上传递的是一个方法引用invocation::proceed,因此调用InvocationCallback#proceedWithInvocation方法时,实际上就是的调用invocation#proceed方法,也就是继续向后执行其他拦截器或者目标方法的逻辑,后面我们会见到。

/**
 1. TransactionInterceptor的方法
 2. <p>
 3. Spring声明式事务的实现入口
 4.  5. @param invocation 方法调用服务,内部包含了代理对象、目标对象、方法参数、目标类型、该方法的拦截器链集合。
 6. @return 执行结果
 */
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    //计算出目标类型,就是被代理的类,可能是null。TransactionAttributeSource应该通过目标方法和目标类得到
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    //调用父类的invokeWithinTransaction方法实现事务的逻辑
    //在方法执行成功之后,将会执行回调函数,这里的逻辑就是继续向后执行invocation的proceed方法
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

3 invokeWithinTransaction执行并处理事务

  该方法就是Spring事务的核心处理方法,该方法实现了从事务的获取、方法的执行、事务的回滚、事物的提交等一系列完整的逻辑。
   看懂了该方法,就看懂了Spring声明式事务处理的真正实现 ,它的主要逻辑如下:

  1. 调用getTransactionAttributeSource方法,获取事务属性源TransactionAttributeSource;
  2. 调用属性源的getTransactionAttribute方法,获取适用于该方法的事务属性TransactionAttribute,如果获取结果为null,则该方法为非事务方法。
  3. 调用determineTransactionManager方法,确定给定事务属性所使用的特定事务管理器TransactionManager。
  4. 调用createTransactionIfNecessary方法,根据给定的事务属性、事务管理器、方法连接点描述字符串(全限定方法名)信息创建一个事务信息TransactionInfo对象,该方法中将会解析各种事务属性,应用不同的处理流程,比如创建、开启新事物、加入事务、抛出异常等,该方法处理完毕,那么Spring声明式事物的前半部分流程就执行完了。
  5. 在一个try块中,调用invocation.proceedWithInvocation()方法,实际上是执行invocation的proceed方法,即这里将会继续向后调用链中的下一个拦截器,最后还会导致目标方法的调用(执行业务逻辑),然后会倒序返回。
  6. 在catch块中,调用completeTransactionAfterThrowing方法,处理执行过程中抛出的异常,该方法可能会导致事务的回滚或者提交,随后抛出该异常。
  7. 在finally块中,调用cleanupTransactionInfo方法,即无论事务正常还是异常完成,都会走这一步,用于清除当前线程绑定的事务信息、恢复老的事务信息绑定
  8. 后面就是事务正常完成之后的逻辑。在目标方法成功执行之后,调用返回结果之前提交事务commitTransactionAfterReturning方法,用于在返回结果之前尝试提交事务,但是如果TransactionStatus.isRollbackOnly()方法被设置为true,那么仍然会回滚
  9. 事务正常处理完毕,返回执行结果,Spring声明式事务处理到此结束!
/**
 * TransactionAspectSupport的方法
 * <p>
 * 用于根据各种配置以及事务管理器实现事务处理
 *
 * @param method      正在调用的目标方法
 * @param targetClass 正在调用的方法的目标类
 * @param invocation  方法调用服务,用于向后执行AOP调用
 * @return 该方法的返回值
 * @throws Throwable 从目标调用传播的异常
 */
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
                                         final InvocationCallback invocation) throws Throwable {
    /*
     * 1 获取事务属性源,也就是transactionAttributeSource属性,这个属性源在创建bean定义的时候就被设置了。
     *
     * 基于注解的属性源是AnnotationTransactionAttributeSource
     * 基于XML标签的属性源是NameMatchTransactionAttributeSource
     */
    TransactionAttributeSource tas = getTransactionAttributeSource();
    /*
     * 2 调用属性源的getTransactionAttribute方法获取适用于该方法的事务属性,如果TransactionAttribute为null,则该方法为非事务方法。
     *
     * 对于AnnotationTransactionAttributeSource,它的getTransactionAttribute
     * 方法我们在此前的BeanFactoryTransactionAttributeSourceAdvisor部分就讲过了。
     * 对于NameMatchTransactionAttributeSource,它的getTransactionAttribute就是通过方法名进行匹配。
     */
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);

    /*
     * 3 确定给定事务属性所使用的特定事务管理器。
     */
    final TransactionManager tm = determineTransactionManager(txAttr);
    /*这里用于支持Spring5的响应式编程,先不管……*/
    if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
        ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
            if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && TransactionAspectSupport.KotlinDelegate.isSuspend(method)) {
                throw new TransactionUsageException(
                        "Unsupported annotated transaction on suspending function detected: " + method +
                                ". Use TransactionalOperator.transactional extensions instead.");
            }
            ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
            if (adapter == null) {
                throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                        method.getReturnType());
            }
            return new ReactiveTransactionSupport(adapter);
        });
        return txSupport.invokeWithinTransaction(
                method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
    }
    //强制转换为PlatformTransactionManager,如果事务管理器不是PlatformTransactionManager类型,将会抛出异常
    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    //获取方法的信息,也就是此前设置的DefaultTransactionAttribute中的descriptor,或者方法的全路径类名,主要用于记录日志
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    /*
     * 如果事务属性为null,或者获取事务管理器不是回调事务管理器,那么走下面的逻辑
     * 这是最常见的标准声明式事务的逻辑,比如DataSourceTransactionManager
     */
    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        /*
         * 5 根据给定的事务属性、事务管理器、方法连接点描述字符串(全限定方法名)信息创建一个事务信息对象
         * 将会解析各种事务属性,应用不同的流程,比如创建新事物、加入事务、抛出异常等
         */
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
        //
        Object retVal;
        try {
            /*
             * 5 invocation参数传递的是一个lambda表达式,传递的是一个方法调用invocation::proceed
             * 因此这里的proceedWithInvocation实际上是执行invocation的proceed方法
             * 即这里将会继续向后调用链中的下一个拦截器,最后还会导致目标方法的调用(执行业务逻辑),然后会倒序返回
             */
            retVal = invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            /*
             * 6 处理执行过程中抛出的异常,可能会有事务的回滚或者提交
             */
            completeTransactionAfterThrowing(txInfo, ex);
            //抛出异常
            throw ex;
        } finally {
            /*
             * 7 清除当前绑定的事务信息、恢复老的事务信息绑定
             * 无论正常还是异常完成,都会走这一步
             */
            cleanupTransactionInfo(txInfo);
        }
        //正常完成之后的逻辑

        //类路径上存在Vavr库时的处理,这个需要引入外部依赖,因此一般都是不存在的
        if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
            // Set rollback-only in case of Vavr failure matching our rollback rules...
            TransactionStatus status = txInfo.getTransactionStatus();
            if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
            }
        }
        /*
         * 7 目标方法成功执行之后,返回结果之前提交事务
         */
        commitTransactionAfterReturning(txInfo);
        /*
         * 8 事务正常处理完毕,返回执行结果
         */
        return retVal;
    }
    /*
     * 如果txAttr不为null并且事务管理器属于CallbackPreferringPlatformTransactionManager,走下面的逻辑
     * 只有使用WebSphereUowTransactionManager时才会走这个逻辑,不常见,甚至几乎见不到
     */
    else {
        Object result;
        final ThrowableHolder throwableHolder = new ThrowableHolder();

        // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
        try {
            result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                try {
                    Object retVal = invocation.proceedWithInvocation();
                    if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                        // Set rollback-only in case of Vavr failure matching our rollback rules...
                        retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                    }
                    return retVal;
                } catch (Throwable ex) {
                    if (txAttr.rollbackOn(ex)) {
                        // A RuntimeException: will lead to a rollback.
                        if (ex instanceof RuntimeException) {
                            throw (RuntimeException) ex;
                        } else {
                            throw new ThrowableHolderException(ex);
                        }
                    } else {
                        // A normal return value: will lead to a commit.
                        throwableHolder.throwable = ex;
                        return null;
                    }
                } finally {
                    cleanupTransactionInfo(txInfo);
                }
            });
        } catch (ThrowableHolderException ex) {
            throw ex.getCause();
        } catch (TransactionSystemException ex2) {
            if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                ex2.initApplicationException(throwableHolder.throwable);
            }
            throw ex2;
        } catch (Throwable ex2) {
            if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
            }
            throw ex2;
        }

        // Check result state: It might indicate a Throwable to rethrow.
        if (throwableHolder.throwable != null) {
            throw throwableHolder.throwable;
        }
        return result;
    }

}


@Nullable
private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
    //转换为PlatformTransactionManager
    if (transactionManager == null || transactionManager instanceof PlatformTransactionManager) {
        return (PlatformTransactionManager) transactionManager;
    } else {
        //如果不是null且不是PlatformTransactionManager类型就抛出异常
        throw new IllegalStateException(
                "Specified transaction manager is not a PlatformTransactionManager: " + transactionManager);
    }
}

4 getTransactionAttribute获取事务属性

  该方法用于回去当前方法匹配的事务属性源。
  对于AnnotationTransactionAttributeSource,它的getTransactionAttribute方法我们在此前的BeanFactoryTransactionAttributeSourceAdvisor的文章部分就讲过了,就是通过这个方法来判断对应的方法是否可被事务代理来进行增强的,并且还会将结果缓存起来,这里再次调用该方法,可能会直接从缓存获取。
  对于NameMatchTransactionAttributeSource,它的getTransactionAttribute就是通过方法名进行匹配,支持"xxx*“, “*xxx” 和”*xxx*"模式匹配,它的属性源来自于<tx:method/>子标签的解析。 匹配规则如下,很简单:

  1. 如果能够直接通过方法名获取到对应的事务属性,那么直接返回。
  2. 否则,那么尝试通配符匹配,并且找到最佳匹配。最佳匹配规则是:如果存在多个匹配的key,那么谁的字符串长度最长,谁就是最佳匹配,就将是使用谁对应的事务属性,如果长度相等,那么后面匹配的会覆盖此前匹配的……emm。
  3. 如果还是没有匹配到,就返回null。
/*NameMatchTransactionAttributeSource的属性*/

/**
 * 一个<tx:method/>子标签将被解析为一个的键值对
 * <p>
 * key为方法名,支持*通配符
 * value是对应的TransactionAttribute
 */
private Map<String, TransactionAttribute> nameMap = new HashMap<>();


/**
 * NameMatchTransactionAttributeSource的方法
 * <p>
 * 获取目标方法的事务属性,坑能使通过XML的<tx:method/>标签配置的,也可能是通过事务注解比如@Transactional配置的
 * 如果返回null,那么说明当前方法为非事务方法。
 *
 * @param method      调用的方法
 * @param targetClass 目标类型
 * @return TransactionAttribute事务属性
 */
@Override
@Nullable
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    //如果方法不是用户声明的方法或者不是指向用户声明的方法,比如合成方法、GroovyObject方法等,那么直接返回null
    //请注意,尽管是合成的,桥接方法(method.isbridge())仍然被视为用户级方法,因为它们最终指向用户声明的泛型方法。
    if (!ClassUtils.isUserLevelMethod(method)) {
        return null;
    }

    //获取方法名
    String methodName = method.getName();
    //尝试寻找直接方法名称的匹配的属性源
    TransactionAttribute attr = this.nameMap.get(methodName);
    /*
     * 如果为null,那么尝试通配符匹配,并且找到最佳匹配
     * 最佳匹配规则是:
     *  如果存在多个匹配的key,那么谁的字符串长度最长,谁就是最佳匹配,就将是使用谁对应的事务属性
     *  如果长度相等,那么后面匹配的会覆盖此前匹配的……emm
     */
    if (attr == null) {
        //寻找最匹配的名称匹配。
        String bestNameMatch = null;
        //遍历beanName数组
        for (String mappedName : this.nameMap.keySet()) {
            //如果isMatch返回true,表示匹配当前mappedName
            //并且此前没有匹配其他bestNameMatch,或者此前匹配的bestNameMatch的长度小于等于当前匹配的mappedName的长度
            if (isMatch(methodName, mappedName) &&
                    (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) {
                //获取当前mappedName对应的事务属性
                attr = this.nameMap.get(mappedName);
                //bestNameMatch最佳匹配的beanName设置为当前匹配的mappedName
                bestNameMatch = mappedName;
            }
        }
    }
    //返回最佳匹配的
    return attr;
}


/**
 1. 如果给定的方法名称与映射的名称匹配,则返回true。
 2. <p>
 3. 默认实现将检查"xxx*", "*xxx" and "*xxx*"模式匹配,以及是否直接相等,可以在子类中重写。
 */
protected boolean isMatch(String methodName, String mappedName) {
    return PatternMatchUtils.simpleMatch(mappedName, methodName);
}

5 determineTransactionManager确定事务管理器

  该方法根据给定的事务属性获取对应的事务管理器。

  1. 如果事务属性中配置了qualifier,那么通过qualifier作为beanName去容器查找TransactionManager类型的事务管理器。找不到就会抛出异常,找到了就返回并且存入缓存中。
    1. qualifier一般就是指的基于@Transactional注解的value属性或者transactionManager属性。基于XML的<tx:method/>标签没有配置该属性。
  2. 如果拦截器中配置了transactionManagerBeanName属性,那么该属性同样作为事务管理器的beanName。那么通过transactionManagerBeanName去容器查找TransactionManager类型的事务管理器。该方法如果找不到对应的事务管理器就会抛出异常,找到了就返回并且会存入缓存中。
    1. transactionManagerBeanName一般就是指的基于XML的<tx:annotation-driven/>标签的transaction-manager属性
  3. 否则,将会查找事务管理器,这个就是最常见的逻辑(在没有指定事务管理器的情况下)。
    1. 首先获取拦截器中transactionManager,即手动设置的事务管理器。该属性一般是基于XML配置的<tx:advice/>标签的transaction-manager属性,或者基于@EnableTransactionManagement注解并且通过自定义TransactionManagementConfigurer配置类配置的
    2. 如果没有手动配置的事务管理器,那么尝试从容器中查找TransactionManager类型的bean,并且作为默认事务管理器这就是基于注解配置的最常见的逻辑,该方法如果找不到事务管理器或者找到多个事务管理器,那么将抛出异常。这里同样有一个缓存的设计,找到之后就存入缓存中。
/*TransactionAspectSupport的属性*/

/**
 * 事务管理器
 * 也就是XML的<tx:advice/>标签的transaction-manager属性
 * 或者基于@EnableTransactionManagement注解并且通过TransactionManagementConfigurer配置类获取的
 */
@Nullable
private TransactionManager transactionManager;

/**
 * 事务管理器beanName
 * <p>
 * 也就是XML的<tx:annotation-driven/>标签的transaction-manager属性
 */
@Nullable
private String transactionManagerBeanName;

/**
 * 事务管理器的缓存
 */
private final ConcurrentMap<Object, TransactionManager> transactionManagerCache =
        new ConcurrentReferenceHashMap<>(4);

/**
 * 用于存储默认事务管理器的key
 */
private static final Object DEFAULT_TRANSACTION_MANAGER_KEY = new Object();


/**
 * TransactionAspectSupport的方法
 * <p>
 * 确定给定事务属性所使用的特定事务管理器
 */
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
    // 如果没有事务属性,那么直接获取手动设置的事务管理器,可能返回null
    //编程式事务也会调用该方法,并且没有事务属性
    if (txAttr == null || this.beanFactory == null) {
        return getTransactionManager();
    }
    //获取限定符,也就是每一个@Transactional注解的value属性或者transactionManager属性
    String qualifier = txAttr.getQualifier();
    //如果设置了该属性,那么通过限定符从容器中查找指定的事务管理器
    //一般都是注解设置的,基于XML的<tx:method/>标签没有配置该属性
    if (StringUtils.hasText(qualifier)) {
        return determineQualifiedTransactionManager(this.beanFactory, qualifier);
    }
    //否则,如果设置了transactionManagerBeanName属性,那么通过限定符查找指定的事务管理器
    //一般都是基于XML的<tx:annotation-driven/>标签的transaction-manager属性
    else if (StringUtils.hasText(this.transactionManagerBeanName)) {
        return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
    }
    //否则,将会查找事务管理器,这个就是现在最常见的逻辑
    else {
        //获取手动设置的事务管理器
        TransactionManager defaultTransactionManager = getTransactionManager();
        //如果没有,那么获取默认的事务管理器
        if (defaultTransactionManager == null) {
            //从缓存中通过默认key尝试获取默认的事务管理器
            defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
            //如果为null
            if (defaultTransactionManager == null) {
                //那么尝试从容器中查找TransactionManager类型的bean,并且作为默认事务管理器
                //这就是基于注解配置的最常见的逻辑,该方法如果找不到事务管理器或者找到多个事务管理器,那么将抛出异常
                defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
                //找到了之后存入缓存,key就是默认key
                this.transactionManagerCache.putIfAbsent(
                        DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
            }
        }
        //返回事务管理器
        return defaultTransactionManager;
    }
}

/**
 * 根据限定符查找指定的事务管理器
 * 这里的限定符实际上就是beanName
 */
private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
    //首先从缓存中通过qualifier作为key尝试获取事务管理器
    TransactionManager txManager = this.transactionManagerCache.get(qualifier);
    //如果为null
    if (txManager == null) {
        //那么尝试从容器中查找TransactionManager类型且名为qualifier的bean,并且作为该限定符的事务管理器
        //这就是指定通过@Transactional注解指定事务管理器配置,该方法如果找不到事务管理器,那么将抛出异常
        txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
                beanFactory, TransactionManager.class, qualifier);
        //找到了之后存入缓存,key就是默认qualifier
        this.transactionManagerCache.putIfAbsent(qualifier, txManager);
    }
    return txManager;
}

@Nullable
public TransactionManager getTransactionManager() {
    return this.transactionManager;
}

6 小结

  本次我们介绍了Spring AOP中最重要的类之一——TransactionInterceptor事务拦截器的工作总体流程,以及invoke方法的前半部分源码。
  它的后续的几个方法createTransactionIfNecessary、proceedWithInvocation、completeTransactionAfterThrowing、cleanupTransactionInfo、
commitTransactionAfterReturning都比较的重要,源码也很多,我们放在后面的文章中讲解。

相关文章:
  https://spring.io/
  Spring Framework 5.x 学习
  Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘Java

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

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

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

打赏作者

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

抵扣说明:

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

余额充值