为了账号安全,请及时绑定邮箱和手机立即绑定

Spring 5 中文解析核心篇-IoC容器之Spring AOP API

标签:
Java Spring

上一章通过@AspectJ和基于schema的切面定义描述了Spring对AOP的支持。在本章中,我们讨论了较低级别的Spring AOP API。对于常见的应用程序,我们建议将Spring AOP与AspectJ切入点一起使用,如上一章所述。

6.1 本节描述了Spring如何处理关键切入点概念。
6.1.1 概念

Spring的切入点模型使切入点重用不受通知类型的影响。你可以使用相同的切入点来定位不同的通知。org.springframework.aop.Pointcut接口是核心接口,用于将通知定向到特定的类和方法。完整的接口如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

}

Pointcut接口分为两部分,可以重用类和方法匹配的部分以及细粒度的合成操作(例如与另一个方法匹配器执行“联合”)。

ClassFilter接口用于将切入点限制为给定的一组目标类。如果matches()方法始终返回true,则将匹配所有目标类。以下清单显示了ClassFilter接口定义:

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);
}

matchs(Method,Class)方法用于测试此切入点是否与目标类上的给定方法匹配。创建AOP代理时可以执行此评估,以避免需要对每个方法调用进行测试。如果两个参数的match方法对于给定的方法返回true,并且MethodMatcherisRuntime()方法返回true,则在每次方法调用时都将调用三个参数的match方法。这样,切入点就可以在执行目标通知之前立即查看传递给方法调用的参数。

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

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

6.1.2 切入点的操作

Spring支持切入点上的操作(特别是联合和交集)。

联合表示两个切入点匹配其中一个的方法。交集是指两个切入点都匹配的方法。联合通常更有用。你可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法或使用同一包中的ComposablePointcut类来组成切入点。但是,使用AspectJ切入点表达式通常是一种更简单的方法。但是,使用AspectJ切入点表达式通常是一种更简单的方法。

6.1.3 AspectJ 表达式切入点

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

有关支持的AspectJ切入点原语的讨论,请参见上一章

6.1.4 便捷切入点实现

Spring提供了几种方便的切入点实现。你可以直接使用其中一些。其他的则打算在特定于应用程序的切入点中被子类化。

静态切入点

静态切入点基于方法和目标类,并且不能考虑方法的参数。静态切入点足以满足大多数用途,并且最好。首次调用方法时,Spring只能评估一次静态切入点。之后,无需在每次方法调用时再次评估切入点(备注:第一次评估后进行缓存)。

本节的其余部分描述了Spring附带的一些静态切入点实现。

正则表达式切入点

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

使用JdkRegexpMethodPointcut类,可以提供模式字符串的列表。如果其中任何一个匹配,则切入点的评估结果为true。(因此,结果实际上是这些切入点的并集。)

以下示例显示如何使用JdkRegexpMethodPointcut

<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与任何Advice类型一起使用。

属性驱动的切入点

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

动态切入点

动态切入点比静态切入点更昂贵。它们考虑了方法参数以及静态信息。这意味着必须在每次方法调用时对它们进行评估,并且由于参数会有所不同,因此无法缓存结果。

主要示例是control flow切入点。

控制流切入点

Spring控制流切入点在概念上类似于AspectJ cflow切入点,尽管功能不那么强大。(目前还没有办法指定一个切入点在与另一个切入点匹配的连接点下面执行。)控制流切入点与当前调用堆栈匹配。例如,如果连接点是由com.mycompany.web包中的方法或SomeCaller类调用的,则可能会触发。通过使用org.springframework.aop.support.ControlFlowPointcut类指定控制流切入点。通过使用org.springframework.aop.support.ControlFlowPointcut类指定控制流切入点。

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


6.1.5 切入点超类

Spring提供了有用的切入点超类,以帮助你实现自己的切入点。因为静态切入点最有用,所以你可能应该子类化StaticMethodMatcherPointcut。这仅需要实现一个抽象方法(尽管你可以覆盖其他方法以自定义行为)。下面的示例显示如何对StaticMethodMatcherPointcut进行子类化:

class TestStaticPointcut extends StaticMethodMatcherPointcut {

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

动态切入点也有超类。你可以将自定义切入点与任何通知类型一起使用。

6.1.6 自定义切面

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

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

6.2 Spring中的通知API

现在,我们可以检查Spring AOP如何处理通知。

6.2.1 通知生命周期

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

每个类通知最常用。适用于一般通知,例如事物advisors(切面和通知组合)。这些不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。

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

你可以在同一AOP代理中混合使用共享通知和基于实例的通知。

6.2.2 Spring中通知类型

Spring提供了几种通知类型,并且可以扩展以支持任意通知类型。本节介绍基本概念和标准通知类型。

拦截环绕通知

Spring中最基本的通知类型是环绕通知的拦截。

对于使用方法拦截的通知,Spring符合AOP Alliance接口。实现MethodInterceptor和环绕通知的类也应该实现以下接口:

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;
    }
}

请注意对MethodInvocationproceed()方法的调用。这沿着拦截器链向下到达连接点。大多数拦截器都调用此方法并返回其返回值。但是,MethodInterceptor就像其他的环绕通知一样,可以返回不同的值或引发异常,而不是调用proceed方法。但是,你没有充分的理由就不要这样做。

MethodInterceptor实现提供与其他符合AOP Alliance要求的AOP实现的互操作性。本节其余部分讨论的其他通知类型将实现常见的AOP概念,但以特定于Spring的方式。尽管使用最具体的通知类型有一个优势,但是如果你可能想在另一个AOP框架中运行切面,则在环绕通知使用MethodInterceptor。请注意,切入点当前无法在框架之间互操作,并且AOP Alliance当前未定义切入点接口。

前置通知

一种最简单的通知类型是前置通知。这个不需要MethodInvocation对象,因为它仅仅在进入方法前被调用。

前置通知的主要优点在于,无需调用proceed()方法,因此,不会因疏忽而未能沿拦截器链继续前进。

以下清单显示了MethodBeforeAdvice接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

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

(尽管通常的对象适用于字段拦截,并且Spring不太可能实现它,但Spring的API设计允许前置通知。)

请注意,返回类型为void。通知可以在连接点执行之前插入自定义行为,但不能更改返回值。如果前置的通知引发异常,它将中止拦截器链的进一步执行。异常会传播回拦截器链。如果是未检查异常在调用的方法的签名上,则将其直接传递给客户端。否则,它将被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;
    }
}

前置通知可以被使用于任何切入点。

异常通知

如果连接点引发异常,则在连接点返回后调用引发通知。Spring提供抛出异常通知。注意这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。这是一个标记接口,表示这个对象实现一个或多个抛出异常通知方法。这些应采用以下形式:

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

仅最后一个参数是必需的。方法签名可以具有一个或四个参数,具体取决于通知方法是否对该方法参数感兴趣。接下来的两个清单显示类,它们是抛出异常通知的示例。

如果引发RemoteException(包括从子类),则调用以下通知:

public class RemoteThrowsAdvice implements ThrowsAdvice {

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

与前面的通知不同,下一个示例声明了四个参数,以便可以访问被调用的方法、方法参数和目标对象。如果抛出ServletException,则调用以下通知:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

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

最后一个示例说明如何在处理RemoteExceptionServletException的单个类中使用这两种方法。可以将任意数量的异常通知方法组合到一个类中。以下清单显示了最后一个示例:

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方法抛出一个已检查的异常,则它必须与目标方法的声明异常匹配,因此在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的未声明的检查异常!

异常通知可以被使用与任何切入点。

后置返回通知

在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;
    }
}

此通知不会更改执行路径。如果它抛出异常,则抛出的是拦截器链,而不是返回值。

后置返回通知可以被用于任何切入点。

引入通知

Spring将引入通知视为一种特殊的拦截通知。

引入需要实现以下接口的IntroductionAdvisorIntroductionInterceptor

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

从AOP Alliance MethodInterceptor接口继承的invoke()方法必须实现引入。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用,不能调用proceed()

引入通知不能与任何切入点一起使用,因为它仅适用于类,而不适用于方法级别。你只能通过IntroductionAdvisor使用引入通知,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class<?>[] getInterfaces();
}

没有MethodMatcher,因此没有与引入通知相关的Pointcut。只有类过滤是合乎逻辑的。

getInterfaces()方法返回此advisor引入的接口。

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

考虑一下Spring测试套件中的一个示例,并假设我们想为一个或多个对象引入以下接口:

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

这说明了混合。我们希望能够将通知对象强制转换为Lockable,无论它们的类型和调用锁和解锁方法如何。如果我们调用lock()方法,我们希望所有的setter方法都抛出一个LockedException。因此,我们可以添加一个切面,使对象在不了解对象的情况下不可变:AOP的一个很好的例子。

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

DelegatingIntroductionInterceptor被设计为将引入委托给所引入接口的实际实现,而隐藏了监听的使用。你可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)是this。因此,在下一个示例中,委托是DelegatingIntroductionInterceptorLockMixin子类。给定一个委托(默认情况下为本身),DelegatingIntroductionInterceptor实例将查找由委托实现的所有接口(IntroductionInterceptor除外),并支持针对其中任何一个的引入。诸如LockMixin的子类可以调用suppressInterface(Class intf)方法来禁止不应公开的接口。但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor被使用控制实际公开哪些接口。引入的接口隐藏了目标对同一接口的任何实现。

因此,LockMixin扩展了DelegatingIntroductionInterceptor并实现了Lockable本身。超类会自动选择可以支持Lockable进行引入的方法,因此我们不需要指定它。我们可以通过这种方式引入任意数量的接口。

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

下面的示例显示LockMixin类:

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创建它。以下示例显示了我们的LockMixinAdvisor类:

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

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

我们可以非常简单地应用此advisor程序,因为它不需要配置。(但是,如果没有IntroductionAdvisor,则无法使用IntroductionInterceptor。)像通常的介绍一样,advisor必须是按实例的,因为它是有状态的。对于每个被通知的对象,我们需要一个LockMixinAdvisor的不同实例,因此也需要LockMixin的不同实例。advisor包含被通知对象状态的一部分。我们可以像其他任何advisor一样,通过使用Advised.addAdvisor()方法或XML配置(推荐方式)以编程方式应用此advisor。下文讨论的所有代理创建选择,包括“自动代理创建器”,都可以正确处理引入和有状态的混合。

6.3 在Spring中的Advisor API

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

除了介绍的特殊情况外,任何advisor都可以与任何通知一起使用。org.springframework.aop.support.DefaultPointcutAdvisor是最常用的advisor类。它可以与MethodInterceptorBeforeAdviceThrowsAdvice一起使用。

可以在Spring中将advisoradvice类型混合在同一个AOP代理中。在一个代理配置中,可以使用环绕通知、异常通知和前置通知的拦截。Spring自动创建必要的拦截器链。

6.4 使用ProxyFactoryBean创建AOP代理

如果你的业务对象使用Spring IoC容器(一个ApplicationContextBeanFactory)(你应该这样做!),那么你想使用Spring的AOP FactoryBean实现之一。(请记住,工厂bean引入了一个间接层,使它可以创建其他类型的对象。)

Spring AOP支持还在后台使用了工厂bean。

在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样可以完全控制切入点,任何适用的通知及其顺序。但是,如果不需要这样的控制,则有一些更简单的选项比较可取。

6.4.1 基础

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

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

6.4.2 JavaBean属性

与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: 如果代理配置被冻结,则不再允许对配置进行更改。当你不希望调用者在创建代理后(通过已通知接口)能够操作代理时,这对于轻微的优化是非常有用的。此属性的默认值为false,因此允许进行更改(例如添加其他通知)。
  • exposeProxy:确定当前代理是否应该在ThreadLocal中暴露,以便目标可以访问它。如果目标需要获取代理,并且暴露代理属性设置为true,则目标可以使用AopContext.currentProxy()方法。

ProxyFactoryBean特有的其他属性包括:

  • proxyInterfaces:字符串接口名称的数组。如果未提供,则使用目标类的CGLIB代理(另请参见基于JDK和CGLIB的代理)。
  • interceptorNames: Advisor,拦截器或要应用的其他通知名称的字符串数组。顺序很重要,先到先得。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。
  • 你可以在拦截器名称后加上星号(*)。这样做会导致所有advisor bean的应用程序的名称都以要应用的星号之前的部分开头。你可以在使用Global Advisors中找到使用此特性的示例。
  • singleton:无论getObject()方法被调用的频率如何,工厂是否应返回单个对象。一些FactoryBean实现提供了这种方法。默认值是true。如果你想使用有状态通知—例如,有状态混合—使用原型通知和单例值false
6.4.3 基于JDK和CGLIB代理

本部分是有关ProxyFactoryBean如何选择为特定目标对象(将被代理)创建基于JDK的代理或基于CGLIB的代理的权威性文档。

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

如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则创建基于CGLIB的代理。这是最简单的情况,因为JDK代理是基于接口的,并且没有接口意味着甚至无法进行JDK代理。你可以插入目标bean并通过设置interceptorNames属性来指定拦截器列表。请注意,即使ProxyFactoryBeanproxyTargetClass属性已设置为false,也会创建基于CGLIB的代理。(这样做没有任何意义,最好将其从bean定义中删除,因为它充其量是多余,并且在最糟的情况下会造成混淆。)

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

如果ProxyFactoryBeanproxyTargetClass属性已设置为true,则将创建基于CGLIB的代理。这很有道理,也符合最小意外原则。即使ProxyFactoryBeanproxyInterfaces属性被设置为一个或多个完全限定的接口名,proxyTargetClass属性被设置为true也会使基于cglib的代理生效。

如果ProxyFactoryBeanproxyInterfaces属性被设置为一个或多个完全限定的接口名称,那么将创建一个基于jdk的代理。创建的代理实现了proxyInterfaces属性中指定的所有接口。如果目标类碰巧实现了比proxyInterfaces属性中指定的更多的接口,那也没什么问题,但是那些额外的接口不是由返回的代理实现的。

如果没有设置ProxyFactoryBeanproxyInterfaces属性,但是目标类实现了一个(或多个)接口,那么ProxyFactoryBean会自动检测到目标类确实实现了至少一个接口,并创建一个基于jdk的代理。实际代理的接口是目标类实现的所有接口。实际上,这与将目标类实现的每个接口的列表提供给proxyInterfaces属性相同。然而,这大大减少了工作量,也不容易出现书写错误。

6.4.4 代理接口

考虑一个简单的ProxyFactoryBean操作示例。此示例涉及:

  • 代理的目标bean。这是示例中的personTarget bean定义。
  • 用于提供通知的Advisor和拦截器。
  • 一个用于指定目标对象(personTarget bean)、代理接口和应用通知的AOP代理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属性接受一个字符串列表,其中包含当前工厂中的拦截器或advisors的bean名称。你可以使用advisors、拦截器、前置通知、后置通知和异常通知对象。advisors的顺序很重要。

你可能想知道为什么列表不保存bean引用。这样做的原因是,如果ProxyFactoryBeansingleton属性被设置为false,那么它必须能够返回独立的代理实例。如果任何advisors本身是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。

可以使用前面显示的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代理可以透明地代替真person实现。但是,其类将是动态代理类。可以将其转换为Advised接口(稍后讨论)。

你可以使用匿名内部bean隐藏目标和代理之间的区别。仅ProxyFactoryBean定义不同。该建议仅出于完整性考虑。以下示例显示了如何使用匿名内部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 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>

使用匿名内部bean的优点是只有一个Person类型的对象。如果我们想防止应用程序上下文的用户获得对未通知对象的引用,或者需要避免使用Spring IoC自动装配产生歧义,这是很有用的。可以说,还有一个优点是ProxyFactoryBean定义是独立的。然而,有时候,能够从工厂获得未通知的目标实际上可能是一种优势(例如,在某些测试场景中)。

6.4.5 代理类

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

想象一下,在我们之前的示例中,没有Person接口。我们需要通知一个名为Person的类,该类没有实现任何业务接口。在这种情况下,你可以将Spring配置为使用CGLIB代理而不是动态代理。为此,请将前面显示的ProxyFactoryBeanproxyTargetClass属性设置为true。虽然最好是根据接口而不是类编程,但在处理遗留代码时,通知没有实现接口的类的能力可能很有用。(一般来说,Spring是没有规定性的。虽然它使应用良好实践变得容易,但它避免了强制使用特定的方式或方法。)

如果需要,即使有接口,也可以在任何情况下强制使用CGLIB。

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

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

  • final 的方法不能被通知,因为它们不能被覆盖(备注:子类不能覆盖被final标记方法)。
  • 无需将CGLIB添加到你的类路径中。从Spring 3.2开始,CGLIB被重新打包并包含在spring-coreJAR中。换句话说,基于CGLIB的AOP就像JDK动态代理一样“开箱即用”。

CGLIB代理和动态代理之间几乎没有性能差异。

在这种情况下,性能不应作为决定性的考虑因素。

6.4.6 使用全局Advisors

通过向拦截器名称附加星号,所有具有与星号之前的部分相匹配的bean名称的advisor都会被添加到advisor链中。如果你需要添加一组标准的全局advisor,这将非常有用。以下示例定义了两个全局advisor程序:

<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"/>

参考代码:com.liyong.ioccontainer.starter.ProxyFactoryBeanIocContainer

6.5 简洁的代理定义

特别是在定义事务代理时,你可能会得到许多类似的代理定义。使用父bean和子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:

<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属性设置为true来将父bean定义显式标记为抽象,如前所述,因此实际上可能不会实例化它。默认情况下,应用程序上下文(但不是简单的bean工厂)预实例化所有单例。因此,重要的是(至少对于单例bean),如果你有一个(父)bean定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract属性设置为true。否则,应用程序上下文实际上会尝试对其进行实例化。

6.6 通过ProxyFactory编程式地创建AOP代理

使用Spring以编程方式创建AOP代理很容易。这使你可以使用Spring AOP,而无需依赖Spring IoC。

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

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

第一步是构造一个类型为org.springframework.aop.framework.ProxyFactory的对象。你可以使用目标对象(如前面的示例所示)来创建它,也可以在替代构造函数中指定要代理的接口。

你可以添加通知(拦截器是一种专门的通知)、advisors,或者同时添加它们,并在ProxyFactory的生命周期中操作它们。如果添加了IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。

ProxyFactory上还有一些方便的方法(继承自AdvisedSupport),可以添加其他通知类型,比如beforethrow adviceAdvisedSupportProxyFactoryProxyFactoryBean的超类。

在大多数应用程序中,将AOP代理创建与IoC框架集成在一起是最佳实践。通常,建议你使用AOP从Java代码外部化配置。

6.7 操作通知对象

无论如何创建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、拦截器或其他通知类型返回一个advisor。如果添加了advisor,则此索引处返回的advisor是你添加的对象。如果添加了拦截器或其他通知类型,Spring会将其包装在带有指向总是返回true的切入点的advisor中。因此,如果你添加一个MethodInterceptor,为这个索引返回的advisor是一个DefaultPointcutAdvisor,它返回你的MethodInterceptor和一个匹配所有类和方法的切入点。

addAdvisor()方法可用于添加任何Advisor。通常,持有切入点和通知的advisor是通用的DefaultPointcutAdvisor,你可以将其用于任何通知或切入点(但不用于introduction)。

默认情况下,即使已创建代理,也可以添加或删除advisor或拦截器。唯一的限制是不可能添加或删除一个introduction advisor,因为工厂中的现有代理不会显示接口更改。(你可以从工厂获取新的代理来避免此问题。)

以下示例显示了将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来检查数据库是否被正确更新。)

根据创建代理的方式,通常可以设置冻结标志。在这种情况下,Advised isFrozen()方法返回true,并且任何通过添加或删除来修改通知的尝试都会导致AopConfigException。冻结已通知对象状态的能力在某些情况下非常有用(例如,防止调用代码删除安全拦截器)。

6.8 使用“自动代理”功能

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

Spring还允许我们使用“自动代理” Bean定义,该定义可以自动代理选定的Bean定义。它构建在Spring的bean后处理器基础设施上,该基础设施允许在装载容器时修改任何bean定义。

在这个模型中,你在XML bean定义文件中设置了一些特殊的bean定义来配置自动代理基础设施。这使你可以声明有资格进行自动代理的目标。你无需使用ProxyFactoryBean

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

  • 通过使用在当前上下文中引用特定bean的自动代理创建器。
  • 自动代理创建的一个特殊情况值得单独考虑:由源码级别元数据属性驱动的自动代理创建。
6.8.1 自定代理Bean定义

本节介绍了org.springframework.aop.framework.autoproxy包提供的自动代理创建器。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator类是一个BeanPostProcessor,可以自动为名称与文字值或通配符匹配的bean创建AOP代理。以下示例显示了如何创建BeanNameAutoProxyCreator bean:

<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属性而不是一列拦截器,以允许原型advisors的正确行为。名为“拦截器”的可以是advisors或任何通知类型。

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

名称匹配的Bean定义,例如前面示例中的jdkMyBeanonlyJdk,是带有目标类的普通旧Bean定义。BeanNameAutoProxyCreator自动创建一个AOP代理。相同的通知适用于所有匹配的bean。注意,如果使用了advisors(而不是前面的示例中的拦截器),则切入点可能会不同地应用于不同的bean。

DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator是更通用,功能极其强大的自动代理创建器。这将自动在当前上下文中应用合格的advisor,而不需要在自动代理advisor bean定义中包含特定的bean名称。与BeanNameAutoProxyCreator一样,它具有一致的配置和避免重复的优点。

使用此机制涉及:

  • 指定DefaultAdvisorAutoProxyCreator bean定义。
  • 在相同或关联的上下文中指定任何数量的advisor。请注意,这些必须是advisor,而不是拦截器或其他通知。这是必要的,因为必须有一个要评估的切入点来检查每个通知到候选bean定义的资格。DefaultAdvisorAutoProxyCreator自动评估每个advisor中包含的切入点,以查看应该将什么(如果有的话)通知应用到每个业务对象(例如示例中的businessObject1businessObject2)。

这意味着可以将任意数量的advisor自动应用于每个业务对象。如果任何advisor中没有切入点匹配业务对象中的任何方法,则该对象不会被代理。当为新的业务对象添加Bean定义时,如有必要,它们会自动被代理。

通常,自动代理的优点是使调用者或依赖者无法获得未通知的对象。在此ApplicationContext上调用getBean(“ businessObject1”)会返回AOP代理,而不是目标业务对象。(前面显示的“ inner bean”也提供了这一好处。)

以下示例创建一个DefaultAdvisorAutoProxyCreator bean和本节中讨论的其他元素:

<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支持过滤(通过使用命名约定,只有特定的advisor被评估,这允许在同一个工厂中使用多个不同配置的AdvisorAutoProxyCreators)和排序。Advisor可以实现org.springframework.core.Ordered接口,以确保在出现问题时可以正确排序。前面示例中使用的TransactionAttributeSourceAdvisor具有可配置的顺序值。默认设置为无序。

参考代码:com.liyong.ioccontainer.starter.AdvisorAutoProxyCreatorIocContainer

6.9 使用TargetSource实现

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

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

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

本节的其余部分描述了Spring随附的标准目标源以及如何使用它们。

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

6.9.1 可热交换目标源

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.9.2 池目标源

使用池目标源可以提供类似于无状态会话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接口,该接口通过introduction来公开有关池的配置和当前大小的信息。

你需要定义类似于以下内容的advisor

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

通过在AbstractPoolingTargetSource类上调用便捷方法来获得此advisor,因此可以使用MethodInvokingFactoryBean。该advisor的名称(在此处为poolConfigAdvisor)必须位于暴露池对象的ProxyFactoryBean中的拦截器名称列表中。

转换的定义如下:

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

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

通过使用自动代理,可以实现更简单的池化。你可以设置任何自动代理创建者使用的TargetSource实现。

6.9.3 原型目标源

设置“原型”目标源类似于设置池化TargetSource。在这种情况下,每次方法调用都会创建目标的新实例。尽管在现代JVM中创建新对象的成本并不高,但连接新对象(满足其IoC依赖项)的成本可能会更高。因此,没有充分的理由就不应使用此方法。

为此,你可以修改前面显示的poolTargetSource定义,如下所示(为清楚起见,我们也更改了名称):

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

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

6.9.4 ThreadLocal目标源

如果需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源很有用。ThreadLocal的概念提供了JDK范围的功能,可以透明地将资源与线程一起存储。设置ThreadLocalTargetSource几乎与针对其他类型的目标源所说明的相同,如以下示例所示:

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

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

6.10 定义新通知类型

Spring AOP被设计为可扩展的。虽然目前在内部使用拦截实现策略,但是除了在环绕通知、前置通知、异常通知以及在返回通知进行拦截外,还可以支持任意的通知类型。

适配器包是一个SPI包,它允许在不更改核心框架的情况下添加对新的自定义通知类型的支持。对自定义Advice类型的唯一限制是它必须实现org.aopalliance.aop.Advice标记接口。

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

参考代码:com.liyong.ioccontainer.starter.TargetSourceIocContainer

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

博客地址: http://youngitman.tech

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消