Spring 学习总结之AOP基础——基于《精通Spring4.x——企业应用开发实战》

这本书不知不觉已经看了七章了,一直感觉光看书很快就会忘记很多知识点,遂痛下决心,决定每学完一章就总结一下这一章的知识点,一来可以巩固所学,二来方便以后查看,毕竟自己记的东西很容易想起来,另外也供广大网友参考与批评,大家一起学习进步。闲话到此为止,开始第一天的旅程。

1、关于AOP的理解

一直认为Spring的两个核心基础IOC和AOP都不是很直观,之前在《Spring实战》中看到AOP的解释,虽然写得已经比较形象了,但过了一段时间仍然很模糊,《精通Spring4.x——企业应用开发实战》中对AOP有一个很形象的描述,如下图所示:


我们将程序的代码逻辑看作树干的横截面,如果树中心为业务逻辑,则围绕在树中心的年轮就可以看作横切逻辑,这也是横切代码概念的由来。与继承和多态不一样,这里的年轮和树心是在一个平面上,而非上下关系,因此可以把横切作为继承在另一个维度的表达。关于横切编程脑补了一个场景:树的年轮是在基因中就定义好了的,因此树在生长过程中根本不会自己刻意去关注年轮的生长,它仅仅关注于自身的身高与胖瘦,前者理解为切面编程,而后者理解为业务逻辑。

2、关于AOP的术语

(1)连接点(Joinpoint):横切逻辑织入(可理解为木马植入)的特定位置,对于Spring来说,连接点是围绕目标bean的方法来说的(方法前,方法后以及方法前后均有)。值得注意的是连接点是一个集合,而切点则是连接点的特定元素;

(2)切点(Pointcut):上面已经说了

(3)增强(Advice):增强就是织入目标类连接点上的一段程序代码以及该代码的方位信息(前,后以及前后),增强一般位于某个类中,这个类我认为可以看做增强的容器;

(4)目标对象(Target):这个不多说,顾名思义即可;

(5)引介(Introuduction):另一种增强,为目标对象添加新的属性和方法,原理上还是使用动态代理实现的,只是从另一个角度上对目标对象进行了扩展,因此成为另一种增强;

(6)织入(Weaving):将增强添加到目标对象的过程,似乎和植入也没多大区别。

(7)代理(Proxy):增强被植入目标类之后的类。

(8)切面(Aspect):增强其实就是切面的一种,只是增强的横切逻辑会织入到目标类的所有方法中,而切面和增强相比多了切点,也即可以对目标类的方法有选择的织入,而非织入目标类的所有方法。

3、jdk动态代理

jdk动态代理主要由java.lang.reflect包下的Proxy类和InvocationHandler接口的实现类实现,该动态代理只能针对接口,将实现步骤总结如下:

(1) 实现InvocationHandler,实现其invoke方法,该方法返回Object对象,有三个参数:Object proxy,Method method, Object[] args,proxy是最终生成的代理实例,一般不会用到(不知道放在这里做什么),method是目标类的方法,通常可以获取对象方法的一些信息,也可以用来调用目标方法,args是method的参数,该参数和method一起用来调用目标类的方法,一般来说横切逻辑也放在invoke中了,而且一般也会在invoke中调用目标方法(因此目标类的对象会被放置到该InvocationHandler的实现类之中),这样就实现了横切逻辑的织入过程;

(2)调用Proxy的newProxyInstance(方法):

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

loader为类加载器,一般为目标类的加载器:target.getClass().getClassLoader();

interfaces为代理类要实现的接口

h为InvocationHandler的实现类

该方法返回的即是目标类的代理,该代理对象包含横切逻辑

4、CGLib动态代理

CGLib底层采用字节码技术,可以为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑,将实现步骤总结如下:

(1)实现MethodInterceptor接口,在实现类中实现getProxy()方法和intercept()方法

①getProxy()的实现:

public Object getProxy(Class clazz) {
    //this.enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(this);
    return enhancer.create();
}

②intercept()的实现:

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
    //横切逻辑
    Object result = proxy.invokeSuper(obj, args);
    //横切逻辑
    return result;
} 

(2)new一个代理类的对象, 通过getProxy()方法获取代理类,该代理类的方法中即包含了横切逻辑

注意:由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final获private的方法进行代理。

5、创建增强类

(1) 增强类型

① 前置增强:在目标方法之前前执行增强逻辑:org.springframework.aop.BeforeAdvice;

② 后置增强:在目标方法执行之后执行增强逻辑:org.springframework.aop.AfterReturningAdvice

③ 环绕增强:在目标方法执行前后执行增强逻辑:org.aopalliance.intercept.MethodInterceptor;

④ 异常抛出增强:在目标方法抛出异常后执行增强逻辑:org.springframework.aop.ThrowsAdvice;

⑤ 引介增强:在目标类中添加新的属性和方法:org.springframework.aop.IntroductionInterceptor;

这些增强均为接口,通过实现这些接口可以添加横切逻辑。



(2) 增强的创建步骤:

① 创建增强的实现类,在实现类中实现增强接口的方法,添加横切逻辑,每一类增强的需要实现的方法如下:

a) 前置增强:

public void before(Method method, Object[] args, Object obj) throw Throwable
其中method为目标类的方法,args为目标类的入参,obj为目标类的实例,这样看来是完全可以在增强中执行目标对象的方法的。

b) 后置增强:

public void afterReturning(Object returnObj, Method method, Object[] args, Object obj) throw Throwable

其中returnObj为目标实例方法的返回结果,其他的同上

c) 环绕增强:

public Object invoke(MethodInvocation invocation) throws Throwable

MethodInvocation不仅封装了目标方法及其入参数组,还封装了目标方法所在的实例对象(其实就相当于前置增强的三个参数封装在了一起),通过getArguments()获得目标方法的入参数组,通过proceed()方法调用目标实例响应的方法。

d) 异常抛出增强:

public void afterThrowing(Method method, Object[] target, Object target, Exception ex) throws Throwable

前三个参数的意义参考前置增强的方法,都是可选的,最后一个参数是必填的,其必须为Throwable的子类

e) 引介增强:介于引介增强的特殊性,后面单独列出来。

疑惑点:除了环绕增强外其实不需要再增强中调用目标对象的方法,不知道把这些参数传进来做什么,而且为什么不像环绕增强那样将参数信息封装起来呢?

② 将目标对象和增强整合到一起,在Spring中有两种方式:

a) 使用代码:

这种方式的核心是ProxyFactory,ProxyFactory内部使用JDK或者CGLib动态代理技术将增强应用到目标类中,简而言之就是ProxyFactory对JDK和CGLib进行了封装,使之符合工厂模式的调用风格。代码步骤如下:

//①Spring提供的工厂代理类
ProxyFactory pf = new ProxyFactory();
//②设置代理目标
pf.setTarget(target);
//③设置增强
pf.setAdvice(advice);
//④生成代理实例
Target proxy = (Target) pf.getProxy();
//⑤用代理实例调用target的方法,此时已经添加了横切逻辑
proxy.method1();
proxy.method2();
...

如果使用ProxyFactory的setInterfaces(Class[] interfaces)方法进行指定目标接口进行代理,则ProxyFactory使用jdk实现动态代理,如果是针对类的代理,则使用CGLib实现动态代理。此外,也可用通过ProxyFactory的setOptimize(true)显示指定CGLib动态代理。

注意:使用CGLib时,必须引入CGLib类库。

如果是一个目标类对应对个增强类的情况,则在没有实现Order接口的情况下会根据添加顺序来执行横切逻辑。(环绕增强似乎并不存在这种场景)

b) 使用Spring配置,这部分结合实例比较直观:

<bean id="advice" class="com.smart.advice.GreetingBeforeAdvice"/>
<bean id="target" class="com.smart.advice.NaiveWaiter"/>
<bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
    p:proxyInterfaces="com.smart.advice.Waiter"
    p:interceptorNames="advice"
    p:target-ref="target"/>

ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化bean,而ProxyFactoryBean顾名思义就是负责实例化代理。其内部使用ProxyFactory来完成这项工作。其几个可配置属性和前面代码的方式对应即可

target:代理的目标对象;

proxyInterfaces:代理所要实现的接口,别名interfaces

singleton:代理是否为单例。默认为单例

optimize:当设置为true时强制使用CGLib动态代理。对于singleton的代理,推荐使用CGLib,而对于其他的代理,则用JDK代理(原因:前者创建慢,运行快,后者相反)。

proxyTargetClass:是否对类进行代理,当设置为true时,使用CGLib代理

注意:interceptorNames是String[]类型的,其接受的是增强Bean的名称而非实例,这是因为ProxyFactoryBean在生成代理类时使用的是类,而非实例。因此可以说增强是类级别的。

(3) 引介增强

说实话,一开始看到引介增强着实让我大吃了一惊。这尼玛完全是逆天的存在啊,不过仔细想想:既然前面几种增强都可以在目标对象的方法中添加横切逻辑,那给目标对象新增方法又有何不可?虽然暂时还没想通内部是怎么实现的,但也觉得不是没有可能。先说一下引介增强和前面四种增强最主要的区别:引介增强不是给目标对象的方法新增横切逻辑,而是给目标对象新增方法和属性,因此其连接点是类级别的,而非方法级别的。

Spring定义了引介增强接口IntroductionInterceptor,该接口没有定义任何方法,Spring为其提供了DelegatingIntroductionInterceptor实现类。一般情况下通过扩展该实现类定义自己的引介增强类。

使用引介的步骤总结如下:

①定义一个新的接口NewInterface,该接口包含要给目标对象新加的方法newMethod。

②定义一个扩展DelegatingIntroductionInterceptor,实现新增接口NewInterface的类subClassAdvice:

a) 实现NewInterface的新增方法newMethod;

b) 复写DelegatingIntroductionInterceptor的invoke()方法,其定义如下:

public Object invoke(MethodInvocation mi) throws Throwable

在这一步把新增的接口内容和DelegatingIntroductionInterceptor的内容融合到了一个类中,这个类就是一个引介增强类。这个类包含新的方法newMethod()。

③将上面的引介增强添加到目标类中:

<bean id="forumService" class="..."
    p:interfaces="com...newInterface"
    p:target-ref="target"
    p:interceptorNames="subClassAdvice"
    p:proxyTargetClass="true"/>

这样就可以在forumService Bean中调用newMethod()了。

6、创建切面

增强有一个很明显的缺陷,那就是横切逻辑会被添加到目标对象的所有方法中,这时候就需要切面编程了。切面和增强相比,多了切点的概念。

Spring通过org.springframework.aop.Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成,前者定位特定类,后者定位特定方法。UML模型如下:


由上图可以看到ClassFilter仅仅定义了一个matches()方法,用以判定入参是否是指定的类,而MethodMatcher包含两个matches()方法。第一个是静态匹配器,这种匹配器仅仅对方法签名(方法名和入参及其顺序)进行匹配,因此只匹配一次,第二个是动态匹配器,通过观察参数信息就可以知道这种匹配器和第一种比多了参数信息,因此它会根据参数值进行动态匹配,动态匹配对性能影响非常大,一般不采用。方法匹配器的类型由isRuntime()方法返回值决定,true标识是动态匹配。

(1) 切点类型

① 静态方法切点:

org.springframework.aop.support.StaticMethodMatcherPointcut是静态方法切点的抽象基类,默认情况下匹配所有类。其包含两个子类:NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者将方法名按字符串匹配,后者按正则表达式匹配。

② 动态方法切点:

org.springframework.aop.support.DynamicMethodMatcherPointcut是动态方法切点的基类,默认匹配所有类

这两个类应该是和前面的动态匹配和静态匹配相对应的,我猜想静态方法切点只有静态匹配器,isRuntime()方法返回false;而动态方法只有动态匹配器,isRuntime()方法返回true。

③ 注解切点:

org.springframework.aop.support.AnnotationMatchingPointcut标识注解切点。支持在Bean中通过注解标签定义切点,这个目前还不是很明白,第八章有详细介绍。

④ 表达式切点:

org.springframework.aop.support.ExpressionPointcut,为支持AspectJ切点表达式定义的接口,现在还不是很清楚,第八章有详细说明。

⑤ 流程切点:

org.springframework.aop.support.ControlFlowPointcut, 控制流程切点,可以通过该切点指定(构造器)类和方法,凡是通过指定类的方法直接调用的其他方法都会添加横切逻辑。

⑥ 复合切点:

org.springframework.aop.support.ComposablePointcut,该切点可以创建多个切点,并且可以指定他们之间的关系(且或)。

(2)  切面类型:

切面类型主要有三类:

Advisor:仅包含一个Advice,其功能和增强似乎也没什么区别。

PointcutAdvisor:包含Advice和Pointcut,这个就是平时用的最多的切面了。

IntroductionAdvisor:顾名思义,这是引介切面,其用于类层面上,不牵涉到方法,因此它的切点仅仅使用ClassFilter来进行定义。


(3) PointcutAdvisor

由于用的最多,这里细化一下PointcutAdvisor的内容


PointcutAdvisor实现类主要有六类,可以结合前面的切点类型来进行类比,大部分是由对应的切面类的。可以这么说,其包含的切点类型很大程度上决定了切面的定义,最常用的是DefaultPointcutAdvisor,其可以通过指定增强和切点来定义。

(4) 静态普通方法名匹配切面

StaticMethodMatcherPointcutAdvisor代表静态方法匹配切面(我依然认为其内部只有一个方法静态匹配器,isRuntime()返回值为false),内部通过StaticMethodMatcherPointcut来定义切点。并通过类过滤和方法名来匹配所定义的切点。使用步骤如下:

① 定义一个类继承StaticMethodMatcherPointcutAdvisor抽象类,复写matches()方法和getClassFilter()方法。

② 定义一个增强类。

③ 将增强类设置到切面中。

注意:第一步也可以通过Spring配置的方式获取ClassFilter,将其作为切面的属性,但是matches还是得自己编码。

思考:这个地方仅仅设置了一个增强类,并没有显式设置切点,我猜想StaticMethodMatcherPointcutAdvisor可能内置了一个StaticMethodMatcherPointcut类型的成员,我们实现matches()和getClassFilter()实际上就是实现其内置的StaticMethodMatcherPointcut成员的matches()和getClassFilter()方法。这里将advisor的定义和Pointcut的定义搅在了一起,容易让人疑惑,也不知道猜得对不对。

(5) 静态正则表达式方法匹配切面

RegexpMethodPointcutAdvisor是正则表达式方法匹配的切面实现类,该类已经是功能齐备的实现类了,一般情况下无需扩展该类(和StaticMethodMatcherPointcutAdvisor不同)。例子如下:

<bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
p:advice-ref="advice">
<property name="patterns">
   <list>
        <value>.*greet.*</value>
    </list>
</property>
</bean>
<bean id="waiter1" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="regexpAdvisor"
p:target-ref="waiterTarget"
p:proxyTargetClass="true"/>

这里直接注入了增强,另外通过patterns属性指定匹配的正则表达式,需要注意的是这里匹配的是目标类的全限定名,换句话说,该正则表达式不仅可以匹配方法名,也可以匹配类名。

(6)流程切面

流程切面由DefaultPointcutAdvisor和ControlFlowPointcut实现,其使用和前面的静态普通方法名匹配切面不太一样,其使用步骤总结如下(按照Spring配置的方式):

① 配置一个ControlFlowPointcut的Bean,指定流程切点的类(调用其他方法的类)和方法(调用其他方法的类的方法);

② 配置一个DefaultPointcutAdvisor的Bean,指定其pointcut属性为①的Bean以及一个增强的Bean;

③ 配置一个被调用类的代理类的Bean,指定其InterceptorNames属性为②中的Bean;

④ 将③的Bean装配到一个调用类中,调用①中配置的方法,即可发现被调用类对应的方法添加了横切逻辑。

(7) 复合切点切面

复合切点用来组合两个或两个以上的单独切点,将这些切点以交集或并集的方式组合起来。 ComposablePointcut本身就是一个起点,可以通过如下的构造器静定定义:

① ComposablePointcut():构造一个匹配所有类所有方法的复合切点。

② ComposablePointcut(ClassFilter classFilter):构造一个匹配特定类所有方法的复合切点。

③ ComposablePointcut(MethodMatcher methodMatcher):构造一个匹配所有类特定方法的复合切点。

④ ComposablePointcut(ClassFilter classFilter, MethodMatcher methodMatcher):构造一个匹配特定类特定方法的复合切点。

复合切点包含三个切点交集运算的方法

① ComposablePointcut intersection(ClassFilter filter):将复合切点和一个ClassFilter对象进行交集运算,得到一个复合切点。

② ComposablePointcut intersection(MethodMatcher mm):将复合切点和一个MethodMatcher对象进行交集运算,得到一个复合切点。

③ ComposablePointcut intersection(Pointcut other):将复合切点和一个切点对象进行交集运算,得到一个复合切点。

至于并集运算方面,方法名为union,仅提供了ClassFilter和MethodMatcher两种参数形式的重载。如果需要直接对两个Pointcut进行交并集的运算,可以使用org.springframework.aop.support.Pointcuts工具类进行:

Pointcut intersection(Pointcut a, Pointcut b);

Pointcut union(Pointcut a, Pointcut b);

复合切面的编程步骤总结:

①定义一个复合切点(通过类或方法)。

② 在Spring配置中配置切面,只要把复合切点和增强赋给DefaultPointcutAdvisor即可代表。

③ 创建代理

说实话,切面的类型很多时候都是由切点类型决定的,比如复合切面就是一个很好的例子。

(8) 引介切面

引介切面的类继承图如下所示:


IntroductionAdvisor类同时继承了Advisor类和introductionInfo类,后者描述了目标类需要实现的新接口,注意到IntroductionAdvisor只有一个ClassFilter,没有MethodMatcher,可以想想这是为什么。

IntroductionAdvisor的继承类中DefaultIntroductionAdvisor是最常用的。DeclareParentsAdvisor是针对AspectJ语言的DeclareParents注解标识的切面。Default包含三个构造器:

① 参数为Advice:为目标类新增增强对象中所有接口的实现。

② 参数为DynamicIntroductionAdvice和Class: 通过增强和指定接口类创建引介切面,仅为目标对象新增Class参数的接口实现。

③ 参数为Advice和IntroductionInfo,目标对象需要实现哪些接口由IntroductionInfo的getInterfaces指定。

个人认为引介切面其实和引介增强差不了太多。没有实质性的区别

7、自动创建代理

Spring在内部使用BeanPostProcessor自动完成代理的创建工作,BeanPostProcessor只是一个接口,其实现类根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例。这些代理分为三类:

① 基于Bean配置名称规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理实例。实现类为BeanNameAutoCreator。

② 基于Advisor匹配机制的自动代理创建器:它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中,为期创建代理实例,实现类为DefaultAdvisorAutoProxyCreator。

③ 基于Bean中AspectJ注解标签的自动代理创建器:为包含AspectJ注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator。

由于自动代理创建器都实现了BeanPostProcessor,因此可以知道自动创建代理实例发生在容器实例化Bean后期加工的时候,因此自动代理创建器有机会对满足条件的Bean自动创建代理实例。

(1) BeanNameAutoProxyCreator

示例如下:

<bean id="waiter" class="com.smart.advisor.waiter"/>
<bean id="seller" class="com.smart.advisor.seller" />
<bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" />

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"
    p:beanNames="*er"
    p:interceptorNames="greetingAdvice"
    p:optimize="true"/> 

其中的beanNames属性可以通过<list>子元素或逗号,空格,分号的方式设定多个Bean名称。

(2) DefaultAdvisorAutoProxyCreator

示例如下:

<bean id="waiter" class="com.smart.advisor.Waiter" />
<bean id="seller" class="com.smart.advisor.Seller" />
<bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" />
<bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
    p:patterns=".*greet.*"
    p:advice-ref="greetingAdvice" />
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

这里,引入DefaultAdvisorAutoProxyCreator为容器中所有带“greet”方法名的目标Bean自动创建代理。注意到这里的DefaultAdvisorAutoProxyCreator只是创建了一个Bean,并没有为它指定什么属性。这个Bean会扫描所有的切面Bean,本例是regexpAdvisor, 它会按照Advisor Bean实例的属性去(ClassFilter和MethodMatcher过滤)寻找匹配的Bean,然后为这些Bean自动创建Bean。

8、AOP无法增强疑难问题剖析

必要条件:

1、JDK动态代理:必须确保要拦截的目标方法在接口中有定义;

2、CGLib动态代理:必须确保要拦截的目标方法必须为非final,非私有方法。

除此之外还可能增强失效的场景:

在方法内部之间调用的时候,不会使用被增强的代理类,而是直接通过调用未被增强原类的方法。解决方法总结如下:

总的来说就是在内部方法调用时,让被调用的方法通过代理类来调用。步骤如下:

① 定义一个接口BeanSelfProxyAware,在这个类中定义一个setSelfProxy()方法,顾名思义,就是让这个类将代理设置到自身。让目标类去实现这个接口;

② 定义一个系统所有组件都装载完毕后、准备就绪前调用的插件接口SystemBootAddon,其内部只有一个onReady()方法,用于Spring容器启动完毕之后的回调。

② 可采用一个可复用的注入装配器BeanSelfProxyAwareMounter来让实现了BeanSelfProxyAware的Bean执行自身代理Bean的注入;这个过程细分如下:

a) BeanSelfProxyAwareMounter继承SystemAddon和ApplicationContextAware接口;

b) 复写ApplicationContextAware的setApplicationContext注入ApplicationContext实例;

c) 实现SystemAddon接口的onReady()方法,在其内部利用ApplicationContext实例的getBeansOfType()方法返回Spring容器中所有实现了BeanSelfProxyAware接口的Bean的代理,调用这些Bean的setSelfProxy()方法实现自身代理的注入;

③ 最后定义一个启动管理器SystemBootManager, 让其实现ApplicationListenser<ContextRefreshedEvent>接口,让其监听Spring的ContextRefreshedEvent事件并触发BeanSelfProxyAwareMounter装配器。具体细节细分如下:

a) 自动注入所有的SystemAddon Bean(设置器参数设置为List<SystemAddon>);

b) 实现ApplicationListenser<ContextRefreshedEvent>的onApplicationEvent()方法,在其内部调用所有SystemAddon的onReady方法。

④ 让目标类实现BeanSelfProxyAware方法,在setSelfProxy()方法中设置自身代理Bean实例。

思考:②中的步骤c)的ApplicationContext实例的getBeansOfType()方法返回的是BeanSelfProxyAware接口的Bean的代理,而非Bean本身,这个是我推测的,因为setSelfProxy()方法的参数正好是这个,因此注入的应该是代理。另外系统的SystemAddon是从哪儿来的呢?其实在定义BeanSelfProxyAwareMounter时应该加上@Component,这样Spring容器会自动创建一个BeanSelfProxyAwareMounterBean,而BeanSelfProxyAwareMounter实现了SystemAddon,因此这个BeanSelfProxyAwareMounter就会自动注入到SystemBootManager中,如果我没猜错的话SystemBootManager里面的SystemAddon Bean只有一个,尽管SystemBootManager的setSystemAddons方法的参数是一个List<SystemAddon>,鉴于这个分析,这里的setSystemAddons方法改为setSystemAddon(SystemAddon systemAddon)应该也能够正常工作。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值