Spring学习笔记——AOP(4)

一、学习AOP

1.1 AOP的概述

AOP---->面向切面编程 是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性方法与方法对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程

在这里插入图片描述

1.2 AOP思想实现方案

代理技术:
动态代理技术,在运行期间,对目标方法进行增强,代理对象同名方法内执行原有逻辑或方法的同时可以执行其他的增强逻辑,或者其他对象的方法
在这里插入图片描述

1.3、模拟AOP的基础代码

其实在之前学习BeanPostProcessor时,在BeanPostProcessorafter方法中使用动态代理对Bean进行了增强,实际存储到单例池singleObjects中的不是当前目标对象本身,而是当前目标对象的代理对象Proxy,这样在调用目标对象方法时,实际调用的是代理对象Proxy的同名方法,起到了目标方法前后都进行增强的功能,对该方式进行一下优化,将增强的方法提取出去到一个增强类中,且只对com.itheima.service.impl包下的任何类的任何方法进行增强

//自定义增强类
pubiic class MyAdvice{
	public void beforeAdvice(){
		system.out.println("beforeAdvice...");
	)
	public void afterAdvice() {
		System.out.println("afterAdvice...");
	}
} 
public class MockAopBeanPostProcessor implements BeanPostProcessor , ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //目的:对UserServiceImpl中的Show1和Show2方法进行增强,增强方法存在于Myadvice
        //问题:筛选 service.impl包下的所有类的所有方法都可以进行增强 解决方案:if-else
        //问题:Myadvice怎么获取 解决方案:从Spring容器中获取Myadivce
        if (bean.getClass().getPackage().getName().equals("com.Smulll.service.Impl")){
            //生成Bean的Proxy对象
            Object beanProxy = Proxy.newProxyInstance(
                    bean.getClass().getClassLoader(),
                    bean.getClass().getInterfaces(),
                    (Object proxy, Method method, Object[] args) -> {
                        Myadvice myadvice = applicationContext.getBean(Myadvice.class);
                        //执行增强对象的before方法
                        myadvice.before();
                        //执行目标对象的指定方法
                        Object invoke = method.invoke(bean, args);
                        //执行增强对象的after方法
                        myadvice.after();
                        return invoke;
                    });
            return beanProxy;
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
} 

1.4、AOP的相关概念

概念单词解释
目标对象Target被增强的方法所在的对象
代理对象Proxy对目标对象进行增强后的对象,客户端实际调用的对象
连接点Joinpoint目标对象中可以被增强的方法
切入点Pointcut目标对象中实际被增强的方法
通知\增强Advice增强部分的代码逻辑
切面Aspect增强和切入点的组合
织入Weaving将通知和切入点组合动态组合的过程

在这里插入图片描述

二、基于xml配置AOP

2.1 AOP基础入门

基本步骤:

  1. 导入AOP相关坐标;
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency> 
  1. 准备目标类、准备通知类,并配置给Spring管理;
  2. 配置切点表达式(哪些方法被增强);
  3. 配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)。
<?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
       http://www.springframework.org/schema/aop/spring-aop.xsd
">
    <!--配置目标类-->
    <bean id="userService" class="com.huanglei.service.Impl.UserServiceImpl"></bean>
    <!--配置增强类-->
    <bean id="myadvice" class="com.huanglei.advice.Myadvice"></bean>
    <!--配置aop-->
    <aop:config>
        <!--配置切点表达式 目的:指定哪些方法被增强-->
        <aop:pointcut id="mtPointcut" expression="execution(void com.Smulll.service.Impl.UserServiceImpl.show1())"/>
        <!--配置织入 目的:指定哪些切点与哪些通知结合-->
        <aop:aspect ref="myadvice">
            <aop:before method="before" pointcut-ref="mtPointcut"></aop:before>
            <aop:after method="after" pointcut-ref="mtPointcut"></aop:after>
        </aop:aspect>
    </aop:config>
</beans> 

2.2、XML方式AOP配置详解

xml配置AOP的方式还是比较简单的,下面看一下AOP详细配置的细节:

  • 切点表达式的配置方式
    可以配置多个切点
<!--配置aop-->
<aop:config>
    <!--配置切点表达式 目的:指定哪些方法被增强-->
    <aop:pointcut id="mtPointcut" expression="execution(void com.huanglei.service.Impl.UserServiceImpl.show1())"/>
    <aop:pointcut id="mtPointcut2" expression="execution(void com.huanglei.service.Impl.UserServiceImpl.show2())"/>
</aop:config> 

pointcut属性可以再后面直接写上要结合的切点

<!--配置aop-->
<aop:config>
    <!--配置切点表达式 目的:指定哪些方法被增强-->
    <aop:pointcut id="mtPointcut" expression="execution(void com.huanglei.service.Impl.UserServiceImpl.show1())"/>
    <!--配置织入 目的:指定哪些切点与哪些通知结合-->
    <aop:aspect ref="myadvice">
        <aop:before method="before" pointcut-ref="mtPointcut"></aop:before>
        <aop:after method="after" pointcut="execution(void com.huanglei.service.Impl.UserServiceImpl.show1())"></aop:after>
    </aop:aspect>
</aop:config> 

切点表达式的配置语法

execution([访问修饰符]返回值类型 包名.类名.方法名(参数))
  • 详解:
    • 访问修饰符可以省略不写;
    • 返回值类型、某一级包名、类名、方法名可以使用*表示任意;
    • 包名与类名之间使用单点.表示该包下的类,使用双点…表示该包及其子包下的类;
    • 参数列表可以使用两个点…表示任意参数。

常见的切点表达式:

//表示访问修饰符为public、无返回值、在com.itheima . aop包下的TargetImpl类的无参方法show
execution(public void com.itheima.aop.TargetImpl.show())
//表述com.itheima.aop包下的TargetImpl类的任意方法
execution(* com.itheima.aop.TargetImpl.*(..))
//表示com.itheima.aop包下的任意类的任意方法
execution(* com.itheima.aop.*.*(..))
//表示com.itheima. aop包及其子包下的任意类的任意方法
execution(* com.itheima.aop..*.*(..))
//表示任意包中的任意类的任意方法
execution(* *..*.*(..) )
  • 通知的类型
通知名称配置方式执行时机
前置通知<aop:before>目标方法执行之前执行
后置通知<aop:after-returning>目标方法执行之后执行,目标方法异常时,不在执行
环绕通知<aop:around>目标方法执行前后执行,目标方法异常时,环绕后方法不在执行
异常通知<aop:after-throwing>目标方法抛出异常时执行
最终通知<aop:after>不管目标方法是否有异常,最终都会执行
  • 通知方法再被调用时,Spring可以为其传递一些必要的参数
参数类型作用
JoinPoint连接点对象,任何通知都可使用,可以获得当前目标对象、目标方法参数等信息
ProceedingJoinPointJoinPoint子类对象,主要是在环绕通知中执行proceed(),进而执行目标方法
Throwable异常对象,使用在异常通知中,需要在配置文件中指出异常对象名称
public void 通知方法名称(JoinPoint joinPoint){
	//获得目标方法的参数
	system.out.println(joinPoint.getArgs()) ;
	//获得目标对象
	system.out.println(joinPoint.getTarget());
	//获得精确的切点表达式信息
	System.out.println(joinPoint.getstaticPart());
} 

Throwable对象

public void afterThrowing (JoinPoint joinPoint, Throwable th) {
//获得异常信息
	System.out.println("异常对象是:"+th+"异常信息是:"+th.getMessage());
} 
<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut" throwing="th"/> 
  • AOP的配置的两种方式
    • 使用<advisor>配置切面
    • 使用<aspect>配置切面

spring定义了一个Advice接口,实现了该接口的类都可以作为通知类出现

public interface Advice{
}

2.3、XML方式AOP原理剖析

动态代理的实现的选择,在调用getProxy()方法时,我们可选用的AopProxy接口有两个实现类,如上图,这两种都是动态生成代理对象的方式,一种就是基于JDK的,一种是基于Cglib的

代理技术使用条件配置方式
JDK动态代理技术目标类有接口,是基于接口动态生成实现类的代理对象目标类有接口的情况下,默认方式
Cglib 动态代理技术目标类无接口且不能使用final修饰,是基于被代理对象动态生成子对象为代理对象目标类无接口时,默认使用该方式;目标类有接口时,手动配置<aop:config proxy-target-class="true”>强制使用Cglib方式

在这里插入图片描述

Target target = new Target() ;//目标对象
Advices advices = new Advices();//通知对象
Enhancer enhancer = new Enhancer();//增强器对象
enhancer.setSuperclass(Target.class);//增强器设置父类
//增强器设置回调
enhancer.setCallback((MethodInterceptor) (o,method,objects,methodProxy)->{
	advices.before ( ) ;
	object result = method.invoke(target,objects);
	advices.afterReturning();
	return result;
});
//创建代理对象
Target tagetProxy = (Target) enhancer.create();
//测试
String result = targetProxy.show("haohao");

三、注解式开发AOP

3.1 注解式开发AOP入门

Spring的AOP也提供了注解方式配置,使用相应的注解替代之前的xml配置,xml配置AOP时,我们主要配置了三部分:目标类被Spring容器管理、通知类被Spring管理、通知与切点的织入(切面),如下:

<!--配置目标-->
<bean id="target" class="com.itheima.aop.TargetImpl"></bean>
<!--配置通知-->
<bean id="advices" class="com.itheima.aop.Advices"></bean>
<!--配置aop-->
<aop:config proxy-target-class="true">
	<aop:aspect ref="advices">
		<aop:around method="around" pointeut="execution(* com.itheima.aop.*.*(..))"/>
	</aop:aspect>
</aop:config> 

配置aop,其实配置aop主要就是配置通知类中的哪个方法(通知类型)对应的切点表达式是什么:

在这里插入图片描述
注解@Aspect、@Around需要被Spring解析,所以在Spring核心配置文件中需要在配置文件当中添加的自动代理

<aop:aspectj-autoproxy> 

3.2 AOP注解详细介绍

通过不同的注解去完成各项通知类型:

//前置通知
@Before("execution( *com. itheima.aop.*.*(..))")
public void before(JoinPoint joinPoint){}
//后置通知
@AfterReturning("execution(* com.itheima.aop.*.*(..))")
public void afterReturning(JoinPoint joinPoint){}
//环绕通知
@Around("execution (* com.itheima.aop.*.*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable{}
//异常通知
@AfterThrowing("execution(* com.itheima.aop.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){}
//最终通知
@After("execution(* com.itheima.aop.*.*(..))")
public void after(JoinPoint joinPoint){} 

3.3、注解方式AOP原理解析

之前在使用xml配置AOP时,是借助的Spring的外部命名空间的加载方式完成的,使用注解配置后,就抛弃了<aop.config>标签,而该标签最终加载了名为AspectJAwareAdvisorAutoProxyCreator的BeanPostProcessor ,最终,在该BeanPostProcessor中完成了代理对象的生成。

同样,从aspectj-autoproxy标签的解析器入手

this.registerBeanDefinitionParser("aspectj-autoproxy",new AspectJAutoProxyBeanDefinitionParser());

四、基于AOP的声明式事务控制

4.1 Spring事务编程概述

事务在开发的时候是必不可少的东西,在使用JDBC开发的时候,我们使用的是connection对事物进行控制,使用Mybatis时,我们使用的时SqlSession对事物进行控制,缺点显而易见,我们在切换数据库访问技术的时候,事务控制的方式就会总是在改变,Spring就在这些技术的基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制声明式事务控制

事务控制的方式解释
编程式事务控制Spring提供了事务控制的类和方法,使用编码的方式对业务代码进行了事务控制,事务控制的代码和业务操作代码耦合在一起,在开发当中很少使用
声明式事务控制Spring提供了事务控制的封装,对外提供了xml配置和注解式开发的配置方式,可以通过配置的方式完成对事物的控制,可以达到事务控制和业务操作代码的解耦合,在开发当中推荐使用

Spring事务编程相关的类:

事务控制相关的类解释
平台事务管理器 PlatformTransactionManager是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案
事务定义 TransactionDefinition封装事务的隔离级别、传播行为、过期时间等属性信息
事务状态 TransactionStatus存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等

虽然我们在开发当中不怎么使用这个编程式事务控制,但是对于这个编程式事务控制的相关类我们需要了解一下,因为我们在进行声明式配置的时候我们还会看见他们

4.2 搭建测试环境

搭建一个转账的环境,dao层一个转出钱的方法,一个转入钱的方法service层一个转账业务方法,内部分别调
用dao层转出钱和转入钱的方法,准备工作如下:

  • 数据库准备一个账户表tb_account;
  • dao层准备一个AccountMapper,包括incrMoney和decrMoney两个方法;
  • service层准备一个transferMoney方法,分别调用incrMoney和decrMoney方法;
  • 在applicationContext文件中进行Bean的管理配置;
  • 测试正常转账与异常转账。

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:cotext="http://www.springframework.org/schema/context"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
">
    <!--组件扫描-->
    <cotext:component-scan base-package="com.huanglei"/>
    <!--加载properties文件-->
    <cotext:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源信息-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!--配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象存储到Spring容器-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.huanglei.mapper"></property>
    </bean>
</beans>

Mapper映射文件

public interface accountMapper {
    /*
    *   加钱
    * */
    @Update("update tb_account set money = money+#{money} where account_name = #{accountName}")
    public void incrMoney(@Param("accountName") String accountName,@Param("money") Double money);
    /*
    *   减钱
    * */
    @Update("update tb_account set money = money-#{money} where account_name = #{accountName}")
    public void decrMoney(@Param("accountName") String accountName,@Param("money") Double money);
}

service代码:

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private accountMapper accountMapper;
    public void transferMoney(String outAccount, String inAccount, Double money){
        accountMapper.decrMoney(outAccount,money);
        accountMapper.incrMoney(inAccount,money);
    }
}

4.3 基于XML声明式事务控制

结合我们学过的AOP技术,我们可以使用AOP对service的方法进行事务增强。

  • 目标类:自定义的AccountServicelmpl,内部的方法是切点

  • 通知类: Spring提供的,通知方法已经定义好,只需要配置即可

  • 通知类是Spring提供的,需要导入Spring事务的相关的坐标;

  • 配置目标类AccountServicelmpl;

  • 使用advisor标签配置切面。

<?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:cotext="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd

">
    <!--组件扫描-->
    <cotext:component-scan base-package="com.huanglei"/>
    <!--加载properties文件-->
    <cotext:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源信息-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!--配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象存储到Spring容器-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.huanglei.mapper"></property>
    </bean>

    <!--配置平台事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--事务增强的aop-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!--事务增强的AOP-->
    <aop:config>
        <!--配置切点表达式-->
        <aop:pointcut id="txPointcut" expression="execution(* com.Smulll.service.Impl.*.*(..))"/>
        <!--配置织入关系 通知advice-ref引入Spring提供好的-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
</beans>
<!--事务增强的aop-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
   <tx:attributes>
       <!--
           配置不同的方法的事务属性
           name:方法名称   *代表通配符
           isolaton:控制事务的隔离级别
           timeout:超时时间 默认-1 单位是秒
                例:设置为3 则若3秒内事务没有提交则系统回自动返回
           read-only:是否只读  查询操作设置为只读,其他的为false
           propagation:事务的传播行为 解决业务方法调用业务方法(事务嵌套问题)
       -->
       <tx:method name="*" isolation="READ_COMMITTED" propagation="" timeout="3" read-only="false"/>
   </tx:attributes>
</tx:advice>

**isolation属性:**指定了事务的隔离级别,事务并发存在三大问题:脏读,不可重复读,幻读/虚度。可以通过设置事务的隔离级别来保证并发问题的实现,常用的是READ_COMMITTED和REPEATABLE_READ

常见的isolation设置:

isolation属性解释
DEFAULT表示的为默认的隔离级别,这个取决于你使用的哪种数据库,如MySQL就是REPEATABLE_READ
READ_UNCOMMITTEDA事务可以读取到B事务尚未提交的事务记录,不能解决任何并发问题,安全性最低,性能最高
READ_COMMITTEDA事务只能读取到其他事务已经提交的记录,不能读取到未提交的记录。可以解决脏读问题,但是不能解决不可重复读和幻读
REPEATABLE_READA事务多次从数据库读取某条记录结果一致,可以解决不可重复读,不可以解决幻读
SERIALIZABLE串行化,可以解决任何并发问题,安全性最高,但是性能最低

**read-only属性:**就是设置当前的状态,为只读还是可以修改。设置为true表示只读,那么这样可以提高查询的性能,如果要增加或者删除修改,那必须得设置为法false

<!--一般查询相关的业务操作都会设置只读模式-->
<tx:method name="select*" read-only="true"/>
<tx:method name="find*" read-only="false"/>

**timeout属性:**设置事务提交的最长时间,如果超过这个时间那么事务就会自动回滚,不再执行。默认值为-1,表示没有时间限制。

<!--设置查询操作的超时时间是3秒-->
<tx:method name="select*" read-only="true" timeout="3" />

propagation属性:设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,例如:使用单方的事务,还是A和B都使用自己的事务等。事务的传播行为有如下七种属性值可配置

事务传播的行为解释
REQUIRED (默认值)A调用B,B需要事务,如果A有事务B就加入A的事务中,如果A没有事务,B就自己创建一个事务
REQUIRED_NEWA调用B,B需要新事务,如果A有事务就挂起,B自己创建一个新的事务
SUPPORTSA调用B,B有无事务无所谓,A有事务就加入到A事务中,A无事务B就以非事务方式执行
NOT_SUPPORTSA调用B,B有无事务无所谓,A有事务就加入到A事务中,A无事务B就以非事务方式执行
NEVERA调用B,B以无事务方式执行,A如有事务则抛出异常
MANDATORYA调用B,B要加入A的事务中,如果A无事务就抛出异常
NESTEDA调用B, B创建一个新事务,A有事务就作为嵌套事务存在,A没事务就以创建的新事务执行

4.4 注解声明式事务控制

@Configuration
@ComponentScan("com.huanglei")
@PropertySource("classpath:jdbc.properties")
@MapperScan("com.huanglei.mapper")
@EnableTransactionManagement//相当于<tx:annotation-driven transaction-manager="transactionManager"/>
public class SpringConfig {

    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driver}") String driver,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password
    ){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);

        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值