五、Spring AOP

目录

 

1. AOP入口类的添加

2. 匹配Advisor生成代理

2.1    基本概念

2.1.1 Pointcut

2.1.2 Advice

2.1.3 Advisor

2.2    生成代理

2.2.1 找到bean匹配的Advisor

2.2.2如果bean有匹配到Advisor,则为其生成代理对象

3. 被代理方法的执行

3.1 获取被代理方法匹配的增强统一包装成MethodInterceptor

3.2方法调用

3.2.1 Before 增强的调用

3.2.2 After增强的调用

3.2.3 Around增强的调用

3.2.4 AfterReturning增强的调用

3.2.5 AfterThrowing增强的调用

4. 事务切面

4.1开启事务支持

4.1.1 添加AOP入口bean

4.1.2 添加事务advisor

4.2 事务pointCut

4.3事务增强advice

4.3.1 底层JDBC事务

4.3.2 常用事务传播属性

4.3.3 事务代码分析

5. 异步切面

5.1开启异步支持

5.2 异步advisor

5.2.1 异步pointCut

5.2.2 异步advice

5.2.3 bean添加异步advisor


(本篇中用到的演示项目地址:https://gitee.com/yejuan/spring-learning-no-xml 对应tag: spring-c5)

1. AOP入口类的添加

上一篇我们分析到AbstractAutoProxyCreator#postProcessAfterInitialization这个方法中如果bean有匹配到advisor将会生成bean的代理实例。AbstractAutoProxyCreator是一个抽象类,要相关方法得以执行就必须要将其子类并且是实现了BeanPostProcessor接口的子类注册到BeanFactoryList<BeanPostProcessor> beanPostProcessors容器中。

@EnableAspectJAutoProxy通过@Import将AspectJAutoProxyRegistrar加入到spring 容器中,AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口,在registerBeanDefinitions方法中就会将AOP的入口类AnnotationAwareAspectJAutoProxyCreator加入spring容器。

2. 匹配Advisor生成代理

切面使用示例

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.yej.learning.service.AccountService.*(..))")
    public void myPoint(){}

    @Pointcut("execution(* com.yej.learning.service.AccountService.exceptionMethod(..))")
    public void exceptionPoint(){}

    @Before("myPoint()")
    public void befor(){
        System.out.println("---------MyAspect befor -------------");
    }

    @After("myPoint()")
    public void after(){
        System.out.println("---------MyAspect after -------------");
    }

    @Around("myPoint()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        try {
            System.out.println("---------MyAspect around before -------------");
            obj = joinPoint.proceed();
            System.out.println("---------MyAspect around after -------------");
        } catch (Throwable e) {
           e.printStackTrace();
        }
        return obj;
    }

    @AfterReturning(returning = "retObj", pointcut = "myPoint()")
    public void afterReturning(Object retObj) throws Throwable {
       System.out.println("return =" + retObj + "---------MyAspect   afterReturning---------------");
    }

    @AfterThrowing(throwing = "e", pointcut = "myPoint()")
    public void afterThrowing(Exception e) throws Throwable {
       System.out.println("Exception =" + e.getMessage() + "---------MyAspect   afterThrowing---------------");
    }

}

 

2.1    基本概念

2.1.1 Pointcut

切点,匹配哪些方法需要被增强

2.1.2 Advice

增强,上面@Before、@After、@Around、@AfterReturning、@AfterThrowing等注解修饰的方法体,即方法调用时正常方法外需要额外执行的内容。

2.1.3 Advisor

一组PointCut和Advice的封装

2.2    生成代理

2.2.1 找到bean匹配的Advisor

获取与bean匹配的Advisor,找到spring容器中所有的Advisor:1.获取容器中所有Advisor类型的bean;2.过滤有@Aspect注解修饰的bean,遍历bean中所有方法,包装 Around, Before, After, AfterReturning,AfterThrowing注解修饰的方法和注解中配置的PointCut得到Advisor。循环Advisor,获取与当前bean类匹配的Advisor,pointCut与bean进行匹配时会先进行类匹配再进行方法匹配,类过滤器未匹配上时直接返回false,类过滤器匹配上再进行方法匹配,有一个方法匹配上该advisor即与bean匹配上,则该Advisor与目标类匹配。

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary

被不同注解Around, Before, After, AfterReturning, AfterThrowing修饰的切面方法生成的advice增强是不同类型的。对应的增强实现的接口也是不一样的,在代理对象方法调用时会针对advice实现的不同接口进行再次包装,以实现对advice调用的统一、标准化。

org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisor

 

org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvice

2.2.2如果bean有匹配到Advisor,则为其生成代理对象

2.2.2.1获取通过interceptorNames添加的通用advisor

获取通用advisor,遍历interceptorNames容器,通过getBean获取实例,包装生成对应的Advisor。可以自定义实现MethodInterceptor、AfterReturningAdvice、MethodBeforeAdvice、ThrowsAdvice等接口的bean添加到interceptorNames容器,包装生成通用DefaultPointcutAdvisor,DefaultPointcutAdvisor会拦截代理实例的每个实例方法。

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy

添加通用Advisor示例

@Component
public class CommonMethodInterceptor implements MethodInterceptor, BeanFactoryAware {

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        AbstractAdvisorAutoProxyCreator proxyCreator = beanFactory.getBean(AbstractAdvisorAutoProxyCreator.class);
        /**
         * 添加到interceptorNames容器中
         */
        proxyCreator.setInterceptorNames("commonMethodInterceptor");
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("---------CommonMethodInterceptor invoke--------------------");
        return invocation.proceed();
    }
}

2.2.2.2 创建代理实例

判断beanClass是否有实现接口,设置相关值,为使用CGLIB进行代理还是JDK动态代理做好准备

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy

获取到作用于bean的所有advisor,包括通过interceptorNames添加的通用advisor,通过CGLIB或JDK动态代理生成代理实例。

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy

默认如果有实现接口采用JDK动态代理,没有实现接口采用CGLIB进行代理(注意:对于JDK动态代理的bean,通过类型调用getBean(IUserService.class)获取实例时必须通过接口类型获取,spring容器中缓存的是实现接口的代理实例)

org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy

CGLIB生成代理实例:

默认的一般的方法被将匹配到AOP_PROXY索引,对应的CallBack为DynamicAdvisedInterceptor,被代理方法调用时将调用到DynamicAdvisedInterceptor.intercept方法org.springframework.aop.framework.CglibAopProxy#getProxy(java.lang.ClassLoader)

JDK方式生成代理实例:

生成代理实例,被代理方法调用时会调到JdkDynamicAopProxy.invoke方法

3. 被代理方法的执行

JDK动态代理调用到的代理拦截方法JdkDynamicAopProxy.invoke和CGLIB代理DynamicAdvisedInterceptor.intercept方法实现差不多,都完成了对被代理方法的所有增强Advice调用以及被代理方法的调用,我们以CGLIB代理DynamicAdvisedInterceptor.intercept方法进行分析。

3.1 获取被代理方法匹配的增强统一包装成MethodInterceptor

获取被代理实例,获取被代理方法匹配的增强

org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept

 

遍历被代理bean匹配的所有advisor,筛选被调用方法匹配的advisor,获取advisor中对应的增强advice,Befor对应的advice被包装成MethodBeforeAdviceInterceptor类型的MethodInterceptor,AfterReturning对应的增强包装成AfterReturningAdviceInterceptor类型的MethodInterceptor

org.springframework.aop.framework.DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice

org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry#getInterceptors

3.2方法调用

没有匹配到advice对应的MethodInterceptor直接调用被代理方法

被代理方法对应的MethodInterceptor列表不为空时,依次取出被代理方法对应的增强advice,将增强转换为MethodInterceptor类型通过invoke方法进行统一调用,当所有增强MethodInterceptor都调用后执行被代理方法

org.springframework.aop.framework.ReflectiveMethodInvocation#proceed

3.2.1 Before 增强的调用

        前面讲到Before对应的增强最终会包装成MethodBeforeAdviceInterceptor通过接口方法invoke进行调用。在invoke中会先进行Before修饰的方法调用再将方法调用往后传递回代理方法的调用点。
先调用Before增强方法
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor#invoke

通过getBean获取增强方法对应实例再反射调用增强方法,this.aspectInstanceFactory.getAspectInstance()获取到增强方法对应的实例,this.aspectJAdviceMethod是增强方法method,有了方法、方法对应的实例以及参数,就可以通过通过反射进行方法调用org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs

before增强方法调用后方法调用传递回代理方法调用点ReflectiveMethodInvocation#proceed()

3.2.2 After增强的调用

         After增强调用会调用到AspectJAfterAdvice的invoke方法,先将方法调用传递回代理方法调用点ReflectiveMethodInvocation#proceed()然后在finally中进行After增强方法的调用。After增强方法的调用与上面的before方法调用是相同的通过getBean获取增强方法对应的实例再反射进行方法调用。

org.springframework.aop.aspectj.AspectJAfterAdvice#invoke

3.2.3 Around增强的调用

Around增强调用会调用到AspectJAroundAdvice的invoke方法,在invoke方法中只直接调用的Around增强方法,所以在Around方法的前置增强和后置增强之间我们需要自己添加joinPoint.proceed()触发被代理方法的调用。org.springframework.aop.aspectj.AspectJAroundAdvice#invoke

Around增强使用实例

3.2.4 AfterReturning增强的调用

AfterReturning增强的调用会调用到AfterReturningAdviceInterceptor的invoke方法,先通过mi.proceed将方法调用传递回代理方法调用点ReflectiveMethodInvocation#proceed()获取到被代理方法的返回值,再执行AfterReturning增强方法。org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor#invoke

将被代理方法返回值作为参数传入再通过反射执行AfterReturning增强方法

AfterReturning增强使用示例

@AfterReturning(returning = "retObj", pointcut = "myPoint()")
public void afterReturning(Object retObj) throws Throwable {
   System.out.println("return =" + retObj + "---------MyAspect   afterReturning---------------");
}

在AfterReturning增强方法中我们可以拿到被增强方法的返回值,我们看看是如何实现的。

在生成AspectJAfterReturningAdvice时将AfterReturning注解中配置的returning值设置到advice中org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvice

解析增强方法的参数,将参数名与参数位置索引映射加入advice中的argumentBindings容器

反射调用增强方法前通过argBinding方法进行参数绑定,包括绑定被代理方法返回值和抛出的异常

org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethod(org.aspectj.weaver.tools.JoinPointMatch, java.lang.Object, java.lang.Throwable)

获取返回值在增强方法中参数列表中的位置,并进行赋值

org.springframework.aop.aspectj.AbstractAspectJAdvice#argBinding

3.2.5 AfterThrowing增强的调用

AfterThrowing增强的调用会调用到AspectJAfterThrowingAdvice的invoke方法,先通过mi.proceed将方法调用传递回代理方法调用点ReflectiveMethodInvocation#proceed(),完成被代理方法的调用,抛出异常后调用AfterThrowing增强方法,将异常异常作为参数传入

AfterThrowing增强使用示例

@AfterThrowing(throwing = "e", pointcut = "myExceptionPoint()")

public void afterThrowing(Exception e) throws Throwable {

   System.out.println("Exception =" + e.getMessage() + "---------MyAspect   afterThrowing---------------");

}

AfterThrowing增强方法中拿到异常与AfterReturning增强方法中在参数中设置返回值类似,Advice创建时将AfterThrowing注解中配置的throwing值设置到advice中,反射调用AfterThrowing增强方法前获取异常在增强方法参数列表中的位置并进行赋值 

4. 事务切面

4.1开启事务支持

在spring中通过@EnableTransactionManagement就可以开启事务支持功能,EnableTransactionManagement通过Import导入TransactionManagementConfigurationSelector实现了两个功能:①添加AOP入口bean ②添加事务advisor 

4.1.1 添加AOP入口bean

AutoProxyRegistrar添加AOP入口bean,上面AOP的内容我们分析到需要支持AOP功能就需要将AbstractAdvisorAutoProxyCreator的子类加入spring容器。 

如果容器中包含AOP入口BeanDefinition比较优先级,如果优先级低则进行替换,AnnotationAwareAspectJAutoProxyCreator的优先级最高,如果容器中没有包含AOP入口BeanDefinition则生成对应的beanDefinition并进行注册org.springframework.aop.config.AopConfigUtils#registerOrEscalateApcAsRequired 

4.1.2 添加事务advisor

通过@Bean方式添加事务advisor BeanFactoryTransactionAttributeSourceAdvisor,设置advice和pointCut org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration#transactionAdvisor 

4.2 事务pointCut

前面AOP提到pointCut的功能(1)bean初始化完成后,匹配bean是否需要生成代理;(2)代理bean方法调用时,匹配被代理方法是否需要进行增强。我们看看事务pointCut是如何实现这些功能。

上面讲到pointCut与bean进行匹配时会先进行类匹配再进行方法匹配,类过滤器未匹配上时直接返回false,类过滤器匹配上再进行方法匹配,有一个方法匹配上该advisor即与bean匹配上。事务pointCut TransactionAttributeSourcePointcutClassFilterTransactionAttributeSourceClassFilter,一般情况TransactionAttributeSourceClassFilterbeanClass匹配都会匹配返回true

org.springframework.aop.support.AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)

方法匹配,获取事务增强是否与bean方法匹配

org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut#matches

如果bean方法上或者bean类上有Transactional注解,这bean方法与事务advisor匹配,bean也就与事务advisor匹配

4.3事务增强advice

4.3.1 底层JDBC事务

事物是针对同一个连接的一组dml操作,这组操作要么都成功,要么都失败。具体JDBC实现的是有有三个点:①关闭连接的自动提交connection.setAutoCommit(false)(不关闭时是默认自动提交,即执行完每条ddl会自动提交);②一组dml操作执行完手动调用连接提交方法connection.commit()结束事务;③发生异常时手动调用连接回滚方法connection.rollback()结束事务 

@Component
public class TranditionalTransaction {

    @Value("${jdbc.url.jdbcUrl}")
    private String jdbcUrl;
    @Value("${jdbc.username}")
    private String user;
    @Value("${jdbc.password}")
    private String password;

    /**
     * 传统事务
     */
    public void tranMethodTranditional() {
        final String JDBC_DRIVER = "com.mysql.jdbc.Driver";

        //注册JDBC驱动
        try {
            Class.forName(JDBC_DRIVER);
        } catch (ClassNotFoundException e) {
            //这里会发生类没有找到的异常!
            e.printStackTrace();
        }
        //获得数据库连接
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DriverManager.getConnection(jdbcUrl, user, password);
            connection.setAutoCommit(false);

            //执行查询语句
            statement = connection.createStatement();
            String sql = "INSERT INTO area(name, CODE)\n" +
                    "VALUES(\"BeiJin\", \""+new Date().getTime() +"\")";
            statement.executeUpdate(sql);

            sql = "INSERT INTO area(name, CODE)\n" +
                    "VALUES(\"BeiJing\", \""+new Date().getTime() +"\")";
            statement.executeUpdate(sql);

            connection.commit();


        } catch (SQLException e) {
            e.printStackTrace();
            try {
                connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }finally {
            if (null != statement){
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (null != connection){
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.3.2 常用事务传播属性

事务传播属性是用来区分程序中事务方法嵌套调用时被嵌套方法使用事务的不同形式,常用的事务传播属性REQUIRED、REQUIRES_NEW、NESTED。

以A方法调用B方法为例:

①如果A方法没有开启事务,B方法不管是REQUIRED、REQUIRES_NEW、NESTED效果都是一样的,在B方法开始前会拿到一个连接开启事务,在B方法结束时会进行事务的提交或者事务的回滚;

②如果A方法开启了事务REQUIRED、REQUIRES_NEW、NESTED的表现是不同的:

REQUIRED:B方法与A方法使用同一个连接,B方法加入A方法连接的事务中,未指定时默认为REQUIRED;

REQUIRES_NEW:B方法开始前会拿到一个连接开启事务,在B方法结束时会进行事务的提交或者事务的回滚;

NESTED:B方法与A方法使用同一个连接,B方法开始前会创建一个回滚点,如果B方法正常执行结束将该回滚点移除,如果B方法执行异常则回滚到该回滚点。

 

4.3.3 事务代码分析

事务方法调用的时候会进入TransactionInterceptor的invoke方法。

以事务的方式进行被代理方法调用

4.3.3.1 单个事务方法的执行

首先我们来分析最简单的情况不存在事务方法嵌套调用的,单个事务方法的执行

调用AnnotationTransactionAttributeSource的getTransactionAttribute方法,获取被代理方法或者被代理方法所属类上是否有Transactional注解,封装得到TransactionAttribute对象

获取连接开启事务

首次进入创建事务状态,是否新事务标识为true,获取新连接,关闭连接自动提交,根据dataSource将连接将连接缓存到threadLocal类型的map中

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

执行被代理方法

如果被代理方法执行时抛出异常,则调用连接的rollback进行事务回滚

新连接调用连接的rollback方法进行回滚

org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback

回滚后,清除threadLocal类型map中缓存的dataSource与连接的映射关系,释放连接

org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback

如果为新连接则调用close关闭连接,如果使用的是连接池数据源将连接还回连接池可再次进行复用

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion

被代理方法正常执行完,如果是新事务调用连接的commit方法进行事务提交

调用连接的commit方法提交事务

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCommit

提交之后和同上面rollback一样事务执行完了会进行threadLocal类型map中DataSource缓存连接清除和连接释放

总结下单个事务方法的伪代码执行框架如下:

Connection con = obtainDataSource().getConnection();
con.setAutoCommit(false);
try{
   业务代码...
}catch(Throwable ex){
   con.rollback();
   throw ex;
}
con.commit();

4.3.3.2 事务方法嵌套调用

被嵌套的事务方法再次执行时从threadLocal类型的map中拿到dataSource对应的缓存连接

org.springframework.jdbc.datasource.DataSourceTransactionManager#doGetTransaction

被嵌套方法调用时事务对象中已存在连接

被嵌套的事务方法传播属性为REQUIRED

被嵌套的事务方法调用前,获取到的事务状态newTransaction为false

被嵌套的事务方法执行发生异常,将异常抛出throw ex外层事务捕获到异常,同上面4.3.3.1中单个事务方法的执行发生异常将进行事务回滚清除threadLocal类型map中缓存的dataSource与连接的映射关系,释放连接。

 

被嵌套的事务方法传播属性为REQUIRES_NEW

被嵌套的事务方法调用前,先挂起外层事务(获取外层事务连接,移除之前外层事务设置到threadLocal类型map中dataSource与连接的映射缓存)

org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction

创建事务状态newSynchronization标识设置为true,获取新连接,取消自动提交,这时候被嵌套的事务方法就同上面4.3.3.1当个事务方法的一样方法执行发生异常时调用连接的rollback进行回滚,正常执行完成后调用连接的commit进行事务提交,另外在回滚和提交的finally中的cleanupAfterCompletion方法中多了一步恢复之前挂起的外层事务的操作。恢复之前挂起的外层事务,将外层事务的连接与dataSource的映射关系重新设置到ThreadLocal类型的map中。 

org.springframework.transaction.support.AbstractPlatformTransactionManager#cleanupAfterCompletion 

org.springframework.jdbc.datasource.DataSourceTransactionManager#doResume 

被嵌套事务方法传播属性为NESTED

被嵌套事务方法调用前创建事务状态对象DefaultTransactionStatus设置newTransaction标识为false, 创建回滚点org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction 

被嵌套方法执行发生异常时,回滚到回滚点,然后往外抛异常 

如果被嵌套方法正常执行完,则移除回滚点org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit 

到这里我们就完成了事务主要源码的分析

5. 异步切面

5.1开启异步支持

通过@EnableAsync就可以开启异步切面支持功能,EnableAsync通过@Import最终把AsyncAnnotationBeanPostProcessor加入了spring容器,异步切面的功能主要是通过AsyncAnnotationBeanPostProcessor支持的 

5.2 异步advisor

AsyncAnnotationBeanPostProcessor实现了BeanFactoryAware接口在setBeanFactory方法中创建了异步advisor 

5.2.1 异步pointCut

         匹配有Async注解的类和方法,如果类上有Async注解则类的所有实例方法都会被异步切面拦截进行增强,如果只是某个实例方法上有Async注解则该方法会被异步切面拦截进行增强。

5.2.2 异步advice

异步的增强为AnnotationAsyncExecutionInterceptor,当被代理的异步方法执行时会调到AnnotationAsyncExecutionInterceptor的invoke方法进行增强。
org.springframework.scheduling.annotation.AsyncAnnotationAdvisor#buildAdvice 

被代理方法会被提交到包装有线程池的任务执行器中异步执行

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit

5.2.3 bean添加异步advisor

EnableAsync注解没有加入aop的入口类也没有将异步advisor作为bean加入spring容器,那么异步advisor是怎么作用到需要增强的bean上的呢?我们分析下。首先抛出一个结论AnnotationAwareAspectJAutoProxyCreator的优先级高于AsyncAnnotationBeanPostProcessor的优先级。
AnnotationAwareAspectJAutoProxyCreator的优先级Ordered.HIGHEST_PRECEDENCE 

AsyncAnnotationBeanPostProcessor的优先级为Ordered.LOWEST_PRECEDENCE

即bean初始化完成后循环调用BeanPostProcessor#postProcessAfterInitialization如果同时存在AnnotationAwareAspectJAutoProxyCreator和AsyncAnnotationBeanPostProcessor会先执行AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization。如果一个bean有被Aspect切面拦截到会先生成代理再执行AsyncAnnotationBeanPostProcessor的postProcessAfterInitialization方法。

org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization
如果bean已经生成了AOP代理且bean类或方法上有Async注解,则将异步advisor加入被代理bean的advisor列表中。

 

如果bean没有生成AOP代理,创建AOP代理实例,代理中包含异步advisor 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值