• 20220507 Core 6. Spring AOP API


    前言

    文档地址

    在本章中,将讨论底层的 Spring AOP API 。对于常见的应用程序,推荐使用带有 AspectJ 切点的 Spring AOP

    Spring 中的切点 Pointcut API

    概念

    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 代理时执行此计算,以避免需要对每个方法调用进行测试。如果两个参数的 matches 方法返回 true ,并且 MethodMatcherisRuntime() 方法返回 true ,则每次方法调用时都会调用三个参数的 matches 方法。这让切点在目标通知开始之前立即查看传递给方法调用的参数。

    如果两个参数的 matches 方法返回 true 并且 isRuntime 方法返回 true ,就会调用三个参数的 matches 方法校验参数

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

    如果可能,尽量使切点静态化,允许 AOP 框架在创建 AOP 代理时缓存切点计算的结果。

    切点操作

    Spring 支持切点上的操作,特别是并集(union)和交集(intersection)

    并集表示任一切点匹配的方法。交集意味着两个切点都匹配的方法。并集通常更有用。

    可以通过使用 org.springframework.aop.support.Pointcuts 类中的静态方法或使用相同包中的 ComposablePointcut 类来组合切点 。然而,使用 AspectJ 切点表达式通常是一种更简单的方法。

    AspectJ 表达式切点

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

    便利的切点实现

    静态切点

    静态切点基于方法和目标类,不考虑方法的参数。

    对于大多数用途,静态切点就足够了。当方法第一次被调用时,Spring 计算一次静态切点。之后,无需在每次方法调用时再次评估切点。

    正则表达式切点

    org.springframework.aop.support.JdkRegexpMethodPointcut 是一个通用的正则表达式切点,它使用 JDK 中的正则表达式提供支持。

    <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 可以是拦截器、before 通知、throws 通知等)。在底层,Spring 使用 JdkRegexpMethodPointcutRegexpMethodPointcutAdvisor 简化了装配,一个 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>
    
    属性驱动的切点

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

    动态切点

    动态切点比静态切点的计算成本更高。它考虑了方法参数以及静态信息。

    这意味着必须在每次方法调用时进行计算,并且结果不能被缓存,因为参数会有所不同。

    控制流切点

    Spring 控制流切点在概念上类似于 AspectJ cflow 切点,但功能较弱。(目前没有办法指定一个切点在另一个切点匹配的连接点之下运行)

    控制流切点匹配当前调用堆栈。例如,如果连接点被 com.mycompany.web 包中的方法或 SomeCaller 类调用,它可能会触发。控制流切点是通过使用 org.springframework.aop.support.ControlFlowPointcut 类来指定的。

    切点超类

    Spring 提供了有用的切点超类来帮助实现自定义切点。

    继承 StaticMethodMatcherPointcut 可以自定义静态切点。这只需要实现一个抽象方法。示例如下:

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

    自定义切点

    可以声明自定义切点,无论是静态的还是动态的。Spring 中的自定义切点可以是任意复杂的。但是,建议使用 AspectJ 切点表达式语言。

    Spring 中的通知 Advice API

    img

    通知生命周期

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

    最常用的是 per-class 通知。它适用于通用通知,例如事务顾问。这些不依赖于代理对象的状态或添加新状态。它们仅对方法和参数起作用。

    per-instance 的通知适用于引入,以支持混合( mixin )。这种情况下,建议将状态添加到代理对象。

    可以在同一个 AOP 代理中混合使用共享和 per-instance 的通知。

    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 联盟的 AOP 实现的互操作性。本节剩余部分讨论的其他通知类型实现了常见的 AOP 概念,但以特定于 Spring 的方式实现。

    虽然使用最具体的通知类型有优势,但如果您可能想在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor 环绕通知。注意,切点目前无法在框架之间互操作,并且 AOP 联盟目前没有定义切点接口。

    前置通知

    更简单的通知类型是前置通知。它不需要 MethodInvocation 对象,因为它只在进入方法之前被调用。

    前置通知的主要优点是不需要调用 proceed() 方法,因此不存在无法沿拦截器链继续下去的可能性。

    MethodBeforeAdvice 接口:

    public interface MethodBeforeAdvice extends BeforeAdvice {
        void before(Method m, Object[] args, Object target) throws Throwable;
    }
    

    注意,返回类型是 void 。前置通知可以在连接点运行之前插入自定义行为,但不能更改返回值。

    如果前置通知抛出异常,它会停止拦截器链的进一步执行。异常沿拦截器链向上传播。如果是非检查异常或在调用方法的签名上,则将其直接传递给客户端。否则,它会被 AOP 代理包裹在一个非检查异常中。

    示例:

    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)
    

    方法签名可能有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。

    单参数示例:

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

    四参数示例:

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

    如果异常通知方法本身抛出异常,它会覆盖原始异常(即,它更改抛出给用户的异常)。覆盖异常通常是 RuntimeException ,它与任何方法签名兼容。

    但是,如果异常通知方法抛出检查异常,则它必须与目标方法的声明异常相匹配,因此在某种程度上与特定的目标方法签名耦合。*不要抛出与目标方法的签名不兼容的未声明的检查异常!***

    异常通知可以与任何切点一起使用。

    后置返回通知

    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

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

    引入通知不能与任何切点一起使用,因为它仅适用于类,而不是方法级别。只能将引入通知与 IntroductionAdvisor 一起使用

    public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
    
        ClassFilter getClassFilter();
    
        void validateInterfaces() throws IllegalArgumentException;
    }
    
    public interface IntroductionInfo {
    
        Class<?>[] getInterfaces();
    }
    

    没有 MethodMatcher ,因此,没有 Pointcut 与引入通知相关联。只有类过滤。

    • getInterfaces() 方法返回此顾问引入的接口。
    • validateInterfaces() 方法用于内部查看引入的接口是否可以由配置的 IntroductionInterceptor 实现
    使用示例

    向一个或多个对象引入以下接口:

    public interface Lockable {
        void lock();
        void unlock();
        boolean locked();
    }
    
    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);
        }
    
    }
    
    public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
    
        public LockMixinAdvisor() {
            super(new LockMixin(), Lockable.class);
        }
    }
    

    我们可以通过使用 XML 配置中的方法或 Advised.addAdvisor()(推荐)以编程方式应用此顾问,就像任何其他顾问一样。

    Spring 中的 Advisor API

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

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

    在 Spring 中可以在同一个 AOP 代理中混合顾问和通知类型。Spring 会自动创建必要的拦截器链。

    使用 ProxyFactoryBean 来创建 AOP 代理

    工厂 bean 引入了一个间接层,让它创建不同类型的对象

    Spring AOP 在底层使用工厂 bean。

    在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean 。 这可以完全控制切点、任何适用的通知及其顺序。

    基础

    ProxyFactoryBean 就像其它 FactoryBean 实现一样,引入了一个间接层。如果定义了一个名为 fooProxyFactoryBean ,引用 foo 的对象不会看到 ProxyFactoryBean 实例本身,而是看到 ProxyFactoryBeangetObject() 方法实现创建的对象。此方法创建一个包装目标对象的 AOP 代理。

    使用 ProxyFactoryBean 或 IoC 感知类来创建 AOP 代理的好处之一是通知和切点也可以由 IoC 管理。

    JavaBean 属性

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

    • 指定要代理的目标
    • 指定是否使用 CGLIB

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

    • proxyTargetClass :如果要代理目标类,而不是目标类的接口,则为 true。如果设置为 true ,则会创建 CGLIB 代理
    • optimize :控制是否对通过 CGLIB 创建的代理应用积极优化。除非您完全了解相关 AOP 代理如何处理优化,否则不应随意使用此设置。这目前仅用于 CGLIB 代理,对 JDK 动态代理没有影响。
    • frozen :如果代理配置是 frozen ,则不再允许更改配置。这对于轻微的优化以及在创建代理后不希望调用者能够(通过 Advised 接口)操作代理的情况都很有用。此属性的默认值为 false ,因此允许更改(例如添加额外的通知)
    • exposeProxy :确定当前代理是否应该在 ThreadLocal 中公开, 以便可以访问它。如果需要获取代理并且 exposeProxy 属性设置为 true ,则可以使用 AopContext.currentProxy() 方法

    其他特定于 ProxyFactoryBean 的属性:

    • proxyInterfaces :接口名称 String 数组。如果未提供,则使用目标类的 CGLIB 代理

    • interceptorNames :要应用的 Advisor 、拦截器或其他通知名称的 String 数组。顺序很重要,先入先出,也就是说列表中的第一个拦截器是第一个被拦截调用的。

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

      可以对拦截器名称使用星号 ( * )。这样做会导致应用名称以要应用的星号之前的部分开头的所有顾问 bean。您可以在使用“全局”顾问中找到使用此功能的示例。

    • singleton :无论 getObject() 方法被调用的频率如何,工厂是否应该返回单个对象。一些 FactoryBean 实现提供了这样的方法。默认值为 true 。如果你想使用有状态的通知,设置为 false ——例如,对于有状态的 mixin——使用原型通知。

    基于 JDK 和 CGLIB 的代理

    本节作为关于 ProxyFactoryBean 如何选择为特定目标对象创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。

    如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则创建基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,没有接口意味着不可能进行 JDK 代理。可以通过设置 interceptorNames 属性来指定拦截器列表。请注意,即使 ProxyFactoryBeanproxyTargetClass 属性已设置为 false ,也会创建基于 CGLIB 的代理。

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

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

    如果 ProxyFactoryBeanproxyInterfaces 属性已设置为一个或多个完全限定的接口名称,则会创建一个基于 JDK 的代理。创建的代理实现了 proxyInterfaces 属性中指定的所有接口。如果目标类碰巧实现了比 proxyInterfaces 属性中指定的接口多得多的接口,额外的接口不会由返回的代理实现。

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

    参考源码
    • org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
    • org.springframework.aop.framework.ProxyFactoryBean

    代理接口

    示例:

    <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 名称。可以使用顾问、拦截器、前置、后置和异常通知对象。顾问的排序很重要。

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

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

    同一个 IoC 上下文中的其他 bean 可以表达对它的强类型依赖,就像普通 Java 对象一样。以下示例显示了如何执行此操作:

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

    可以通过使用匿名内部 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 定义是自包含的。但是,有时能够从工厂获得未通知的目标实际上可能是一种优势(例如,在某些测试场景中)。

    代理类

    ProxyFactoryBeanproxyTargetClass 属性设置为 true

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

    CGLIB 代理一般应该对用户透明。但是,有一些问题:

    • 不能通知 final 方法,因为它们不能被覆盖
    • 无需将 CGLIB 添加到您的类路径中。从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR 中

    CGLIB 代理和动态代理之间的性能差异很小。在这种情况下,性能不应成为决定性的考虑因素。

    使用全局顾问

    通过在拦截器名称后附加星号,所有具有与星号之前部分匹配的 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"/>
    

    简洁的代理定义

    通过使用 xml 配置里的模板配置,设置 abstract="true"

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

    ProxyFactory 以编程方式创建 AOP 代理

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

    目标对象实现的接口会自动代理。以下清单显示了为目标对象创建代理,具有一个拦截器和一个顾问:

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

    在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成是最佳实践。我们建议您使用 AOP 从 Java 代码外部进行配置。

    操作被通知对象

    创建 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 ,则此索引处返回的顾问就是您添加的对象。如果您添加了拦截器或其他通知类型,Spring 会将其包装在一个带有始终返回 true 的切点的顾问中。因此,如果您添加了 MethodInterceptor ,则为此索引返回的顾问是 DefaultPointcutAdvisor ,它返回您的 MethodInterceptor 和匹配所有类和方法的切点。

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

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

    使用“自动代理”功能

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

    您需要在 XML bean 定义文件中设置了一些特殊的 bean 定义以配置自动代理基础结构。这使您可以声明符合自动代理条件的目标。不需要使用 ProxyFactoryBean

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

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

    参考源码

    • org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessBeforeInstantiation

    自动代理 Bean 定义

    BeanNameAutoProxyCreator

    BeanNameAutoProxyCreator 类是自动为名称与文本值或通配符匹配的 bean 创建 AOP 代理的 BeanPostProcessor 类。以下示例显示了如何创建 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 属性而不是拦截器列表,以允许原型顾问的正确行为。“拦截器” 可以是顾问或任何通知类型。

    与一般的自动代理一样,使用 BeanNameAutoProxyCreator 的主要目的是将相同的配置一致地应用于多个对象。它是将声明式事务应用于多个对象的流行选择。

    名称匹配的 bean 定义(如上例中的 jdkMyBeanonlyJdk )是带有目标类的普通 bean 定义。AOP 代理由 BeanNameAutoProxyCreator 自动创建。 相同的通知适用于所有匹配的 bean 。请注意,如果使用了顾问(而不是拦截器),切点可能会以不同的方式应用于不同的 bean 。

    DefaultAdvisorAutoProxyCreator

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

    使用这种机制包括:

    • 指定 DefaultAdvisorAutoProxyCreator bean 定义
    • 在相同或相关的上下文中指定任意数量的顾问。注意,必须是顾问,而不是拦截器或其他通知。这是必要的,因为必须有一个切点来评估,以检查每个通知使用应该用于候选 bean 定义

    DefaultAdvisorAutoProxyCreator 自动计算包括在每个顾问中的切点,看看有什么通知,应该适用于每个业务对象

    自动代理通常具有使调用者或依赖项无法获得原始对象的优势。调用 ApplicationContextgetBean("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 支持过滤(通过使用一种命名约定,使得只有特定的顾问进行评估,其允许使用多个不同配置,在同一个工厂的 AdvisorAutoProxyCreators )和顺序。顾问可以实现 org.springframework.core.Ordered 接口以确保正确排序。在前例中使用 TransactionAttributeSourceAdvisor 具有可配置的顺序值。默认设置是无序的。

    使用 TargetSource 实现

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

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

    如果未指定 TargetSource ,则使用默认实现来包装本地对象。每次调用都会返回相同的目标。

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

    热插拔目标源

    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 可以与任意通知结合使用。

    池化目标源

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

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

    Spring 提供了对 Commons Pool 2.2 的支持,它提供了一个相当高效的池化实现。需要应用程序类路径上存在 commons-pool jar 才能使用此功能。您还可以创建 org.springframework.aop.target.AbstractPoolingTargetSource 子类以支持任何其他池化 API。

    示例配置:

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

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

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

    原型目标源

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

    为此,您可以修改前面的 poolTargetSource 定义,如下所示:

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

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

    ThreadLocal 目标源

    如果您需要为每个传入请求(即每个线程)创建一个对象,则 ThreadLocal 目标源非常有用。

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

    定义新的通知类型

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

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

  • 相关阅读:
    嵌入式系统之微处理器篇
    嵌入式系统之基础概念篇
    八大排序算法简述
    进程-PV操作
    实时操作系统与分时操作系统
    串口助手
    STM32通用定时器功能和用法
    三种主流芯片架构简单比较
    python 我的第一个自动化脚本
    jquery部分实用功能
  • 原文地址:https://www.cnblogs.com/huangwenjie/p/16361195.html
Copyright © 2020-2023  润新知