AOP简介及Spring AOP的简单注解实现


AOP简介及Spring AOP的简单注解实现

一、什么是 AOP

AOP(Aspect Oriented Programming),即面向切面编程,可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过 OOP 允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP 技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用”横切”技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

二、AOP 核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

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

3、连接点(joinpoint)

被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

4、切入点(pointcut)

对连接点进行拦截的定义

5、通知(advice)

又称“增强”,指的就是拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

三、Spring 对 AOP 的支持

Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成、管理,其依赖关系也由 IOC 容器负责管理。因此,AOP 代理可以直接使用容器中的其它 bean 实例作为目标,这种关系可由 IOC 容器的依赖注入提供。Spring 创建代理的规则为:

  1. 默认使用 Java 动态代理来创建 AOP 代理,这样就可以为任何接口实例创建代理了
  2. 当需要代理的类不是代理接口的时候,Spring 会切换为使用 CGLIB 代理,也可强制使用 CGLIB

Spring 中 AOP 编程其实是很简单的事情,纵观 AOP 编程,程序员只需要参与三个部分:

  1. 定义普通业务组件
  2. 定义切入点,一个切入点可能横切多个业务组件
  3. 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作

所以进行 AOP 编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP 框架将自动生成 AOP 代理,即:代理对象的方法=增强处理+被代理对象的方法。

Spring2.0 之前版本是使用底层的 Spring AOP api 提供 AOP 支持。Spring 2.0 引入了基于 schema-based 和@AspectJ 注解方式编写自定义切面的更简单和更强大的 AOP 支持。这两种方式都提供了完全类型化的建议并使用 AspectJ 切入点语言,同时仍然使用 Spring AOP 进行织入(weave)。 建议使用后一种方式(官方)。

正如 Spring 官方说法:

Spring AOP will never strive to compete with AspectJ to provide a comprehensive AOP solution. We believe that both proxy-based frameworks like Spring AOP and full-blown frameworks such as AspectJ are valuable, and that they are complementary, rather than in competition. Spring 2.0 seamlessly integrates Spring AOP and IoC with AspectJ, to enable all uses of AOP to be catered for within a consistent Spring-based application architecture. This integration does not affect the Spring AOP API or the AOP Alliance API: Spring AOP remains backward-compatible. See the following chapter for a discussion of the Spring AOP APIs.

Spring AOP

大意是:Spring AOP 不会与 AspectJ 竞争来提供全面的 AOP 解决方案。基于代理的框架(如 Spring AOP)和成熟的框架(如 AspectJ)都是有价值的,它们是互补的,而不是竞争的。Spring 2.0 无缝地将 Spring AOP 和 IoC 与 AspectJ 集成在一起,使 AOP 的所有使用都能在一致的基于 Spring 的应用程序体系结构中得到满足。这种集成并不影响 Spring AOP API 或 AOP Alliance API: Spring AOP 仍然向后兼容。

四、Spring AOP 注解方式简单实现

@AspectJ 是一种使用 Java 注解来实现 AOP 的编码风格.
@AspectJ 风格的 AOP 是 AspectJ 框架在 AspectJ 5 中引入的, Spring 从 Spring2.0 起也支持 @AspectJ 的 AOP 风格.

下面讨论基于@AspectJ 通过注解实现简单 AOP 功能。

1、引入依赖

在 Spring 中使用基于@AspectJ 的 AOP,除了 Spring 核心 jar 依赖外,还需要两个 AspectJ 库,aspectjweaver.jar 和 aspectjrt.jar

Maven 项目引入:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
</dependency>

Gradle 项目引入:

compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.2'
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.2'

2、启用@AspectJ 支持

@AspectJ 支持启用可以通过 XML 和注解方式,

(1)XML 方式

通过在 spring 配置文件中配置<aop:aspectj-autoproxy>来启用的(同时要导入 AOP 命名空间):

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns="http://www.springframework.org/schema/beans"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
(2)注解方式(JAVA 配置类)
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}

3、定义切面(aspect)

先上一段 aspect 类完整代码,后面讲解其中具体内容:

@Component
@Aspect//表明是一个切面类
public class SmsAspect {

    //声明一个切入点
    @Pointcut(value = "execution(* SmsSender.sendSMS(..))")
    private void smsSenderMethod() {
    }

    //后置通知:目标方法正常结束执行
    @AfterReturning(pointcut = "smsSenderMethod()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) throws IOException {
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取方法返回值(根据实际返回类型)
        String resultStr = (String) result;
        //获取方法参数
        Object[] args = joinPoint.getArgs();
        //要做的增强操作
        ......
    }
}

以下为定义切面详细说明:

当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:

@Component
@Aspect//表明是一个切面类
public class SmsAspect {
}

说明:

  • 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Spring Bean, 因此我们还需要使用类似 @Component 之类的注解。
  • 如果一个类被@Aspect 标注, 则这个类就不能是其他 aspect 的advised object了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外。

4、申明切入点(pointcut)

一个 pointcut 的声明由两部分组成:

  • 一个方法签名, 包括方法名和相关参数
  • 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).

在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即:

//声明一个切入点
@Pointcut(value = "execution(* SmsSender.sendSMS(..))")
    private void smsSenderMethod() {
}
  • 这个方法必须无返回值
  • 这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.
  • 切点表达式由标志符和操作参数组成. 如 “execution( greetTo(..))” 的切点表达式, execution 就是标志符, 而圆括号里的 greetTo(..) 就是操作参数。
  • 方法体为空即可

上面这个 pointcut 所描述的是: 匹配当前包下类 SmsSender 中名为 sendSMS 的所有方法的执行.

5、声明 advice

advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围(前置、后置、异常、最终、环绕通知)运行. pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式.
如下例子:

//后置通知:目标方法正常结束执行
@AfterReturning(pointcut = "smsSenderMethod()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) throws IOException {
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取方法返回值(根据实际返回类型)
    String resultStr = (String) result;
    //获取方法参数
    Object[] args = joinPoint.getArgs();
    //要做的增强操作
}

这里, @AfterReturning 表明该通知(又叫增强)是一个后置通知(正常返回通知),引用了一个 pointcut, 即 “smsSenderMethod()” 是一个 pointcut 的名字。

到这里一个完整的基于注解的 Spring AOP 就完成了。

五、扩展

下面进行扩展,包括五种通知(Advise),切点表达式的讲解。

(一)、五种通知(Advise)

1、五种通知说明
(1)前置通知

在目标方法执行之前执行的通知。

前置通知方法,可以没有参数,也可以额外接收一个 JoinPoint,Spring 会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息。

注意,如果接收 JoinPoint,必须保证其为方法的第一个参数,否则报错;前置通知不会影响目标方法的执行,除非此处抛出异常。

对应注解:@Before

(2)后置通知

在目标方法正常执行完成后执行,如果目标方法抛出异常,则不会执行。 

在后置通知中也可以选择性的接收一个 JoinPoint 来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。

对应注解:@AfterReturning

(3)异常通知通知

在目标方法抛出异常时执行的通知

可以配置传入 JoinPoint 获取目标对象和目标方法相关信息,但必须处在参数列表第一位。

另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。

对应注解:@AfterThrowing

(4)最终通知

是在目标方法执行之后执行的通知。

和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。

而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。

另外,后置通知可以通过配置得到返回值,而最终通知无法得到。

最终通知也可以额外接收一个 JoinPoint 参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。

对应注解:@After

(5)环绕通知

在目标方法执行之前和之后都可以执行额外代码的通知。

在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过 ProceedingJoinPoint 来实现的,可以在环绕通知中接收一个此类型的形参,spring 容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。

要注意,只有环绕通知可以接收 ProceedingJoinPoint,而其他通知只能接收 JoinPoint。

环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个 null。

环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。

环绕通知虽然有这样的能力,但一定要慎用,不是技术上不可行,而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。

对应注解:@Around

2、五种通知的执行顺序
(1). 在目标方法没有抛出异常的情况下

前置通知 —> 环绕通知的调用目标方法之前的代码 —> 目标方法 —> 环绕通知的调用目标方法之后的代码 —> 后置通知 —> 最终通知

(2). 在目标方法抛出异常的情况下

前置通知 —> 环绕通知的调用目标方法之前的代码 —> 目标方法 抛出异常 异常通知 —> 最终通知

(3). 如果存在多个切面

多切面执行时,采用了责任链设计模式。

切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的 proceed()执行时,去执行下一个切面或如果没有下一个切面执行目标方法,从而达成了如下的执行过程:

AOP简介及Spring AOP的简单注解实现-打不死的小强

如果目标方法抛出异常:

AOP简介及Spring AOP的简单注解实现-打不死的小强
3、五种通知的常见使用场景

举例但不限于:

前置通知控制数据源的切换
环绕通知控制事务 权限控制
后置通知记录日志(方法已经成功调用)
异常通知异常处理 控制事务
最终通知记录日志(方法已经调用,但不一定成功)

(二)、切点表达式

1、表达式会出现在以下几种场景:

(1)作为@Pointcut 的参数,用以定义连接点

@Pointcut("within(@org.springframework.stereotype.Repository *)")
public void repositoryClassMethods() {}

在上面的代码片段中的注解@Pointcut 的参数”within(@org.springframework.stereotype.Reposity *)”就是使用的切点表达式。而上代码中的 repositoryClassMethods()方法被 AOP AspectJ 定义为切点签名方法,作用是使得通知的注解可以通过这个切点签名方法连接到切点,通过解释切点表达式找到需要被切入的连接点。最终的目的都是为了找到需要被切入的连接点。像下面这段代码片段:

@Around("repositoryClassMethods()")
public Object measureMethodExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

这种方式先定义好切点签名方法,再在通知属性指向该切点签名。

(2)直接作用在五种通知属性定义上,也是用于定义连接点

@Around("within(@org.springframework.stereotype.Repository *)")
public Object measureMethodExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

可以看到这种方式直接在通知上通过切点表达式指定了连接点。

2、切点标志符

切点指示符是切点定义的关键字,切点表达式以切点指示符开始。开发人员使切点指示符来告诉切点将要匹配什么,有以下 9 种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation,下面一一介结这 9 种切点指示符。

(1)execution

execution 是一种使用频率比较高比较主要的一种切点指示符,用来匹配方法签名,方法签名使用全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数、异常类型,其中返回类型,方法,参数是必须的,其它项都是可选的。execution 语法如下:

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

如下面代码片段所示:

@Pointcut("execution(public String com.huatec.dao.UserDao.findById(Long))")

上面的代码片段里的表达式精确地匹配到 UserDao 类里的 findById(Long)方法,但是这看起来不是很灵活。假设我们要匹配 UserDao 类的所有方法,这些方法可能会有不同的方法名,不同的返回值,不同的参数列表,为了达到这种效果,我们可以使用通配符。如下代码片段所示:

@Pointcut("execution(* com.huatec.dao.UserDao.*(..))")

第一个通配符匹配所有返回值类型,第二个匹配这个类里的所有方法,()括号表示参数列表,括号里的用两个点号表示匹配任意个参数,包括 0 个

再如如下例子:

execution(* com.huatec.service.impl..*.*(..))

上面表达式可以分为五个部分:
①、execution(): 表达式主体。
②、第一个*号:表示返回类型,*号表示所有的类型。
③、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.huatec.service.impl 包、子孙包下所有类的方法。
④、第二个*号:表示类名,*号表示所有的类。
⑤、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,..表示任何参数。

(2)within

使用 within 切点标示符可以达到上面例子一样的效果,within 用来限定连接点属于某个确定类型的类。如下面代码的效果与上面的例子是一样的:

@Pointcut("within(com.huatec.dao.UserDao)")

我们也可以使用 within 指示符来匹配某个包下面所有类的方法(包括子包下面的所有类方法),如下代码所示:

@Pointcut("within(com.huatec..*)")
(3)this 和 target

this 用来匹配的连接点所属的对象引用是某个特定类型的实例,target 用来匹配的连接点所属目标对象必须是指定类型的实例;那么这两个有什么区别呢?原来 AspectJ 在实现代理时有两种方式:
1、如果当前对象引用的类型没有实现自接口时,spring aop 使用生成一个基于 CGLIB 的代理类实现切面编程
2、如果当前对象引用实现了某个接口时,Spring aop 使用 JDK 的动态代理机制来实现切面编程
this 指示符就是用来匹配基于 CGLIB 的代理类,通俗的来讲就是,如果当前要代理的类对象没有实现某个接口的话,则使用 this;target 指示符用于基于 JDK 动态代理的代理类,通俗的来讲就是如果当前要代理的目标对象有实现了某个接口的话,则使用 target.:

public class UserDao implements BaseDao {
    ...
}

比如在上面这段代码示例中,spring aop 将使用 jdk 的动态代理来实现切面编程,在编写匹配这类型的目标对象的连接点表达式时要使用 target 指示符, 如下所示:

@Pointcut("target(com.huatec.dao.BaseDao)")

如果 UserDao 类没有实现任何接口,或者在 spring aop 配置属性:proxyTargetClass 设为 true 时,Spring Aop 会使用基于 CGLIB 的动态字节码技为目标对象生成一个子类将为代理类,这时应该使用 this 指示器:

@Pointcut("this(com.huatec.dao.UserDao)")
(4)args

参数指示符是一对括号所括的内容,用来匹配指定方法参数:

@Pointcut("args(* *..find*(Long))")

这个切点匹配所有以 find 开头的方法,并且只一个 Long 类的参数。如果我们想要匹配一个有任意个参数的方法也可以,但是第一个参数必须是 Long 类的,我们则可使用下面这个切点表达式:

@Pointcut("args(* *..find*(Long,..))")
(5)@Target

这个指示器匹配指定连接点,这个连接点所属的目标对象的类有一个指定的注解:

@Pointcut("@target(org.springframework.stereotype.Repository)")
(6)@args

这个指示符是用来匹配连接点的参数的,@args 指出连接点在运行时传过来的参数的类必须要有指定的注解(注意是在参数类上有某个注解,而不是直接在参数上有某个注解),假设我们希望切入所有在运行时接受是@Entity 注解的 bean 对象的方法:

首先,需要在参数的类(User)上注解该 @Entity

@Entity
public class User {
    ......
}

然后定义切入点

@Pointcut("@args(com.huatec.aop.annotations.Entity)")
public void methodsAcceptingEntities() {}

为了在切面里接收并使用这个被@Entity 的对象,我们需要提供一个参数给切面通知:JointPoint(通过 jointPoint 可以拿到参数 )

@Before("methodsAcceptingEntities()")
public void logMethodAcceptionEntityAnnotatedBean(JoinPoint jp) {
    logger.info("Accepting beans with @Entity annotation: " + jp.getArgs()[0]);
}

最后,如下方法将被连接,并进入增强方法 logMethodAcceptionEntityAnnotatedBean

public void userOperation(User user) {
    ......
}

和 args 一样, 如果我们想要匹配除了被@Entity 注解的参数外还有其他若干参数的方法,也可以,但是需要第一个参数必须是被在类上注解@Entity 的那个参数 ,如下:

切入点变为:

@Pointcut("@args(com.huatec.aop.annotations.Entity,..)")
public void methodsAcceptingEntities() {}

以下方法会被连接:

public void userOperation(User user, String msg) {
    ......
}
(7)@within

这个指示器,指定匹配必须包括某个注解的的类里的所有连接点:

@Pointcut("@within(org.springframework.stereotype.Repository)")

上面的切点跟以下这个切点是等效的:

@Pointcut("within(@org.springframework.stereotype.Repository *)")
(8)@annotation

这个指示器匹配那些有指定注解的连接点,比如,我们可以新建一个这样的注解@Loggable:

@Pointcut("@annotation(com.huatec.aop.annotations.Loggable)")
public void loggableMethods() {}

我们可以使用@Loggable 注解标记哪些方法执行需要输出日志:

@Before("loggableMethods()")
public void logMethod(JoinPoint jp) {
    String methodName = jp.getSignature().getName();
    logger.info("Executing method: " + methodName);
}
3、切点表达式组合

可以使用&&、||、!、三种运算符来组合切点表达式,表示(与、或、非)的关系。

@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}



3 thoughts on “AOP简介及Spring AOP的简单注解实现

  1. Hi. I have checked your lwqgj.cn and i see you’ve got some duplicate content so probably it is the reason that you don’t
    rank high in google. But you can fix this issue fast. There is a tool that
    creates content like human, just search in google: miftolo’s tools

发表评论

邮箱地址不会被公开。 必填项已用*标注