【翻译 Spring 5.0.4.RELEASE】6. Spring AOP APIs

6. Spring AOP APIs

6.1. Introduction

上一章介绍了Spring使用@AspectJ和基于模式的方面定义对AOP的支持。 在本章中,我们将讨论Spring 1.2应用程序中通常使用的较低级别的Spring AOP API和AOP支持。 对于新的应用程序,我们推荐使用前一章中介绍的Spring 2.0及更高版本的AOP支持,但是在使用现有应用程序或阅读书籍和文章时,您可能会遇到Spring 1.2样式的示例。 Spring 5保持向后兼容Spring 1.2,并且Spring 5完全支持本章描述的所有内容。

6.2. Pointcut API in Spring

我们来看看Spring如何处理关键的切入点概念。

6.2.1. Concepts

Spring的切入点模型使切入点可以独立于通知类型进行重用。 使用相同的切入点可以针对不同的建议。

org.springframework.aop.Pointcut接口是中央接口,用于将通知提供给特定的类和方法。 完整的界面如下所示:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

}

将Pointcut界面分割成两部分允许重用类和方法匹配部分,以及细粒度的组合操作(例如与另一个方法匹配器执行“联合”)。

ClassFilter接口用于将切入点限制为给定的一组目标类。 如果matches()方法总是返回true,则所有目标类都将匹配:

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher接口通常更重要。 完整的界面如下所示:

public interface MethodMatcher {

    boolean matches(Method m, Class targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class targetClass, Object[] args);
}

matches(Method,Class)方法用于测试此切入点是否曾经匹配目标类上的给定方法。 在创建AOP代理时可以执行此评估,以避免需要对每个方法调用进行测试。 如果对于给定方法,2参数匹配方法返回true,并且MethodMatcher的isRuntime()方法返回true,则将在每个方法调用时调用3参数匹配方法。 这使得切入点可以在目标通知执行之前立即查看传递给方法调用的参数。

大多数MethodMatchers是静态的,意味着它们的isRuntime()方法返回false。 在这种情况下,三个参数的匹配方法将永远不会被调用。

如果可能,请尝试使切入点为静态,从而允许AOP框架在创建AOP代理时缓存切入点评估的结果。

6.2.2. Operations on pointcuts

Spring支持切入点操作:特别是联合和交集。

  • 联合意味着切入点匹配的方法。

  • 交叉点意味着两个切入点匹配的方法。

  • 联盟通常更有用。

  • 切入点可以使用org.springframework.aop.support.Pointcuts类中的静态方法进行组合,也可以使用同一个包中的ComposablePointcut类进行组合。 但是,使用AspectJ切入点表达式通常是一种更简单的方法。

6.2.3. AspectJ expression pointcuts

从2.0开始,Spring使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。 这是一个使用AspectJ提供的库来解析AspectJ切入点表达式字符串的切入点。

有关支持的AspectJ切入点基元的讨论,请参阅上一章。

6.2.4. Convenience pointcut implementations

Spring提供了几个方便的切入点实现。 有些可以在盒子外面使用; 其他人打算在特定于应用程序的切入点中进行子类化。

Static pointcuts

静态切入点基于方法和目标类,并且不能考虑方法的参数。 对于大多数用途来说,静态切入点已经足够,而且是最好的。 当一个方法被第一次调用时,Spring可能只对一个静态切入点进行一次评估:在这之后,不需要再用每个方法调用来评估切入点。

让我们考虑Spring中包含的一些静态切入点实现。

Regular expression pointcuts

指定静态切入点的一种显而易见的方式是正则表达式。 除Spring之外的几个AOP框架使这成为可能。 org.springframework.aop.support.JdkRegexpMethodPointcut是一个通用的正则表达式切入点,使用JDK中的正则表达式支持。

使用JdkRegexpMethodPointcut类,可以提供模式字符串列表。 如果其中任何一个匹配,则切入点将评估为true。 (所以结果就是这些切入点的有效结合。)

用法如下所示:

<bean id="settersAndAbsquatulatePointcut"
        class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

Spring提供了一个方便的类RegexpMethodPointcutAdvisor,它允许我们也引用一个Advice(请记住,Advice可以是一个拦截器,在建议之前,抛出建议等)。 在幕后,Spring将使用JdkRegexpMethodPointcut。 使用RegexpMethodPointcutAdvisor简化了连线,因为一个bean封装了切入点和通知,如下所示:

<bean id="settersAndAbsquatulateAdvisor"
        class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
        <ref bean="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

RegexpMethodPointcutAdvisor可以用于任何建议类型。

Attribute-driven pointcuts

一个重要的静态切入点类型是一个元数据驱动的切入点。 这使用元数据属性的值:通常是源级别元数据。

Dynamic pointcuts

动态切入点比静态切入点更昂贵。 它们考虑了方法参数以及静态信息。 这意味着必须使用每个方法调用来评估它们; 结果不能被缓存,因为参数会有所不同。

主要的例子是控制流切入点。

Control flow pointcuts

Spring控制流切入点在概念上与AspectJ cflow切入点类似,但功能较弱。 (目前没有办法指定切入点在由另一个切入点匹配的连接点下执行)。控制流切入点匹配当前调用栈。 例如,如果连接点由com.mycompany.web包中的方法或SomeCaller类调用,则它可能会触发。 控制流切入点使用org.springframework.aop.support.ControlFlowPointcut类指定。

在运行时评估控制流切入点比其他动态切入点要昂贵得多。 在Java 1.4中,成本约为其他动态切入点的5倍。

6.2.5. Pointcut superclasses

Spring提供了有用的切入点超类来帮助你实现自己的切入点。

因为静态切入点非常有用,所以您可能继承了StaticMethodMatcherPointcut的子类,如下所示。 这需要实现一个抽象方法(尽管可以重写其他方法来自定义行为):

class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

还有动态切入点的超类。

您可以在Spring 1.0 RC2及更高版本中使用任何通知类型的自定义切入点。

6.2.6. Custom pointcuts

因为Spring AOP中的切入点是Java类,而不是语言特性(如AspectJ中的),所以可以声明自定义切入点,无论是静态切入点还是动态切入点。 Spring中的自定义切入点可以任意复杂。 但是,如果可能,建议使用AspectJ切入点表达式语言。

Spring的后续版本可以提供对JAC提供的“语义切入点”的支持:例如,“更改目标对象中的实例变量的所有方法”。

6.3. Advice API in Spring

现在我们来看看Spring AOP如何处理建议。

6.3.1. Advice lifecycles

每个建议是一个Spring bean。 建议实例可以在所有建议的对象上共享,或者对每个建议的对象都是唯一的。 这对应于每类或每个实例的建议。

每班建议最常使用。 适用于交易顾问等通用建议。 这些不依赖于代理对象的状态或添加新的状态; 他们只是在方法和论据上采取行动。

每个实例的建议适合引入,以支持mixin。 在这种情况下,建议将状态添加到代理对象。

可以在同一个AOP代理中混合使用共享和每个实例的建议。

6.3.2. Advice types in Spring

Spring提供了几种开箱即用的建议类型,并且可以扩展以支持任意的建议类型。 让我们看看基本概念和标准建议类型。

Interception around advice

Spring中最基本的建议类型是围绕建议进行拦截。

Spring使用方法拦截来满足AOP Alliance界面的要求。 实施周围建议的MethodInterceptors应该实现以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的MethodInvocation参数公开了正在调用的方法; 目标连接点; AOP代理; 以及该方法的参数。 invoke()方法应该返回调用的结果:连接点的返回值。

一个简单的MethodInterceptor实现如下所示:

public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}

请注意对MethodInvocation的proceed()方法的调用。 这沿着拦截器链向着连接点前进。 大多数拦截器都会调用这个方法,并返回它的返回值。 然而,MethodInterceptor与任何around通知一样,可以返回不同的值或抛出异常,而不是调用proceed方法。 但是,如果没有充分的理由你不想这样做!

MethodInterceptors提供与其他符合AOP联盟的AOP实现的互操作性。 本节其余部分讨论的其他建议类型实现常见的AOP概念,但采用Spring特有的方式。 尽管在使用最具体的建议类型方面有优势,但如果您希望在另一个AOP框架中运行该方面,请在MethodInterceptor附近提供建议。 请注意,切入点当前不能在框架之间互操作,并且AOP联盟当前不定义切入点接口。

Before advice

更简单的建议类型是以前的建议。 这不需要MethodInvocation对象,因为它只会在进入方法之前被调用。

before建议的主要优点是不需要调用proceed()方法,因此不会无意中无法顺利进入拦截器链。

MethodBeforeAdvice接口如下所示。 (Spring的API设计可以在建议之前允许字段,尽管通常的对象适用于字段拦截,Spring不可能实现它)。

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

请注意,返回类型是无效的。 建议可以在连接点执行之前插入自定义行为,但不能更改返回值。 如果before通知引发异常,则会中止进一步执行拦截器链。 异常将传播回拦截器链。 如果它没有被选中,或者被调用方法的签名被直接传递给客户端; 否则它将被AOP代理封装在未经检查的异常中。

Spring中的before建议的一个例子,它计算所有的方法调用:

public class CountingBeforeAdvice implements MethodBeforeAdvice {

    private int count;

    public void before(Method m, Object[] args, Object target) throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

在建议可以用于任何切入点之前。

Throws advice

如果连接点抛出异常,则会在返回连接点之后调用引发通知。 Spring提供了键入引发的建议。 请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法:它是一个标记接口,用于标识给定对象实现一个或多个类型化的throws建议方法。 这些应该是以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

只有最后一个参数是必需的。 方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数感兴趣。 以下类是抛出建议的示例。

如果抛出RemoteException(包括子类),则调用以下建议:

public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}

如果抛出ServletException,将调用以下建议。 与上述建议不同,它声明了4个参数,以便它可以访问被调用的方法,方法参数和目标对象:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

最后一个例子说明了如何在单个类中使用这两种方法,该类同时处理RemoteException和ServletException。 任何数量的throws建议方法都可以合并到一个类中。

public static class CombinedThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

如果throws-advice方法本身抛出异常,它将覆盖原始异常(即更改抛出给用户的异常)。 重写的异常通常是RuntimeException; 这与任何方法签名都是兼容的。 但是,如果throws-advice方法抛出一个检查的异常,它必须匹配目标方法的声明异常,因此在某种程度上与特定的目标方法签名相关联。 不要抛出与目标方法签名不兼容的未声明的检查异常!

抛出建议可以用于任何切入点。

After Returning advice

Spring中返回的建议必须实现org.springframework.aop.AfterReturningAdvice接口,如下所示:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

返回后的建议可以访问返回值(它不能修改),调用方法,方法参数和目标。

在返回通知之后的以下内容会计算所有未抛出异常的成功方法调用:

public class CountingAfterReturningAdvice implements AfterReturningAdvice {

    private int count;

    public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

此建议不会更改执行路径。 如果它抛出一个异常,这将被抛出拦截器链而不是返回值。

返回建议后可以与任何切入点一起使用。

Introduction advice

Spring将引荐建议作为一种特殊的拦截建议来对待。
简介需要一个IntroductionAdvisor和一个IntroductionInterceptor,实现以下接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

从AOP联盟的MethodInterceptor接口继承的invoke()方法必须实现这个介绍:也就是说,如果被调用的方法在引入的接口上,引入拦截器负责处理方法调用 - 它不能调用proceed()。

引言建议不能用于任何切入点,因为它仅适用于类,而不是方法级。 您只能使用IntroductionAdvisor引入建议,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class[] getInterfaces();
}

没有MethodMatcher,因此没有与介绍建议相关联的切入点。 只有类过滤是合乎逻辑的。

getInterfaces()方法返回由此顾问程序引入的接口。

validateInterfaces()方法在内部用于查看引入的接口是否可以通过配置的IntroductionInterceptor实现。

我们来看一下Spring测试套件中的一个简单示例。 假设我们想要将以下接口引入一个或多个对象:

public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}

这说明了一个mixin。我们希望能够将建议的对象转换为Lockable,无论它们的类型如何,并调用锁定和解锁方法。如果我们调用lock()方法,我们希望所有setter方法都抛出一个LockedException。因此,我们可以添加一个方面,提供使对象不可变的能力,而不需要他们有任何知识:AOP的一个很好的例子。

首先,我们需要一个完成繁重工作的IntroductionInterceptor。在这种情况下,我们扩展org.springframework.aop.support.DelegatingIntroductionInterceptor便捷类。我们可以直接实现IntroductionInterceptor,但使用DelegatingIntroductionInterceptor最适合大多数情况。

DelegatingIntroductionInterceptor的设计目的是将介绍委托给引入的接口的实际实现,隐藏使用拦截来实现。可以使用构造函数参数将代理设置为任何对象;默认的委托(当使用no-arg构造函数时)是这样的。因此,在下面的示例中,委托是DelegatingIntroductionInterceptor的LockMixin子类。给定一个委托(默认情况下),DelegatingIntroductionInterceptor实例将查找由委托实现的所有接口(除了IntroductionInterceptor之外),并支持针对它们中的任何接口。像LockMixin这样的子类可以调用suppressInterface(Class intf)方法来抑制不应该公开的接口。但是,无论有多少接口支持IntroductoryInterceptor,所使用的IntroductionAdvisor将控制实际公开的接口。引入的接口将隐藏目标对相同接口的任何实现。

因此LockMixin扩展了DelegatingIntroductionInterceptor并实现了Lockable本身。超类会自动提取Lockable可以被引用支持,所以我们不需要指定。我们可以用这种方式引入任意数量的接口。

请注意使用锁定的实例变量。这有效地将附加状态添加到目标对象中的状态。

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

    private boolean locked;

    public void lock() {
        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

    public boolean locked() {
        return this.locked;
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
            throw new LockedException();
        }
        return super.invoke(invocation);
    }

}

通常不需要重写invoke()方法:DelegatingIntroductionInterceptor实现 - 在引入方法时调用委托方法,否则继续向联接点进行 - 通常就足够了。 在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用setter方法。

介绍顾问所需要的很简单。 它需要做的只是保存一个独特的LockMixin实例,并指定引入的接口 - 在这种情况下,就是Lockable。 一个更复杂的例子可能会引用引入拦截器(将被定义为原型):在这种情况下,没有与LockMixin相关的配置,因此我们只需使用new来创建它。

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}

我们可以非常简单地应用这个顾问:它不需要配置。 (然而,这是必要的:如果没有IntroductoryAdvisor,就不可能使用IntroductionInterceptor。)与通常一样,顾问程序必须是每个实例,因为它是有状态的。 对于每个建议的对象,我们需要一个不同的LockMixinAdvisor实例,因此需要LockMixin。 顾问包含建议对象状态的一部分。

我们可以使用Advised.addAdvisor()方法以编程方式应用此顾问程序,或者像其他任何顾问程序一样在XML配置中使用推荐的方式。 下面讨论的所有代理创建选项,包括“自动代理创建者”,都能正确处理引入和状态混合。

6.4. Advisor API in Spring

在Spring中,顾问是一个只包含与切入点表达式关联的单个通知对象的方面。

除了特殊情况的介绍外,任何顾问都可以使用任何建议。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。 例如,它可以与MethodInterceptor,BeforeAdvice或ThrowsAdvice一起使用。

Spring可以在同一个AOP代理中混合顾问和建议类型。 例如,您可以在一个代理配置中使用围绕建议的拦截,抛出建议和建议之前:Spring将自动创建必要的拦截器链。

6.5. Using the ProxyFactoryBean to create AOP proxies

如果您使用Spring IoC容器(ApplicationContext或BeanFactory)作为业务对象,那么您应该! - 你会想使用Spring的AOP FactoryBeans。 (请记住,一个工厂bean引入了一个间接层,使其能够创建不同类型的对象。)

Spring AOP支持也使用封面下的工厂bean。

在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。 这样可以完全控制将要应用的切入点和建议,以及它们的排序。 但是,如果您不需要这种控制,则有更简单的选项。

6.5.1. Basics

与其他Spring FactoryBean实现一样,ProxyFactoryBean引入了间接级别。 如果使用名称foo定义ProxyFactoryBean,则引用foo的引用的对象不是ProxyFactoryBean实例本身,而是由ProxyFactoryBean实现getObject()方法创建的对象。 此方法将创建包装目标对象的AOP代理。

使用ProxyFactoryBean或另一个支持IoC的类创建AOP代理的最重要的好处之一是,这意味着建议和切入点也可以由IoC管理。 这是一个强大的功能,实现了其他AOP框架难以实现的某些方法。 例如,建议本身可以引用应用程序对象(除了应该在任何AOP框架中可用的目标),从依赖注入提供的所有可插入性中受益。

6.5.2. JavaBean properties

与Spring提供的大多数FactoryBean实现一样,ProxyFactoryBean类本身就是一个JavaBean。 其属性用于:

  • 指定您想要代理的目标。

  • 指定是否使用CGLIB(请参阅下文以及基于JDK和CGLIB的代理)。

一些关键属性是从org.springframework.aop.framework.ProxyConfig继承而来的(Spring中所有AOP代理工厂的超类)。 这些关键属性包括:

  • proxyTargetClass:如果要代理目标类,则为true,而不是目标类的接口。如果此属性值设置为true,则会创建CGLIB代理(但也可以参阅基于JDK和CGLIB的代理)。

  • optimize:控制是否将积极的优化应用于通过CGLIB创建的代理。除非完全理解相关AOP代理如何处理优化,否则不应该轻率使用此设置。这目前仅用于CGLIB代理;它对JDK动态代理无效。

  • frozen:如果代理配置被冻结,则不再允许更改配置。这对于稍微优化以及在代理创建后不希望调用者能够操作代理(通过Advised界面)的情况都很有用。此属性的默认值为false,因此允许更改(如添加其他建议)。

  • exposeProxy:确定当前代理是否应该暴露在ThreadLocal中,以便目标可以访问它。如果目标需要获取代理并且exposeProxy属性设置为true,则目标可以使用AopContext.currentProxy()方法。

ProxyFactoryBean特有的其他属性包括:

  • proxyInterfaces:String接口名称的数组。 如果未提供,则将使用目标类的CGLIB代理(但也请参阅基于JDK和CGLIB的代理)。

  • interceptorNames:要应用的Advisor,拦截器或其他建议名称的字符串数组。 订购很重要,先到先得。 也就是说,列表中的第一个拦截器将是第一个能够拦截调用的拦截器。

名称是当前工厂中的bean名称,包括来自祖先工厂的bean名称。 您不能在这里提及bean引用,因为这样做会导致ProxyFactoryBean忽略通知的单例设置。

你可以附加一个带有星号(*)的拦截器名称。 这将导致应用名称以星号前的部分开头的所有顾问bean。 使用此功能的示例可以在使用’全球’顾问中找到。

  • singleton:无论工厂应该返回单个对象,无论调用getObject()方法的频率如何。 几个FactoryBean实现提供了这样一种方法。 默认值是true。 如果你想使用有状态的建议 - 例如,有状态的混合 - 使用原型建议以及单值false。

6.5.3. JDK- and CGLIB-based proxies

本节作为关于ProxyFactoryBean如何选择为特定目标对象(即将被代理)创建一个基于JDK和CGLIB的代理的权威性文档。

ProxyFactoryBean在创建基于JDK或CGLIB的代理方面的行为在Spring的1.2.x和2.0版本之间发生了变化。 现在,ProxyFactoryBean展示了类似于TransactionProxyFactoryBean类的自动检测接口的语义。

如果要被代理的目标对象(以下简称为目标类)的类没有实现任何接口,则将创建一个基于CGLIB的代理。这是最简单的情况,因为JDK代理是基于接口的,没有接口意味着JDK代理甚至不可能。一个只需插入目标bean,并通过interceptorNames属性指定拦截器列表。请注意,即使ProxyFactoryBean的proxyTargetClass属性已设置为false,也会创建基于CGLIB的代理。 (显然这是没有意义的,并且最好从bean定义中删除,因为它至多是多余的,并且最糟糕的是混淆。)

如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。

如果ProxyFactoryBean的proxyTargetClass属性已设置为true,则将创建一个基于CGLIB的代理。这是有道理的,并且符合最少突击的原则。即使ProxyFactoryBean的proxyInterfaces属性已设置为一个或多个完全限定的接口名称,但proxyTargetClass属性设置为true的事实将导致基于CGLIB的代理生效。

如果ProxyFactoryBean的proxyInterfaces属性已设置为一个或多个完全限定接口名称,则将创建一个基于JDK的代理。创建的代理将实现在proxyInterfaces属性中指定的所有接口;如果目标类恰好实现了比proxyInterfaces属性中指定的接口多得多的接口,那就很好,但这些附加接口将不会由返回的代理实现。

如果ProxyFactoryBean的proxyInterfaces属性尚未设置,但目标类确实实现了一个(或多个)接口,则ProxyFactoryBean将自动检测目标类实际上是否至少实现了一个接口,以及JDK-将创建基于代理的代理。实际被代理的接口将是目标类实现的所有接口;实际上,这与简单地将目标类实现的每个接口的列表提供给proxyInterfaces属性相同。但是,它的工作量要少得多,而且不太容易出现拼写错误。

6.5.4. Proxying interfaces

让我们看一下ProxyFactoryBean的一个简单例子。 这个例子涉及:

  • 将被代理的目标bean。 这是下例中的“personTarget”bean定义。

  • 顾问和拦截器用于提供建议。

  • 一个AOP代理bean定义,指定目标对象(personTarget bean)以及要代理的接口以及要应用的通知。

<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name" value="Tony"/>
    <property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>

    <property name="target" ref="personTarget"/>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

请注意,interceptorNames属性接受一个String列表:当前工厂中拦截器或顾问的bean名称。 顾问,拦截器之前,在返回和抛出通知对象之后都可以使用。 顾问的排序很重要。

您可能想知道为什么列表不包含bean引用。 原因是如果ProxyFactoryBean的singleton属性设置为false,它必须能够返回独立的代理实例。 如果任何顾问本身就是一个原型,那么就需要返回一个独立的实例,所以有必要从工厂获得一个原型实例; 举办参考是不够的。

上面的“person”bean定义可以用来代替Person实现,如下所示:

Person person = (Person) factory.getBean("person");

与普通的Java对象一样,同一IoC上下文中的其他bean可以表达强类型依赖关系:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>

此示例中的PersonUser类将公开Person类型的属性。 就其而言,AOP代理可以透明地用来代替“真实”的人员实施。 但是,它的类将是一个动态代理类。 将它转换到Advised接口(下面讨论)是可能的。

可以使用匿名内部bean隐藏目标和代理之间的区别,如下所示。 只有ProxyFactoryBean定义不同; 该建议仅包括完整性:

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <!-- Use inner bean, not local reference to target -->
    <property name="target">
        <bean class="com.mycompany.PersonImpl">
            <property name="name" value="Tony"/>
            <property name="age" value="51"/>
        </bean>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

这具有如下优点:只有一个Person类型的对象:如果我们想阻止应用程序上下文的用户获得对未建议对象的引用,或者需要避免使用Spring IoC自动装配出现任何歧义,那么这很有用。 ProxyFactoryBean定义是独立的,这也有一个优势。 但是,有时能够从工厂获得未建议的目标可能实际上是一个优势:例如,在某些测试场景中。

6.5.5. Proxying classes

如果您需要代理一个类而不是一个或多个接口,该怎么办?

想象一下,在我们上面的示例中,没有Person接口:我们需要建议一个名为Person的类,它没有实现任何业务接口。在这种情况下,您可以将Spring配置为使用CGLIB代理,而不是动态代理。只需将上面的ProxyFactoryBean上的proxyTargetClass属性设置为true即可。尽管最好对接口进行编程,而不是类,但在处理遗留代码时,建议不实现接口的类的能力会很有用。 (一般来说,Spring不是规定性的,虽然它可以很容易地应用良好的实践,但它避免了强制某种特定的方法。)

如果你愿意,你可以在任何情况下强制使用CGLIB,即使你有接口。

CGLIB代理通过在运行时生成目标类的子类来工作。 Spring将这个生成的子类配置为将方法调用委托给原始目标:子类用于实现Decorator模式,在建议中编织。

CGLIB代理通常应该对用户透明。但是,有一些问题需要考虑:

  • 最终方法不能被建议,因为它们不能被覆盖。

  • 不需要将CGLIB添加到您的类路径中。从Spring 3.2开始,CGLIB被重新包装并包含在Spring-JAR中。换句话说,与JDK动态代理一样,基于CGLIB的AOP将“开箱即用”。

CGLIB代理与动态代理之间几乎没有性能差异。从Spring 1.0开始,动态代理稍快一点。但是,这可能在未来发生变化。在这种情况下,绩效不应该成为决定性的考虑因素。

6.5.6. Using ‘global’ advisors

通过向拦截器名称附加星号,所有具有匹配星号之前部分的bean名称的顾问将被添加到顾问链中。 如果您需要添加一组标准的“全球”顾问,这可以派上用场:

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="service"/>
    <property name="interceptorNames">
        <list>
            <value>global*</value>
        </list>
    </property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>

6.6. Concise proxy definitions

特别是在定义事务代理时,最终可能会有许多类似的代理定义。 使用父和子bean定义以及内部bean定义可以产生更简洁,更简洁的代理定义。

首先为代理创建一个父代,模板,bean定义:

<bean id="txProxyTemplate" abstract="true"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

这将永远不会实例化,因此实际上可能不完整。 然后,每个需要创建的代理只是一个子bean定义,它将代理的目标作为内部bean定义进行包装,因为目标永远不会自行使用。

<bean id="myService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MyServiceImpl">
        </bean>
    </property>
</bean>

当然可以覆盖父模板的属性,例如在这种情况下,事务传播设置:

<bean id="mySpecialService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MySpecialServiceImpl">
        </bean>
    </property>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="store*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

请注意,在上面的示例中,我们已经明确地将父bean定义标记为抽象,如前所述,使用abstract属性,以便它实际上可能不会实例化。 应用程序上下文(但不是简单的bean工厂)将默认预先实例化所有单例。 因此,如果你有一个你打算只用作模板的(父)bean定义,并且这个定义指定了一个类,那么重要的是(至少对于单例bean),你必须确保将abstract属性设置为true, 否则应用程序上下文将实际尝试预先实例化它。

6.7. Creating AOP proxies programmatically with the ProxyFactory

使用Spring以编程方式创建AOP代理很容易。 这使您能够在不依赖于Spring IoC的情况下使用Spring AOP。

以下清单显示了使用一个拦截器和一个顾问程序为目标对象创建代理。 目标对象实现的接口将自动被代理:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是构建一个org.springframework.aop.framework.ProxyFactory类型的对象。 您可以使用目标对象来创建此对象,如上例所示,或者指定要在替代构造函数中进行代理的接口。

您可以添加建议(使用拦截器作为专门的建议)和/或顾问,并在ProxyFactory的生命周期中操作它们。 如果添加IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。

ProxyFactory上还有一些便利方法(继承自AdvisedSupport),它们允许您添加其他的建议类型,比如before和throws建议。 AdvisedSupport是ProxyFactory和ProxyFactoryBean的超类。

将AOP代理创建与IoC框架集成是大多数应用程序的最佳实践。 与一般情况相同,我们建议您使用AOP从Java代码中外化配置。

6.8. Manipulating advised objects

但是,您创建AOP代理,您可以使用org.springframework.aop.framework.Advised界面来操作它们。 任何AOP代理都可以转换为此接口,无论它实现了哪个其他接口。 该界面包含以下方法:

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors()方法将为已添加到工厂的每个顾问程序,拦截器或其他建议类型返回Advisor。如果您添加了顾问,则此索引处的退货顾问将成为您添加的对象。如果您添加了拦截器或其他建议类型,Spring将用一个总是返回true的切入点将其包装在顾问程序中。因此,如果您添加了一个MethodInterceptor,那么为该索引返回的顾问将是一个返回您的MethodInterceptor的DefaultPointcutAdvisor,以及一个匹配所有类和方法的切入点。

addAdvisor()方法可以用来添加任何Advisor。通常持有切入点和建议的顾问将是通用的DefaultPointcutAdvisor,它可以用于任何建议或切入点(但不能用于介绍)。

默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。唯一的限制是无法添加或删除引入顾问程序,因为来自工厂的现有代理程序不会显示界面更改。 (您可以从工厂获取新代理以避免此问题。)

一个将AOP代理投射到Advised界面并检查和操作其建议的简单示例:

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());

// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);

尽管毫无疑问,合法的使用案例是否可行(是否意图)修改生产中的业务对象的建议值得怀疑。 但是,它在开发中可能非常有用:例如,在测试中。 我有时发现能够以拦截器或其他建议的形式添加测试代码非常有用,可以进入我想测试的方法调用。 (例如,建议可以进入为该方法创建的事务中:例如,在标记事务回滚之前,运行SQL以检查数据库是否已正确更新。)

根据创建代理的方式,通常可以设置一个冻结标志,在这种情况下,建议的isFrozen()方法将返回true,并且任何尝试通过添加或删除来修改建议都会导致AopConfigException。 在某些情况下,冻结建议对象状态的功能很有用,例如,可以防止调用代码移除安全拦截器。 如果知道不需要修改运行时建议,它也可以在Spring 1.1中使用,以允许激进的优化。

6.9. Using the “auto-proxy” facility

到目前为止,我们已经考虑使用ProxyFactoryBean或类似的工厂bean显式创建AOP代理。

Spring还允许我们使用“自动代理”bean定义,它可以自动代理选定的bean定义。 这是建立在Spring“bean post processor”基础上的,它可以在容器加载时修改任何bean定义。

在这个模型中,您在XML bean定义文件中设置了一些特殊的bean定义来配置自动代理基础结构。 这允许您只声明符合自动代理的目标:您不需要使用ProxyFactoryBean。

有两种方法可以做到这一点:

  • 使用在当前上下文中引用特定bean的自动代理创建器。

  • 自动代理创建的一个特例,值得单独考虑; 由源代码级元数据属性驱动的自动代理创建。

6.9.1. Autoproxy bean definitions

org.springframework.aop.framework.autoproxy包提供了以下标准自动代理创建者。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator类是一个BeanPostProcessor,它自动为名称与字面值或通配符匹配的bean创建AOP代理。

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

与ProxyFactoryBean一样,有一个interceptorNames属性而不是一个拦截器列表,以允许原型顾问的正确行为。 命名为“拦截者”可以是顾问或任何建议类型。

与一般的自动代理一样,使用BeanNameAutoProxyCreator的要点是将相同的配置一致地应用于多个对象,同时配置量最小。 将声明性事务应用于多个对象是一种流行的选择。

在上例中,名称匹配的Bean定义(例如“jdkMyBean”和“onlyJdk”)是带目标类的普通旧式bean定义。 一个AOP代理将由BeanNameAutoProxyCreator自动创建。 相同的建议将应用于所有匹配的bean。 请注意,如果使用顾问(而不是上述示例中的拦截器),则切入点可能对不同的bean有不同的应用。

DefaultAdvisorAutoProxyCreator

一个更通用和非常强大的自动代理创建器是DefaultAdvisorAutoProxyCreator。 这将自动在当前上下文中应用符合条件的顾问,而不需要在自动代理顾问程序的bean定义中包含特定的bean名称。 它提供了与BeanNameAutoProxyCreator相同的配置和避免重复的优点。

使用这种机制涉及:

  • 指定一个DefaultAdvisorAutoProxyCreator bean定义。

  • 在相同或相关的上下文中指定任意数量的顾问。 请注意,这些必须是顾问,而不仅仅是拦截器或其他建议。 这是必要的,因为必须有一个切入点来评估,以检查每个通知是否符合候选bean定义的资格。

DefaultAdvisorAutoProxyCreator将自动评估每个顾问程序中包含的切入点,以查看它应该应用于每个业务对象(如示例中的“businessObject1”和“businessObject2”)的建议(如果有的话)。

这意味着可以将任意数量的顾问自动应用于每个业务对象。 如果任何顾问中的任何切入点都不匹配业务对象中的任何方法,则该对象将不会被代理。 由于为新业务对象添加了bean定义,因此必要时它们将自动进行代理。

一般而言,自动代理具有使呼叫者或依赖关系不可能获得未建议的对象的优点。 在此ApplicationContext上调用getBean(“businessObject1”)将返回AOP代理,而不是目标业务对象。 (之前显示的“内豆”成语也提供了这种好处。)

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>

如果您想要将相同的建议一致地应用于许多业务对象,则DefaultAdvisorAutoProxyCreator非常有用。 一旦基础架构定义就绪后,您就可以简单地添加新的业务对象,而无需包含特定的代理配置。 您还可以非常轻松地添加其他方面 - 例如跟踪或性能监视方面 - 只需对配置进行最少的更改即可。

DefaultAdvisorAutoProxyCreator支持过滤(使用命名约定,以便只评估某些顾问,允许在同一工厂使用多个不同配置的AdvisorAutoProxyCreators)并进行排序。 顾问可以实现org.springframework.core.Ordered接口,以确保正确的排序,如果这是一个问题。 上例中使用的TransactionAttributeSourceAdvisor具有可配置的订单值; 默认设置是无序的。

6.10. Using TargetSources

Spring提供了一个TargetSource的概念,用org.springframework.aop.TargetSource接口表示。 该接口负责返回实现连接点的“目标对象”。 每当AOP代理处理方法调用时,都会要求TargetSource实现目标实例。

使用Spring AOP的开发人员通常不需要直接使用TargetSource,但这提供了一种强大的支持池,热交换和其他复杂目标的手段。 例如,池的TargetSource可以为每个调用返回一个不同的目标实例,使用一个池来管理实例。

如果您未指定TargetSource,则会使用包装本地对象的默认实现。 每次调用都会返回相同的目标(如您所期望的那样)。

让我们看看Spring提供的标准目标资源,以及如何使用它们。

当使用自定义目标源时,你的目标通常需要是一个原型而不是单一的bean定义。 这允许Spring在需要时创建新的目标实例。

6.10.1. Hot swappable target sources

org.springframework.aop.target.HotSwappableTargetSource的存在允许AOP代理的目标被切换,同时允许呼叫者保留对它的引用。

更改目标源的目标会立即生效。 HotSwappableTargetSource是线程安全的。

您可以通过HotSwappableTargetSource上的swap()方法更改目标,如下所示:

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

所需的XML定义如下所示:

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
    <constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="swapper"/>
</bean>

上面的swap()调用改变了可交换bean的目标。 持有该bean的引用的客户将不知道该更改,但会立即开始触发新目标。

虽然这个例子没有添加任何建议 - 并且没有必要添加使用TargetSource的建议 - 当然,任何TargetSource都可以与任意建议一起使用。

6.10.2. Pooling target sources

使用池化目标源向无状态会话EJB提供了类似的编程模型,其中维护了相同实例池,并使用方法调用去释放池中的对象。

Spring池和SLSB池之间的一个重要区别是Spring池可以应用于任何POJO。 与一般的Spring一样,该服务可以以非侵入方式应用。

Spring为Commons Pool 2.2提供了开箱即用的支持,它提供了相当高效的池化实现。 您需要应用程序类路径上的commons-pool Jar才能使用此功能。 也可以对org.springframework.aop.target.AbstractPoolingTargetSource进行子类化以支持任何其他池化API。

Commons Pool 1.5+也支持,但从Spring Framework 4.2开始不推荐使用。

示例配置如下所示:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
        scope="prototype">
    ... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
    <property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="poolTargetSource"/>
    <property name="interceptorNames" value="myInterceptor"/>
</bean>

请注意,目标对象 - 示例中的“businessObjectTarget” - 必须是原型。 这允许PoolingTargetSource实现根据需要创建目标的新实例以增大池。 查看AbstractPoolingTargetSource的javadoc和您希望用于获取其属性信息的具体子类:“maxSize”是最基本的,并且始终保证呈现。

在这种情况下,“myInterceptor”是需要在同一个IoC上下文中定义的拦截器的名称。 但是,没有必要指定拦截器来使用池。 如果您只想要合并,并且没有其他建议,请不要设置interceptorNames属性。

可以配置Spring,以便能够将任何池化对象转换到org.springframework.aop.target.PoolingConfig接口,该接口通过介绍公开有关池的配置和当前大小的信息。 你需要像这样定义一个顾问:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="poolTargetSource"/>
    <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

这个顾问程序是通过调用AbstractPoolingTargetSource类的便捷方法获得的,因此可以使用MethodInvokingFactoryBean。 此顾问程序的名称(此处为“poolConfigAdvisor”)必须位于ProxyFactoryBean的拦截器名称列表中,以暴露池中的对象。

演员阵容如下:

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

汇集无状态服务对象通常不是必需的。 我们不相信它应该是默认选择,因为大多数无状态对象自然是线程安全的,并且如果资源被缓存,实例池是有问题的。

使用自动代理可以使用更简单的池。 可以设置任何自动代理创建者使用的TargetSources。

6.10.3. Prototype target sources

设置“原型”目标源与合并TargetSource类似。 在这种情况下,每个方法调用都会创建一个新的目标实例。 尽管在现代JVM中创建新对象的成本不高,但连接新对象(满足其IoC依赖性)的成本可能更高。 因此,如果没有很好的理由,你不应该使用这种方法。

为此,您可以修改上面显示的poolTargetSource定义,如下所示。 (为了清楚,我也改了名字。)

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
    <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

只有一个属性:目标bean的名称。 TargetSource实现中使用了继承来确保一致的命名。 与池化目标源一样,目标bean必须是原型bean定义。

6.10.4. ThreadLocal target sources

如果您需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源非常有用。 ThreadLocal的概念提供了一个JDK范围的工具来透明地将资源与线程一起存储。 设置ThreadLocalTargetSource几乎与其他类型的目标源所解释的一样:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>

当在多线程和多类加载器环境中错误地使用它们时,ThreadLocals会带来严重的问题(可能导致内存泄漏)。 一个人应该总是考虑在其他类中包装一个threadlocal,并且永远不要直接使用ThreadLocal本身(当然在包装类中除外)。 另外,应该始终记住正确设置和取消设置(后者只需调用ThreadLocal.set(null))线程的本地资源。 在任何情况下都应该进行取消设置,因为不会导致问题可能会导致问题行为。 Spring的ThreadLocal支持为你做到了这一点,并且应该始终考虑使用没有其他适当处理代码的ThreadLocals。

6.11. Defining new Advice types

Spring AOP被设计为可扩展的。 虽然拦截实施策略目前在内部使用,但除了在建议之前,抛出建议和返回建议之后,还可以支持任意建议类型以及开箱即用的拦截。

org.springframework.aop.framework.adapter包是一个SPI包,允许在不更改核心框架的情况下添加新的自定义建议类型。 自定义建议类型的唯一约束是它必须实现org.aopalliance.aop.Advice标记接口。

有关更多信息,请参阅org.springframework.aop.framework.adapter javadocs。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值