理解Spring AOP

节选自http://www.cnblogs.com/xrq730/p/4919025.html

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

 

AOP核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象

3、连接点(joinpoint)

被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

4、切入点(pointcut)

对连接点进行拦截的定义

5、通知(advice)

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

 

主要取自《Spring技术内幕》第一版

 

AOP联盟定义的AOP体系结构把AOP相关的概念大致分为了由高到低、从使用到实现的三个层次。从上往下,做高层是语言和开发环境,在这个环境中可以看到几个重要的概念:基础可以视为待增强对象或者说目标对象;切面通常包含于基础的增强应用;配置可以看成是一种编织或者说配置,通过在AOP体系中提供这个配置环境,可以把基础和切面结合起来,从而完成切面对目标对象的编织实现。

Advice(通知):定义在连接点做什么,为切面增强提供织入接口。在Spring AOP中,他主要描述Spring AOP围绕方法调用而注入的切面行为。Advice通知是AOP联盟定义的一个接口,具体的接口定义在org.aopalliance.aop.Advice中。在Spring AOP的实现中,使用了这个统一接口,并通过这个接口为AOP切面增强的织入功能做了更多的细化和扩展,比如提供了更具体的通知类型,像BeforeAdvice、AfterAdvice、ThrowAdvice等。作为Spring AOP定义的接口类,具体的切面增强可以通过这些接口继承到AOP框架中去发挥作用。

Pointcut(切点):决定Advice通知应该作用于哪个连击点,也就是说通过Pointcut切点来定义需要增强的方法的集合,这些集合的选取可以按照一定的规则来完成。在这种情况下,Pointcut通常意味着标识方法,例如,这些需要增强的地方可以是被某个正则表达式进行标识,或根据某个方法名进行匹配等。

Advisor(通知器):当我们完成对目标方法的切面增强设计(Advice)和关注点的设计(pointcut)以后,需要一个对象把他们结合起来,完成这个作用的就是Advisor。通过Advisor,可以定义应该使用哪个Advice并在哪个Pointcut使用它,也就是说通过Advisor把Advice和Pointcut结合起来了,这个结合为应用使用IoC容器配置AOP应用,或者说即开即用地使用AOP基础设置。在Sping AOP中,我们举一个Advisor的实现(DefaultPointcutAdvisor)作为例子,去了解Advisor的工作原理。在DefaultPointcutAdvisor中有两个属性,分别是Advice(父类AbstractGenericPointcutAdvisor的属性)和Pointcut。

例如:

<?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-3.0.xsd
        http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
           ">
<bean id="myTestBeanA" class="com.zdws.spring.MyTestBeanA" lazy-init="false">
<property name="testStr" value="Blueberry Cheesecake" />
</bean>
<bean id="userManager" class="com.tgb.aop.UserManagerImpl" />
<bean id="xmlHandler" class="com.tgb.aop.XMLAdvice" />
<aop:config>
<aop:aspect id="aspect" ref="xmlHandler">
<aop:pointcut id="pointUserMgr" expression="execution(* com.tgb.aop.*.find*(..))" />
<aop:before method="doBefore" pointcut-ref="pointUserMgr" />
</aop:aspect>
</aop:config>
</beans>

其中,切面是xmlHandler,基础是userManager。

以BeanFactory bf = new ClassPathXmlApplicationContext("beanFactoryTest.xml");为例,分析源代码。

AbstractApplicationContext.refresh()方法会完成bean的查找、解析、注册、初始化。

1、解析说明

对aop的解析分多种情况,例如注解和xml配置两种方式就会导致对aop解析方式的不同。这里,仅以xml格式的解析为例。

DefaultBeanDefinitionDocumentReader类负责具体解析,这里先说明对aop的解析。对aop的解析,利用DefaultBeanDefinitionDocumentReader.parseCustomElement方法调用BeanDefinitionParserDelegate类,BeanDefinitionParserDelegate类调用NamespaceHandlerSupport类,NamespaceHandlerSupport类调用ConfigBeanDefinitionParser完成。实际的解析过程,在ConfigBeanDefinitionParser类内。

 

 

 

 

 

a、代理的生成方式

普通代理的生成方式

INFO : org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator:479 - createProxy beanName = userManager,advisor = class org.springframework.aop.interceptor.ExposeInvocationInterceptor$1,advice = org.springframework.aop.interceptor.ExposeInvocationInterceptor@224edc67
INFO : org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator:479 - createProxy beanName = userManager,advisor = class org.springframework.aop.aspectj.AspectJPointcutAdvisor,advice = org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void com.tgb.aop.XMLAdvice.doBefore(org.aspectj.lang.JoinPoint)]; aspect name 'xmlHandler'

事务代理的生成方式
INFO : org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator:479 - createProxy beanName = accountService,advisor = class org.springframework.aop.interceptor.ExposeInvocationInterceptor$1,advice = org.springframework.aop.interceptor.ExposeInvocationInterceptor@3a883ce7
INFO : org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator:479 - createProxy beanName = accountService,advisor = class org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor,advice = org.springframework.transaction.interceptor.TransactionInterceptor@4973813a

两者的主要区别,在于advice的类型不同

b、解析过程的自动添加问题

以xml为例,有事务配置的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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd 
           ">
<!-- 验证事务开始 -->
<!-- 配置业务类 -->
 
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
  ****
</bean>

    <bean id="accountService" class="com.zdws.spring.tx.AccountServiceImpl">
        <!-- 注入DAO -->
        <property name="accountDao" ref="accountDao"/>
    </bean>
    <!-- 配置DAO类 -->
    <bean id="accountDao" class="com.zdws.spring.tx.AccountDaoImpl">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource"/>   
    </bean>
    <aop:config>
        <aop:pointcut id="serviceMethodWang" expression="execution(* com.zdws.spring.tx.*.*(..))"/>
        <aop:advisor pointcut-ref="serviceMethodWang" advice-ref="txAdvice"/>
    </aop:config>
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="find*" read-only="false"/>
            <tx:method name="add*" rollback-for="Exception"/>
            <tx:method name="transfer*" rollback-for="Exception"/>
        </tx:attributes>
    </tx:advice>
<!-- 验证事务结束 -->
</beans>


在解析上面这个配置文件的时候,针对特定标签的解析会引入特定类的添加。

INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = dataSource,class = com.alibaba.druid.pool.DruidDataSource
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = accountService,class = com.zdws.spring.tx.AccountServiceImpl
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = accountDao,class = com.zdws.spring.tx.AccountDaoImpl
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = transactionManager,class = org.springframework.jdbc.datasource.DataSourceTransactionManager
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = org.springframework.aop.config.internalAutoProxyCreator,class = org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = serviceMethodWang,class = org.springframework.aop.aspectj.AspectJExpressionPointcut
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#0,class = org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
INFO : DefaultListableBeanFactory - registerBeanDefinition start beanName = txAdvice,class = org.springframework.transaction.interceptor.TransactionInterceptor

aop:config标签,会引入beanName = org.springframework.aop.config.internalAutoProxyCreator,class = AspectJAwareAdvisorAutoProxyCreator的添加,AspectJAwareAdvisorAutoProxyCreator类实现了SmartInstantiationAwareBeanPostProcessor和BeanPostProcessor两个接口,所以AspectJAwareAdvisorAutoProxyCreator类会被加入到AbstractBeanFactory类的beanPostProcessors集合内,在初始化任何bean时,都会调用AspectJAwareAdvisorAutoProxyCreator对应实现的postProcessBeforeInitialization和postProcessAfterInitialization方法。具体些,一种情况是,AbstractAutowireCapableBeanFactory类的createBean最终会调用自身的initializeBean方法,initializeBean方法内会分别调用applyBeanPostProcessorsBeforeInitialization和applyBeanPostProcessorsAfterInitialization。这两个方法内部,会迭代AbstractBeanFactory类的beanPostProcessors集合内的值,分别调用它们的postProcessBeforeInitialization和postProcessAfterInitialization方法。

AspectJAwareAdvisorAutoProxyCreator继承关系如下,所以对于两个方法的调用,是在AbstractAutoProxyCreator类内。

AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator
AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator
AbstractAutoProxyCreator.postProcessAfterInitialization

AbstractAutoProxyCreator.postProcessAfterInitialization方法内部,会调用wrapIfNecessary方法。方法内部,会获取已有的advisor(即实现了Advisor接口的类,最终通过BeanFactoryAdvisorRetrievalHelper类内的advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false);获取),并判断已有的advisor是否有和beanName匹配的。这里只考虑类,不考虑aop配置的拦截的具体方法。可能aop只拦截某个类的一个方法,但你不确定他使用时究竟调用的是哪个方法,所以这里的匹配,是大匹配,只要有一个满足条件,那么就意味着匹配,需要创建代理返回。代理再执行时,会根据执行的方法,判断是否有拦截链执行。

/**
* Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
* @param bean the raw bean instance
* @param beanName the name of the bean
* @param cacheKey the cache key for metadata access
* @return a proxy wrapping the bean, or the raw bean instance as-is
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
logger.info("AbstractAutoProxyCreator wrapIfNecessary beanName = " + beanName + ",class = " + bean.getClass());
if (beanName != null && this.targetSourcedBeans.containsKey(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
logger.info("AbstractAutoProxyCreator wrapIfNecessary beanName = " + beanName + " before getAdvicesAndAdvisorsForBean");
// Create proxy if we have advice.
//调用的是AbstractAdvisorAutoProxyCreator的方法,返回Advisor类型的数组
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
logger.info("AbstractAutoProxyCreator wrapIfNecessary after createProxy beanName = " + beanName + ",class = " + proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

aop:advisor标签,会引入beanName = org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#0,class = org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor。因为没有id,所以会默认生成一个id,值为DefaultBeanFactoryPointcutAdvisor#0。

DefaultBeanFactoryPointcutAdvisor的advice为TransactionInterceptor。

AspectJAwareAdvisorAutoProxyCreator:479-createProxy beanName = accountDao,advisor = class org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor,advice = org.springframework.transaction.interceptor.TransactionInterceptor@4973813a
 

c、事务拦截的具体实现

上面介绍了aop:advisor标签自动引入DefaultBeanFactoryPointcutAdvisor类,advice = org.springframework.transaction.interceptor.TransactionInterceptor。执行方法时,以JdkDynamicAopProxy为例,invoke方法内会获取当前方法对应的所有interception,Interceptor接口继承自Advice接口,public interface Interceptor extends Advice

// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

所以,涉及到事务时,会执行TransactionInterceptor的invokeWithinTransaction.invoke方法,里面的调用TransactionAspectSupport.invokeWithinTransaction方法,

/**
* General delegate for around-advice-based subclasses, delegating to several other template
* methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
* as well as regular {@link PlatformTransactionManager} implementations.
* @param method the Method being invoked
* @param targetClass the target class that we're invoking the method on
* @param invocation the callback to use for proceeding with the target invocation
* @return the return value of the method, if any
* @throws Throwable propagated from the target invocation
*/
protected Object invokeWithinTransaction(Method method, Class targetClass, final InvocationCallback invocation) throws Throwable {

//最终可能会调用NameMatchTransactionAttributeSource.getTransactionAttribute方法,里面会匹配方法是否有事务处理需求
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);

final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}

else {
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus status) {
TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
try {
return invocation.proceedWithInvocation();
}
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.
return new ThrowableHolder(ex);
}
}
finally {
cleanupTransactionInfo(txInfo);
}
}
});


// Check result: It might indicate a Throwable to rethrow.
if (result instanceof ThrowableHolder) {
throw ((ThrowableHolder) result).getThrowable();
}
else {
return result;
}
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
}
}
 

public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) {
// look for direct name match
String methodName = method.getName();
TransactionAttribute attr = this.nameMap.get(methodName);


if (attr == null) {
// Look for most specific name match.
String bestNameMatch = null;
for (String mappedName : this.nameMap.keySet()) {
if (isMatch(methodName, mappedName) &&
(bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) {
attr = this.nameMap.get(mappedName);
bestNameMatch = mappedName;
}
}
}


return attr;
}

final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);是关键,里面会根据方法名和事务属性,判断是否生成事务。如果需要生成事务,TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);内会生成事务(里面会有事务传播属性的处理),txInfo.hasTransaction()为true或false直接决定是否会有事务处理。

@SuppressWarnings("serial")
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
/**
* 调用AbstractPlatformTransactionManager.getTransaction方法,里面会有doBegin(transaction, definition);方法调用
*/
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
}
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

 

/**
* This implementation handles propagation behavior. Delegates to
* {@code doGetTransaction}, {@code isExistingTransaction}
* and {@code doBegin}.
* @see #doGetTransaction
* @see #isExistingTransaction
* @see #doBegin
*/
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
Object transaction = doGetTransaction();


// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();


if (definition == null) {
// Use defaults if no transaction definition given.
definition = new DefaultTransactionDefinition();
}


if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(definition, transaction, debugEnabled);
}


// Check definition settings for new transaction.
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}


// No existing transaction found -> check propagation behavior to find out how to proceed.
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}//传播属性的处理
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}

 

 

/**
* Handle a throwable, completing the transaction.
* We may commit or roll back, depending on the configuration.
* @param txInfo information about the current transaction
* @param ex throwable encountered
*/
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.hasTransaction()) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by rollback error", ex);
throw err;
}
}
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by commit error", ex);
throw err;
}
}
}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值