Aspect Oriented Programming with Spring

面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。

OOP中模块化的关键单元是类,而在AOP中模块化是方面。切面使关注点(例如事务管理)的模块化跨越了多个类型和对象。 (这种关注在AOP文献中通常被称为“跨领域”关注。)

Spring的关键组件之一是AOP框架。尽管Spring IoC容器不依赖于AOP,但AOP是对Spring IoC的补充,可以提供功能强大的中间件解决方案。

Spring AOP with AspectJ pointcuts

Spring provides simple and powerful ways of writing custom aspects by using either a schema-based approach or the @AspectJ annotation style. Both of these styles offer fully typed advice and use of the AspectJ pointcut language while still using Spring AOP for weaving.

具有AspectJ切入点的Spring AOP
通过使用基于模式的方法或**@AspectJ注解样式**,Spring提供了编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的建议,并使用了AspectJ切入点语言,同时仍然使用Spring AOP进行编程。

Spring AOP概念

一些重要的AOP概念和术语。这些术语不是特定于Spring的。

  • 切面(Aspect)

    类是对物体特征的抽象,切面就是对横切关注点的抽象

    在Spring AOP中,切面是通过使用常规类(基于架构的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的。

    aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
    AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

    1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
    2. 如何在 advice 中编写切面代码.
  • 连接点(Join point)

    A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

    程序运行中的一些时间点,例如一个方法的执行,或者是一个异常的处理。
    在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.

  • 增强(Advice)

    Action taken by an aspect at a particular join point. Different types of advice include “around”, “before” and “after” advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.

    切面在特定的连接点处采取的操作。不同类型的建议包括aroundbeforeafter通知。 包括Spring在内的许多AOP框架都将通知建模为拦截器,并在连接点周围维护一系列拦截器。

    aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.
    许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.
    例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.

  • 切入点(Pointcut)

    A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.

    匹配连接点的谓词。通知与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。使用切入点表达式来匹配连接点是AOP的核心,并且Spring默认使用AspectJ切入点表达语言。

    Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.

  • 引入(Introduction)

    Declaring additional methods or fields on behalf of a type. Spring AOP lets you introduce new interfaces (and a corresponding implementation) to any advised object. For example, you could use an introduction to make a bean implement an IsModified interface, to simplify caching. (An introduction is known as an inter-type declaration in the AspectJ community.)

    代表类型声明其他方法或字段。 Spring AOP允许您向任何建议的对象引入新的接口(和相应的实现)。例如,您可以使用引入使Bean实现IsModified接口,以简化缓存。 (在AspectJ社区中,引入被称为类型间声明。)

为一个类型添加额外的方法或字段. Spring AOP 允许我们为 目标对象 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现.

  • 目标对象(Target object)

    An object being advised by one or more aspects. Also referred to as the “advised object”. Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object.

    一个或多个切面通知的对象。也称为“目标对象”。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象

    因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)。

    注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类

  • 代理(AOP proxy)

    An object created by the AOP framework in order to implement the aspect contracts (advise method executions and so on). In the Spring Framework, an AOP proxy is a JDK dynamic proxy or a CGLIB proxy.

    一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类。在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象。

  • 织入(Weaving)

    linking aspects with other application types or objects to create an advised object. This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime. Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.

    将切面与其他应用程序类型或对象链接以创建建议的对象(将 aspect 和其他对象连接起来, 并创建 adviced object 的过程)。这可以在编译时(例如,使用AspectJ编译器),加载时或在运行时完成。像其他纯Java AOP框架一样,Spring AOP在运行时执行编织。根据不同的实现技术, AOP织入有三种方式:

    • 编译器织入, 这要求有特殊的Java编译器.
    • 类装载期织入, 这需要有特殊的类装载器.
    • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
      Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

advice的几种类型

  • 前置通知(Before advice)

    在连接点之前运行但无法阻止执行流前进到连接点的通知(除非它引发异常)。

  • 后置通知(After returning advice)

    连接点正常完成后要运行的通知(例如,如果方法返回而没有引发异常)。

  • 抛出异常后通知(After throwing advice)

    如果存在方法则通过抛出异常来执行的通知。

  • 在finally执行后通知(After (finally) advice)

    无论连接点退出的方式如何(正常或异常返回),都将执行通知。

  • 环绕通知(Around advice

    围绕联接点的通知,例如方法调用。这是最有力的通知。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是返回连接点还是通过返回其自身的返回值或引发异常来进行通知的方法执行。

AOP代理

Spring AOP默认将标准JDK动态代理用于AOP代理。这使得可以代理任何接口(或一组接口)。

Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在那些需要建议在接口上未声明的方法或需要将代理对象作为具体类型传递给方法的情况下(在极少数情况下),可以强制使用CGLIB

Spring Bean的生命周期

插播一下Spring Bean的生命周期

两个概念:Spring Bean对象

  1. spring bean——受spring容器管理的对象,可能经过了完整的spring bean生命周期(为什么是可能?难道还有bean是没有经过bean生命周期的?答案是有的,具体我们后面文章分析),最终存在spring容器当中;一个bean一定是个对象
  2. 对象——任何符合java语法规则实例化出来的对象,但是一个对象并不一定是spring bean;

所谓的bean的生命周期就是磁盘上的类通过Spring扫描,然后实例化,跟着初始化,继而放到容器当中的过程。下图展示Spring Bean的生命周期大概有哪些步骤:

Spring Bean的生命周期

其中AOP的代理也是在这个过程中完成的。

AOP的使用

AspectJ与@AspectJ

@AspectJ是一种将切面声明为带有注解的常规Java类的样式。 @AspectJ样式是AspectJ项目在AspectJ 5版本中引入的。 Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

为了方便使用,Spring借鉴了AspectJ的语法。

使用AspectJ编译器和weaver可以使用完整的AspectJ语法。

AspectJ 是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现,也借鉴或采纳了 AspectJ 中很多设计。

启用@AspectJ支持

  1. 通过Java配置启用@AspectJ支持

    在配置类加上@EnableAspectJAutoProxy注解以启用@AspectJ支持

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {

    }
  2. 通过XML配置启用@AspectJ支持

    <aop:aspectj-autoproxy/>

声明一个切面

启用@AspectJ支持后,Spring会自动检测在应用程序上下文中使用@AspectJ切面(具有@Aspect批注)的类定义的bean,并用于配置Spring AOP

  1. 使用xml配置声明切面

    <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
    </bean>
  2. 使用注解声明切面

    package org.xyz;
    import org.aspectj.lang.annotation.Aspect;

    @Aspect
    public class NotVeryUsefulAspect {

    }

声明切入点

切入点确定了关注的的连接点,从而使我们能够控制执行通知的时机。 Spring AOP仅支持Spring Bean的方法执行连接点,可以将切入点视为与Spring Bean上的方法执行匹配。

切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式精确地确定我们关注的方法执行。在AOP@AspectJ批注样式中,常规方法定义提供了切入点签名。 并通过使用@Pointcut注解声明切入点表达式(用作切入点签名的方法必须具有void返回类型)。

一个例子:

@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点方法签名

支持的切入点指示符

Spring AOP支持以下在切入点表达式中使用的AspectJ切入点指示符(PCD):

  • execution:匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指定者。

    描述的最小粒度精确到方法(甚至方法的参数)

  • within:限定匹配特定类型的连接点(在使用SpringAOP的时候,在匹配的类型中定义的方法的执行)。

    描述的最小粒度仅仅到一个类

  • this:限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。(代理的对象本身)

  • target:限定匹配特定的连接点(使用SpringAOP的时候方法的执行),其中目标对象(被代理的appolication object)是指定类型的实例。(被代理的对象)

  • args:限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

  • @target:限定匹配特定的连接点(使用SpringAOP的时候方法的执行),其中执行的对象的类已经有指定类型的注解。

  • @args:限定匹配特定的连接点(使用SpringAOP的时候方法的执行),其中实际传入参数的运行时类型有指定类型的注解。

  • @within:限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

  • @annotation:限定匹配特定的连接点(使用SpringAOP的时候方法的执行),其中连接点的主题有某种给定的注解合并切入点表达式

组合切入点

您可以使用&&||组合切入点表达式和您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} // 1⃣️ 匹配所有公共方法

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} // 2⃣️ 匹配指定包里面的所有方法

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} // 3⃣️ 匹配指定包里面的所有公共方法

共享通用切入点定义

在开发应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议为此定义一个 SystemArchitecture切面,以捕获常见的切入点表达式。这样的方面通常类似于以下示例:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}

/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}

/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}

/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}

/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}

}

可以在需要切入点表达式的任何地方引用切面中定义的切入点。例如,要使服务层具有事务性:

<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

Examples

语法:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
  • 问号表示当前项有也可以没有
  • 其中各项语义如下:
    • modifiers- pattern:方法的可见性,如 public, protected
    • ret-type- pattern:方法的返回值类型,如 int, void 等
    • declaring-type- pattern:方法所在类的全路径名,如 com, spring, Aspect
    • name- pattern:方法名,如 bui sinessservice ()
    • param- pattern:方法的参数类型,如 java. Lang String
    • throws- pattern: 方法抛出的异常类型,如 java.Lang. Exception

一些常见的表达式:

  • 匹配任意public方法

    execution(public * *(..))
  • 匹配所有以set开头的方法

    execution(* set*(..))
  • 匹配AccountService接口定义的任何方法

    execution(* com.xyz.service.AccountService.*(..))
  • 匹配指定包下的方法

    execution(* com.xyz.service.*.*(..))
  • 匹配指定包下面的一个或多个子包下的类方法

    execution(* com.xyz.service..*.*(..))
  • 匹配service包中的所有连接点

    within(com.xyz.service.*)
  • 匹配service一个或多个子包中的所有连接点

    within(com.xyz.service..*)
  • 代理实现AccountService接口的任何连接点

    this(com.xyz.service.AccountService)
  • 目标对象实现AccountService接口的任何连接点

    target(com.xyz.service.AccountService)
  • 任何采用单个参数并且在运行时传递的参数为Serializable的连接点

    args(java.io.Serializable)
  • 目标对象具有@Transactional注解的任何连接点

    @target(org.springframework.transaction.annotation.Transactional)
  • 目标对象的声明类型具有@Transactional注解的任何连接点

    @within(org.springframework.transaction.annotation.Transactional)
  • 任何执行方法带有@Transactional批注的连接点

    @annotation(org.springframework.transaction.annotation.Transactional)
  • 任何采用单个参数的联接点,并且传递的参数的运行时类型具有Classified注解

    @args(com.xyz.security.Classified)
  • 名为tradeServiceSpring bean上的任何连接点

    bean(tradeService)
  • Spring Bean上具有与通配符表达式* Service匹配的名称的任何连接点

    bean(*Service)

声明通知

通知用来声明方法在切入点表达式匹配的方法执行之前,之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

Before Advice

使用@Before注解在切面中声明通知。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}

}

声明通知的同时声明切入点:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}

}

After Returning Advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}

}

有时,您需要在通知正文中访问返回的实际值。您可以使用@AfterReturning的形式绑定返回值以获取该访问权限,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}

}

After Throwing Advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}

}

指定异常类型:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}

}

After (Finally) Advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}

}

Around Advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}

}

引入

引入(Introductions)(在AspectJ中称为类型间声明)使切面可以声明通知对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用@DeclareParents批注进行介绍。此批注用于声明匹配类型具有新的父代(因此而得名)。例如,给定一个名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现,以下方面声明服务接口的所有实现者也都实现了UsageTracked接口(例如,通过JMX公开统计信息):

@Aspect
public class UsageTracking {

@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;

@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}

}

Spring AOP实例

代码地址:https://github.com/cayzlh/cayzlh-demos

总结

  • Spring借鉴了AspectJ的语法
  • Spring通过动态代理来实现aop
  • 对接口创建代理优于对类创建代理,因为会产生更加松耦合的系统,所以spring默认是使用JDK代理。对类代理是让遗留系统或无法实现接口的第三方类库同样可以得到通知,这种方式应该是备用方案
  • 标记为final的方法不能够被通知。spring是为目标类产生子类。任何需要被通知的方法都被复写,将通知织入。final方法是不允许重写的
  • spring只支持方法连接点:不提供属性接入点,spring的观点是属性拦截破坏了封装。面向对象的概念是对象自己处理工作,其他对象只能通过方法调用的得到的结果

spring在运行期,生成动态代理对象,不需要特殊的编译器

Spring AOP 优先对接口进行代理 (使用Jdk动态代理)如果目标对象没有实现任何接口,才会对类进行代理 (使用cglib动态代理)

参考