9. Spring AOP

空~2022年9月6日
  • Spring
大约 13 分钟

9. Spring AOP

原文open in new window

1. 基本概念

AOP (Aspect-Oriented Programming),即 面向切面编程, 它与 OOP (Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.

在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)

AOP 是 Spring 是最难理解的概念之一,同时也是非常重要的知识点,因为它真的很常用。

2. 面向切面编程

在面向切面编程的思想里面,把功能分为两种

  • 核心业务:登陆、注册、增、删、改、查、都叫核心业务
  • 周边功能:日志、事务管理这些次要的为周边业务

在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的;

然后把周边功能和核心业务功能 "编织" 在一起,这就叫 AOP

3. AOP 的目的

AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

4. 术语

  • 连接点(join point):对应的是具体被拦截的对象,因为 Spring 只能支持方法,所以被拦截的对象往往就是指特定的方法。具体是指一个方法

  • 切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。具体是指具体共同特征的多个方法。

  • 通知(advice):它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件,有这几种:

    • 前置通知(before advice)
    • 环绕通知(around advice)
    • 后置通知(after advice)
    • 异常通知(afterThrowing advice)
    • 事后返回通知(afterReturning advice)
  • **目标对象(target):**即被代理的对象,通俗理解各个切点的所在的类就是目标对象。

  • 引入(introduction):是指引入新的类和其方法,增强现有 Bean 的功能。

  • 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。

  • 切面(aspect):定义切点、各类通知和引入的内容,AOP 将通过它的信息来增强 Bean 的功能或将对应的方法织入流程。

5. 流程

图片的流程顺序基于 Spring 5

springAop

1. 五大通知执行顺序

不同版本的 Spring 是有一定差异的,使用时候要注意

  • Spring 4

    • 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
    • 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing
  • Spring 5.28

    • 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
    • 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终

2. 图例

举一个实际中的例子来说明一下方便理解:

SpringAop1 房东的核心诉求其实就是签合同,收钱,浅绿部分都是次要的,交给中介就好。

不过有的人可能就有疑问了,让房东带着不是更好吗,租客沟通起来不是更轻松吗?为啥非要分成两部分呢?

那么请看下面这种情况:

SpringAop2 当我们有很多个房东的时候,中介的优势就体现出来了。代入到我们实际的业务中,AOP 能够极大的减轻我们的开发工作,**让关注点代码与业务代码分离!**实现解藕!

6. AOP 实现方式一(SpringAPI 接口)

命名空间:

xmlns:aop="http://www.springframework.org/schema/aop"http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd

两个接口:

interface MethodBeforeAdvice 方法执行前

interface AfterReturningAdvice 方法执行后

  1. 周边功能

    @Component
    public class BeforeLog implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("before");
        }
    }
    @Component
    public class AfterLog implements AfterReturningAdvice {
        @Override
        public void afterReturning(Object o, Method method, Object[] objects, Object o1)
            throws Throwable {
            System.out.println("after");
        }
    }
    
  2. 核心业务

    @Component
    public class ServiceImpl {
        public void service() {
            System.out.println("service");
        }
    }
    
  3. applicationContext.xml

    <?xml version="1.0" encoding="UTF8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/aop
                            https://www.springframework.org/schema/aop/spring-aop.xsd
                            http://www.springframework.org/schema/context
                            https://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="demo"/>
    <aop:config>
        <!--pointcut:切入点;expression:表达式(要执行的位置)-->
        <!--中括号表示可选部分-->
        <!--expression:([访问权限类型] 返回值类型 [包名.类名.]方法名(参数类型和参数个数) [异常类型])-->
        <!--
            *  0至多个任意字符
            .. 用在方法参数中表示任意多个参数 用在包名后, 表示当前包及其子包路径
            +  用在类名后, 表示当前类及其子类 用在接口后, 表示当前接口及其实现类
            -->
        <aop:pointcut id="pointcut" expression="execution(* demo.ServiceImpl.*(..))"/>
        <!--执行环绕增加-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
    </beans>
    
  4. 测试

    public void AOPTest() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = context.getBean("userServiceImpl", UserService.class);
        userService.service();
    }
    
  5. 结果

    springAop3

7. AOP 实现方式二(配置文件)

  1. 周边功能

    public class Broker {
        public void before() {
            System.out.println("带租客看房");
            System.out.println("谈钱");
        }
    
        public void after() {
            System.out.println("给钥匙");
        }
    }
    
  2. 核心业务

    public class Landlord {
    
        public void service() {
            System.out.println("签合同");
            System.out.println("收钱");
        }
    }
    
  3. applicationContext.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/aop
                            http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="broker" class="demo.Broker"/>
    <bean id="landlord" class="demo.Landlord"/>
    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* demo.Landlord.service(..))"/>
        <aop:aspect id="broker" ref="broker">
        <aop:after method="after" pointcut-ref="pointcut"/>
        <aop:before method="before" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
    </beans>
    
  4. 测试

    public void AOPTest2() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = context.getBean("landlord", Landlord.class);
        landlord.service();
    }
    
  5. 结果

    springAop4

8. AOP 实现方式三(注解)

  1. 开启注解支持(自动代理生成器)

    1. <aop:aspectj-autoproxy proxy-target-class="false"/>
    2. false:(默认)jdk 方式实现
      • proxy-target-class="false"
    3. true:cglib 方式实现
      • proxy-target-class="true"
  2. 业务方法实现了接口时, 默认走 jdk 动态代理, 当业务方法没有接口时, 会自动走 cglib

  • jdk 动态代理

    • 使用 jdk 反射包中的类实现创建代理对象的功能
    • 要求:目标类必须实现接口
  • cglib 动态代理

    • 使用第三方的工具库, 实现代理对象的创建
    • 要求: 目标类必须能够继承, 不是 final
    • 原理: 就是继承, 子类就是代理
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="demo"/>
<aop:aspectj-autoproxy proxy-target-class="false"/>
</beans>
  1. @Aspect : 是 aspectj 框架中的注解。

    1. 作用:表示当前类是切面类。
    2. 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
    3. 位置:在类定义的上面
  2. 通知方法,方法是实现切面功能的。

    1. 方法的定义要求:

      1. 公共方法 public
      2. 方法没有返回值
      3. 方法名称自定义
      4. 方法可以有参数,也可以没有参数。
    2. 如果有参数,参数不是自定义的,有几个参数类型可以使用。

      1. 指定通知方法中的参数 : JoinPoint

      2. JoinPoint:业务方法,要加入切面功能的业务方法

        1. 作用是:可以在通知方法中获取业务方法执行时的信息, 例如方法名称,方法的实参
        2. 如果你的切面功能中需要用到业务方法的信息,就加入 JoinPoint
        3. 这个 JoinPoint 参数的值是由框架赋予, 必须是第一个位置的参数

1. 前置通知和后置通知

  1. @Before: 前置通知注解

    1. 属性:value ,是切入点表达式,表示切面的功能执行的位置。

    2. 位置:在通知方法的上面

    3. 特点:

      1. 业务方法之前先执行的
      2. 不会改变业务方法的执行结果
      3. 不会影响业务方法的执行。
  2. @AfterReturning:后置通知注解

    1. 属性:

      1. value ,是切入点表达式,表示切面的功能执行的位置。
      2. returning 自定义的变量,表示业务方法的返回值。
      3. 自定义变量名必须和通知方法的形参名一样。
    2. 位置:在通知方法的上面

    3. 特点:

      1. 业务方法之后先执行的

      2. 能够获取到业务方法的返回值,可以根据这个返回值做不同的处理功能

      3. 可以修改这个返回值

        1. 如果返回的是普通类型, 那么修改后的返回值, 不会影响到业务方法原本的返回值
        2. 如果返回的是对象类型, 那么可以修改该对象的属性值, 业务方法原本的对象属性也会跟着改变, 但是业务方法返回的对象依旧是同一个
  3. 周边功能

    @Component
    @Aspect
    public class Broker {
        /*** 前置通知*/
        @Before(value = "execution(* demo.Landlord.service(..))")
        public void before() {
            System.out.println("带租客看房");
            System.out.println("谈钱");
        }
        /** 后置通知*/
        @AfterReturning(value = "execution(* demo.Landlord.service(..))")
        public void after() {
            System.out.println("给钥匙");
        }
    }
    @Component
    @Aspect
    public class Broker {
        /** 有参通知方法 */
        @Before(value = "execution(* demo.Landlord.service(..))")
        public void before(JoinPoint jp) {
            // 获取方法的完整定义
            System.out.println("方法的签名(定义)=" + jp.getSignature());
            System.out.println("方法的名称=" + jp.getSignature().getName());
            // 获取方法的实参
            Object args[] = jp.getArgs();
            for (Object arg : args) {
                System.out.println("参数=" + arg);
            }
            System.out.println("带租客看房");
            System.out.println("谈钱");
        }
    
        /** 后置通知带returning属性(普通类型) */
        @AfterReturning(value = "execution(* demo.Landlord.service(..))", returning = "res")
        public void after(Object res) {
            System.out.println("给钥匙");
            System.out.println("后置通知接收的返回值:" + res);
            // 修改res的值
            res = "我被修改了";
            System.out.println("后置通知中修改的返回值:" + res);
        }
    }
    
  4. 核心业务

    @Component
    public class Landlord {
        /**无返回值业务方法*/
        public void service() {
            System.out.println("签合同");
            System.out.println("收钱");
        }
    }
    
    @Component
    public class Landlord {
        /**有返回值业务方法(普通类型)*/
        public String service() {
            System.out.println("签合同");
            System.out.println("收钱");
            return "Landlord 的 返回值";
        }
    }
    
  5. 测试

    /**无返回值*/
    public void AOPTest2() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = context.getBean("landlord", Landlord.class);
        landlord.service();
    }
    
    /**有返回值*/
    public void test() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord bean = context.getBean("landlord", Landlord.class);
        String service = bean.service();
        System.out.println(service);
    }
    
  6. 无参通知方法

    springAop5

  7. 有参通知方法

    springAop6

2. 环绕通知(Around)

  1. @Around: 环绕通知

    1. 属性:value 切入点表达式
    2. 位置:在通知方法的定义上面
  2. 特点:

    1. 它是功能最强的通知
    2. 业务方法的前和后都能增强功能。
    3. 控制业务方法是否被调用执行
    4. 修改原来的业务方法的执行结果。 影响最后的调用结果
    5. 环绕通知,等同于 jdk 动态代理的 InvocationHandler 接口
  3. 参数:ProceedingJoinPoint

    1. 就等同于 Method
    2. 作用:执行业务方法
  4. 返回值:就是业务方法的执行结果,可以被修改。

  5. 环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务

/**
   * 环绕增强
   * 可以在该方法中传递一个参数, 获取切入点
   *
   * @param point ProceedingJoinPoint
   */
@Around(value = "execution(* demo.Landlord.service(..))")
public void around(ProceedingJoinPoint point) throws Throwable {
    System.out.println("环绕前");
    System.out.println(point.getSignature());
    System.out.println("环绕后");
    // 执行切入的通知方法
    point.proceed();
}

3. 异常通知 AfterThrowing

  1. @AfterThrowing:异常通知

  2. 属性:

    1. value 切入点表达式
    2. throwinng 自定义的变量,表示业务方法抛出的异常对象。
    3. 变量名必须和通知方法的参数名一样
  3. 特点:

    1. 业务方法抛出异常时执行的
    2. 可以做异常的监控程序, 监控业务方法执行时是不是有异常。
    3. 如果有异常,可以发送邮件,短信进行通知
  4. 异常通知方法的定义格式

    1. public
    2. 没有返回值
    3. 方法名称自定义
    4. 方法有一个 Exception 参数, 如果还有参数那就是 JoinPoint
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
        //发送邮件,短信,通知开发人员
    }
    
  5. 执行

    try{
        SomeServiceImpl.doSecond(..)
        } catch (Exception e) {
        myAfterThrowing(e);
        }
    

4. 最终通知 After

  1. @After :最终通知

  2. 属性:value 切入点表达式

  3. 位置:在通知方法的上面

  4. 特点:

    1. 总是会执行
    2. 业务方法之后执行的
  5. 最终通知方法的定义格式

    1. public
    2. 没有返回值
    3. 方法名称自定义
    4. 方法没有参数,如果还有是 JoinPoint
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
    }
    
  6. 执行

    try{
        SomeServiceImpl.doThird(..)
        } catch (Exception e) {
        ...
        } finally {
        myAfter()
        {
    

5. 定义和管理切入点(Pointcut)

@Pointcut

  • 如果你的项目中有多个切入点表达式是重复的,可以复用的, 可以使用@Pointcut
  1. 属性:value 切入点表达式

  2. 位置:在自定义的方法上面

  3. 特点:

    1. 当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
    2. 其它的通知中,value 属性就可以使用这个方法名称,代替切入点表达式了
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))" )
private void mypt(){
    //无需代码,
}

@After(value = "mypt()")
public  void  myAfter(){
    System.out.println("执行最终通知,总是会被执行的代码");
    //一般做资源清除工作的。
}

@Before(value = "mypt()")
public  void  myBefore(){
    System.out.println("前置通知,在目标方法之前先执行的");
}

6. 总结

  • AOP 的出现是为了对程序解耦,减少系统的重复代码,提高可拓展性和可维护性
  • 常见的应用场景有权限管理、缓存、记录跟踪、优化、校准、日志、事务等等等等……总之 AOP 的使用是非常常见的。
  • 需要注意不同 Spring 版本之间的AOP 通知顺序是有差别的。补充:Spring5.28 为分界线。
  • 环绕通知很灵活、强大,但是也就意味着很难控制,如非必要,优先使用其他通知来完成。
  • 多切面作用同一个切点时候注意切片顺序。