Spring-3-AOP和切入点

Spring框架提供的另一个核心功能是支持面向方面的编程(AOP)。

AOP通常被称为实施横切关注点的工具。

术语横切关注点(crosscutting concerns)是指应用程序中无法从应用程序的其余部分分解并且可能导致代码重复和紧密耦合的逻辑。

通过使用AOP模块化各个逻辑部分(称为关注点(concerns)),可以将它们应用于应用程序的多个部分,而无须复制代码或创建硬性依赖关系。

AOP概念

  • 连接点:连接点是应用程序执行期间明确定义的一个点。连接点的典型示例包括方法调用、方法调用本身、类初始化和对象实例化。连接点是AOP的核心概念,并且定义了在应用程序中可以使用AOP插入其他逻辑的点。
  • 通知:在特定连接点执行的代码就是通知,它是由类中的方法定义的。有许多类型的通知,比如前置通知(在连接点之前执行)和后置通知(在连接点之后执行)。
  • 切入点:切入点是用于定义何时执行通知的连接点集合。通过创建切入点,可以更细致地控制如何将通知应用于应用程序中的组件。如前所述,典型的连接点是方法调用,或是特定类中所有方法调用的集合。通常情况下,可以在复杂的关系中插入切入点,从而进一步限制执行通知的时间。
  • 切面:切面是封装在类中的通知和切入点的组合。这种组合定义了应该包含在应用程序中的逻辑及其应该执行的位置。
  • 织入:织入是在适当的位置将切面插入到应用程序代码中的过程。对于编译时AOP解决方案,织入过程通常在生成时完成。同样,对于运行时AOP解决方案,织入过程在运行时动态执行。此外,AspectJ还支持另一种称为加载时织入(LTW)的织入机制,在该机制中,拦截底层的JVM类加载器,并在类加载器加载字节码时向其提供织入功能。
  • 目标对象:执行流由AOP进程修改的对象被称为目标对象。通常也会看到目标对象被称为被通知(advised)对象。
  • 引入:这是通过引入其他方法或字段来修改对象结构的过程。可以通过引入AOP来使任何对象实现特定的接口,而无须对象的类显式地实现该接口。

AOP的类型

AOP有两种不同的类型:静态AOP和动态AOP。它们之间的区别就在于织入过程发生的地点以及如何实现这一过程。

使用静态AOP

在静态AOP中,织入过程构成了应用程序生成过程中的另一个步骤。

用Java术语来说,可以通过修改应用程序的实际字节码并根据需要更改和扩展应用程序代码来实现静态AOP实现中的织入过程。

这是实现织入过程比较好的方法,因为最终结果只是Java字节码,并且在运行时无须使用任何特殊技巧来确定应该何时执行通知。

但这种机制的缺点是,对切面所做的任何修改都要求重新编译整个应用程序,即使只是想添加另一个连接点。AspectJ的编译时织入是静态AOP实现的一个很好的例子。

使用动态AOP

动态AOP实现(比如Spring AOP)与静态AOP实现不同,因为织入过程在运行时动态执行。

如何实现依赖于具体实现,但正如你所看到的,Spring采用的方法是为所有被通知对象创建代理,以便根据需要调用通知。

动态AOP的缺点是,一般来说性能不如静态AOP,但目前性能在稳步提高。

动态AOP实现的主要优点是可以轻松修改应用程序的整个切面集,而无须重新编译主应用程序代码。

Spring AOP

SpringAOP的核心架构基于代理。当想要创建一个类的被通知实例时,必须使用ProxyFactory创建该类的代理实例,首先向ProxyFactory提供想要织入到代理的所有切面。

可以依赖Spring所提供的声明式AOP配置机制(ProxyFactoryBean类、aop名称空间和@AspectJ样式注解)来完成声明式代理的创建。但是,了解代理创建的工作过程是非常重要的,因此首先演示代理创建的编程方法,然后深入讨论Spring的声明式AOP配置。

在运行时,Spring会分析为ApplicationContext中的bean定义的横切关注点,并动态生成代理bean(封装了底层目标bean)。此时,不会直接调用目标bean,而是将调用者注入代理bean。然后,代理bean分析运行条件(即,连接点、切入点或通知)并相应地织入适当的通知。

下图显示了运行中的SpringAOP代理的高级视图。Spring有两个代理实现:JDK动态代理和CGLIB代理。

在这里插入图片描述
代理bean(由Spring在默认情况下,当被通知的目标对象实现一个接口时,Spring将使用JDK动态代理来创建目标的代理实例。

但是,当被通知目标对象没有实现接口(例如,它是一个具体类)时,将使用CGLIB来创建代理实例。一个主要原因是JDK动态代理仅支持接口代理。

Spring 中创建通知

在这里插入图片描述

通知的接口

在这里插入图片描述

创建前置通知

前置通知Before advice是Spring中最有用的通知类型之一。

该通知可以修改传递给方法的参数,并可以通过抛出异常来阻止方法的执行。

在本节中,将演示两个使用前置通知的简单示例:一个在方法执行前将包含方法名称的消息写入控制台输出,另一个则可用于限制对对象方法的访问。

在下面的代码片段中,可以看到MethodBeforeAdvice类的代码。

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

before()方法被传入三个参数:要调用的方法、将传递给该方法的参数以及作为调用目标的Object。

在这里插入图片描述

创建后置返回通知

后置返回通知在连接点返回方法调用后执行。由于方法已经执行,因此不能更改传递给方法的参数。

虽然可以读取这些参数,但不能更改执行路径,也不能阻止该方法执行。这些限制都在预料之中;但是,没有预料到的是,无法在后置返回通知中修改返回值。

当使用后置返回通知时,只能添加处理。尽管后置返回通知不能修改方法调用的返回值,但却可以抛出可以发送到堆栈的异常(而不是返回值)。

在这里插入图片描述
AfterReturningAdvice接口声明了一个方法afterReturning(),为它传入方法调用的返回值、对所调用方法的引用、传递给方法的参数以及调用的目标。

创建环绕通知

环绕通知(around advice)功能类似于前置通知和后置通知功能的组合,但存在一个很大的区别:可以修改返回值。不仅如此,还可以阻止方法执行。这意味着通过使用环绕通知,基本上可以用新代码替换整个方法的实现。

通过使用Methodlnterceptor接口,可以将Spring中的环绕通知模型化为拦截器。环绕通知有许多用途,你会发现Spring的许多功能都是使用方法拦截器创建的,例如远程代理支持和事务管理功能。方法拦截还是分析应用程序执行的一种很好的机制,它构成了本节中示例的基础。

在前面的示例中,可以看到,MethodInterceptor接口的invoke()方法没有提供与MethodBeforeAdvice和AfterReturningAdvice相同的参数集合。

该方法未被传入调用的目标、方法或所使用的参数。

但是,通过使用传递给invoke()的Methodlnvocation对象可以访问此类数据。

具体而言,想知道该方法执行多长时间。为了达到这个目的,可以使用Spring中包含的StopWatch类,并且需要一个Methodlnterceptor,因为需要在方法调用之前启动StopWatch并在调用之后停止它。
在这里插入图片描述

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
    
    
        // 创建一个StopWatch对象并开始计时
        StopWatch sw = new StopWatch();
        sw.start(invocation.getMethod().getName());

        // 调用原始方法并获取返回值
        Object returnValue = invocation.proceed();

        // 停止计时
        sw.stop();

        // 打印调用信息和总耗时
        dumpInfo(invocation, sw.getTotalTimeMillis());

        // 返回原始方法的返回值
        return returnValue;
    }

创建异常通知

异常通知类似于后置返回通知,因为它也在连接点之后执行,也是一个方法调用,但只有当方法抛出异常时异通知才会执行。

此外,异常通知与后置返回通知还有一点相似,因为它几乎不能控制程序的执行。如果使用了异常通知,则不能选择忽略引发的异常而返回方法的值。

可以对程序流进行的唯一修改是更改抛出的异常类型。

这是一个相当强大的概念,可以使应用程序开发更简单。考虑一种情况,假设有一个API抛出了一组定义不明确的异常。

通过使用异常通知,可以通知该API中的所有类,并对异常层次结构重新进行分类,使其更易于管理和描述。

当然,也可以使用异常通知在整个应用程序中提供集中式错误日志记录,从而减少散布在应用程序中的错误日志代码数量。

ThrowsAdvice接口实现了异常通知。与你在前面看到的接口不同,ThrowsAdvice没有定义任何方法;相反,它只是Spring使用的标记接口。

其原因是Spring允许类型化的异常通知,从而能够准确地定义异常通知应该捕获哪些异常类型。Spring通过使用反射来检测具有特定签名的方法,从而实现了上述功能。

在Spring中使用顾问和切入点

到目前为止,你所看到的所有示例都使用了ProxyFactory类。该类提供了一种简单的方法来获取和配置自定义用户代码中的AOP代理实例。

ProxyFactoryaddAdvice()方法用于配置代理的通知。此方法在后台委托给addAdvisor(),创建DefaultPointcutAdvisor的实例并使用指向所有方法的切入点对其进行配置。

通过这种方式,通知被视为适用于目标上的所有方法。在某些情况下,例如当使用AOP进行记录时,可能需要这么做;但在更多情况下,可能需要限制通知适用的方法。

当然,可以简单地在通知中检查被通知的方法是否正确,但这种方法有几个缺点。

首先,将可接受的方法列表硬编码到通知中会降低通知的可重用性。通过使用切入点,可以配置通知适用的方法,而无须将相关代码放入通知中;这明显提高了通知的可重用性。

将方法列表硬编码到通知中还会对应用程序性能产生影响。

为了检查通知中被通知的方法,每次调用目标上的任何方法时都需要执行检查。

这明显降低了应用程序的性能。当使用切入点时,对每种方法执行一次检查,并将结果缓存起来供以后使用。

不使用切入点来限制通知适用的方法所带来的另一个与性能相关的缺点是,Spring可以在创建代理时对未通知方法进行优化,从而加快对未通知方法的调用。

强烈建议避免将硬编码的方法检查放入通知,而是尽可能使用切入点来控制通知对目标上方法的适用性。

当通知具有很少或没有目标关联性时,应该使用切入点。也就是说,通知可以适用于任何类型或范围广泛的类型。

当通知具有较强的目标关联性时,应该在通知内检查通知是否被正确使用;这有助于在通知被误用时减少出现令人头痛的错误。此外,还建议避免不必要地通知方法。正如稍后将看到的,这样做会导致调用速度明显下降,从而可能对应用程序的整体性能产生巨大影响。

Pointcut接口

Spring中的切入点通过实现Pointcut接口来创建。

public interface Pointcut {
    
    
    Pointcut TRUE = TruePointcut.INSTANCE;

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}

Spring提供了一些可供选择的Pointcut实现,它们覆盖了大部分(但不是全部)用例。

Spring首先使用PointcutgetClassFilter()返回的ClassFilter实例检查Pointcut接口是否适用于该方法的类。

public interface ClassFilter {
    
    
    ClassFilter TRUE = TrueClassFilter.INSTANCE;

    boolean matches(Class<?> clazz);
}

ClassFilter接口定义了单个方法matches(),并传入一个Class实例来表示需要检查的类。

毫无疑问,如果切入点适用于类,matches()方法返回true,否则返回false。

MethodMatcher接口比ClassFilter接口更复杂。

public interface MethodMatcher {
    
    
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

    boolean matches(Method method, Class<?> targetClass);

    boolean isRuntime();

    boolean matches(Method method, Class<?> targetClass, Object... args);
}

spring支持两种类型的MethodMatcher—静态和动态MethodMatcher,具体是哪种类型由isRuntime()的返回值决定。

在使用MethodMatcher之前,Spring调用isRuntime()来确定MethodMatcher是静态的还是动态的,返回值为false表示是静态的,返回值为true表示是动态的。

对于静态切入点,Spring会针对目标上的每个方法调用一次MethodMatcher的matches(Method,Class)方法,并缓存返回值,以便这些方法在后续调用。

这样一来,只会对每个方法进行一次方法适用性检查,方法的后续调用将不会再调用matches()。

即使使用动态切入点,在第一次调用方法来确定方法的整体适用性时,Spring也仍然通过使用matches(Method,Class)执行静态检查。

如果静态检查返回true,那么 Spring将使用matches(Method,Class,Object[])方法对每个方法的调用执行进一步检查。

这样一来,动态MethodMatcher可以根据特定的方法调用(而不仅仅是方法本身)来确定切入点是否应该应用。

例如,只有当参数是一个值大于100的Integer时才需要应用切入点。

在这种情况下,可以编写matches(Method,Class,Object[])方法来对每个调用的参数进行进一步检查。

显然,静态切入点比动态切入点执行得更好,因为它们避免了每次调用时所要进行的额外检查;而动态切入点在决定是否应用通知方面提供了更大的灵活性。

一般来说,建议尽可能使用静态切入点。但是,如果所使用的通知增加了大量开销,那么通过使用动态切入点来避免任何不必要的通知调用可能是比较明智的做法。

可用的切入点实现

从版本4.0开始,Spring提供了八个Pointcut接口的实现:两个用作创建静态和动态切入点的便捷类的抽象类,以及六个具体类,其中每一个具体类完成以下操作。

  • 一起构成多个切入点
  • 处理控制流切入点
  • 执行简单的基于名称的匹配
  • 使用正则表达式定义切入点
  • 使用AspectJ表达式定义切入点
  • 定义在类或方法级别查找特定注解的切入点
    在这里插入图片描述

使用DefaultPointcutAdvisor

DefaultPointcutAdvisor,这是一个简单的PointcutAdvisor,用于将单个Pointcut与单个Advice相关联。

public abstract class StaticMethodMatcher implements MethodMatcher {
    
    
    /**
     * 父类为空的抽象静态方法匹配器
     */
    public StaticMethodMatcher() {
    
    
    }

    /**
     * 判断是否为运行时方法
     *
     * @return 如果是静态方法返回false,如果是运行时方法返回true
     */
    public final boolean isRuntime() {
    
    
        return false;
    }

    /**
     * 判断给定的方法是否与给定的类的匹配
     *
     * @param method  待匹配的方法
     * @param targetClass  目标类
     * @param args  方法参数
     * @return  如果匹配返回true,否则返回false
     */
    public final boolean matches(Method method, Class<?> targetClass, Object... args) {
    
    
        throw new UnsupportedOperationException("Illegal MethodMatcher usage");
    }
}

        // 创建一个GoodGuitarist对象johnMayer
        GoodGuitarist johnMayer = new GoodGuitarist();

        // 创建一个GreatGuitarist对象ericClapton
        GreatGuitarist ericClapton = new GreatGuitarist();

        // 创建两个 Singer类型的变量proxyOne和proxyTwo
        Singer proxyOne;
        Singer proxyTwo;

        // 创建一个Pointcut对象pc
        Pointcut pc = new SimpleStaticPointcut();

        // 创建一个Advice对象advice
        Advice advice = new SimpleAdvice();

        // 创建一个Advisor对象advisor
        Advisor advisor = new DefaultPointcutAdvisor(pc, advice);

        // 创建一个ProxyFactory对象pf
        ProxyFactory pf = new ProxyFactory();

        // 在pf中添加advisor
        pf.addAdvisor(advisor);

        // 设置pf的目标对象为johnMayer
        pf.setTarget(johnMayer);
        
        // 获取proxyOne的代理对象
        proxyOne = (Singer) pf.getProxy();

        // 重新创建一个ProxyFactory对象pf
        pf = new ProxyFactory();

        // 在pf中添加advisor
        pf.addAdvisor(advisor);

        // 设置pf的目标对象为ericClapton
        pf.setTarget(ericClapton);

        // 获取proxyTwo的代理对象
        proxyTwo = (Singer) pf.getProxy();

        // 调用proxyOne的sing方法
        proxyOne.sing();

        // 调用proxyTwo的sing方法
        proxyTwo.sing();

使用DyanmicMethodMatcherPointcut创建动态切入点

创建动态切入点与创建静态切入点没有多大区别。

但与前面示例不同的是,只有在传入的int参数大于或小于100时才通知该方法。与静态切入点一样,Spring为创建动态切入点提供了一个方便的基类:DynamicMethodMatcherPointcut。
在这里插入图片描述

使用简单名称匹配

通常,在创建切入点时,希望仅根据方法名称进行匹配,而忽略方法签名和返回类型。

在这种情况下,应该避免创建StaticMethodMatcherPointcut的子类,并使用NameMatchMethodPointcut(它是StaticMethodMatcherPointcut的子类)与方法名称列表进行匹配。

当使用NameMatchMethodPointcut时,无须考虑方法签名,所以对于方法sing()和sing(guitar)来说,它们都会与名称foo相匹配。

在这里插入图片描述

用正则表达式创建切入点

在上一节中,讨论了如何针对预定义的方法列表执行简单匹配。但是如果事先不知道所有方法的名称,而是只知道名称所遵循的模式,又该怎么做呢?

例如,如果想匹配名称以get开头的所有方法,该怎么办?在这种情况下,可以使用正则表达式切入点JdkRegexpMethodPointcut 来匹配基于正则表达式的方法名称。

在这里插入图片描述

使用AspectJ切入点表达式创建切入点

除了JDK正则表达式,还可以使用AspectJ的切入点表达式语言进行切入点声明。

在本章的后面你将会看到,当使用aop名称空间在XML配置中声明切入点时,Spring默认使用AspectJ的切入点语言。而且当使用Spring的@AspectJ注解类型的AOP支持时,也需要使用AspectJ的切入点语言。

所以当想要使用表达式语言来声明切入点时,使用AspectJ切入点表达式是最好的方式。

Spring提供了AspectJExpressionPointcut类来通过AspectJ的表达式语言定义切入点。

在这里插入图片描述

创建注解匹配切入点

如果应用程序基于注解,那么可能希望使用自己指定的注解来定义切入点,也就是将通知逻辑应用于所有具有特定注解的方法或类型。

Spring提供了AnnotationMatchingPointcut类来定义使用注解的切入点。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zhuyufan1986/article/details/135232895