Spring AOP

1. 简介

1.1 AOP概述

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善(锦上添花)。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面理解为一个动态过程(例如,在对象运行时动态织入一些扩展功能或控制对象执行)。如图所示:

AOPOOP 字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。

1.2.AOP的作用?

  • 代码重用:将一些通用的功能(比如日志记录、安全控制等)抽象出来,形成可重用的模块;
  • 降低代码耦合度:将不同的关注点分离开来,这可以避免代码之间的紧耦合;
  • 简化开发:使开发人员将关注点从业务逻辑中分离出来,使得开发更加简单明了;
  • 提高系统可扩展性:在系统需求变化时,只需要修改AOP模块而不是修改业务逻辑,这可以使得系统更加易于扩展和维护:

1.3. AOP原理分析

假如现在有一个业务对象,这个对象已经实现了一些核心业务,但是我们希望在核心业务的基础上在添加一些拓展业务,而且要求不能对目标业务对象中的实现进行修改(遵循OCP原则-对扩展开放,对修改关闭),请问如何实现?

我们可以这样,自己为目标类创建子类或者为目标类创建兄弟类,对目标业务进行功能拓展。这种方式是可以去实现的,但是假如需要进行业务拓展的类有很多,我们每个类都要基于目标类型进行子类或兄弟类的创建,工作量会比较大,那怎么办呢?

AOP可以在系统启动时为目标类型创建子类或兄弟类型对象,这样的对象我们通常会称之为动态代理对象.如图所示:

其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种:
第一种:借助JDK官方API(Proxy)为目标对象类型创建其兄弟类型对象.
第二种:借助CGLIB库(Enhancer)为目标对象类型创建其子类类型对象.

2. AOP快速入门

业务描述
在项目中定义一个日志切面(LogAspect),通过切面中的通知方法(添加扩展业务的方法)为目标业务对象做记录日志的功能增强。

  • 第1步: 创建SpringBoot工程 _05springaop
  • 第2步: 在项目中添加aop应用依赖,代码如下:
    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    

说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ是一个面向切面的框架,他定义了 AOP的一些语法,有一个专门的字节码生成器来生成遵守 java规范的 class 文件(了解)。

  • 第3步: 准备被代理的目标资源

    • 接口: cn.tedu._05springaop.aop.Calculator

      package cn.tedu._05springaop.aop;
      
      public interface Calculator {
          int add(int m, int n);
          int sub(int m, int n);
          int mul(int m, int n);
          int div(int m, int n);
      }
      
    • 实现类: cn.tedu._05springaop.aop.CalculatorImpl

      package cn.tedu._05springaop.aop;
      
      import org.springframework.stereotype.Component;
      
      @Component
      public class CalculatorImpl implements Calculator{
          @Override
          public int add(int m, int n) {
              int result = m + n;
              System.out.println("方法内部:" + result);
              return result;
          }
      
          @Override
          public int sub(int m, int n) {
              int result = m - n;
              System.out.println("方法内部:" + result);
              return result;
          }
      
          @Override
          public int mul(int m, int n) {
              int result = m * n;
              System.out.println("方法内部:" + result);
              return result;
          }
      
          @Override
          public int div(int m, int n) {
              int result = m / n;
              System.out.println("方法内部:" + result);
              return result;
          }
      }
      
      
  • 第4步: 创建切面类并配置 LogAspect

    用于进行切入点的定义,功能增强方法的定义,在方法内部做日志业务增强,关键代码如下:

    package cn.tedu._05springaop.aop;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    /**
     * Aspect注解:表示这个类是一个切面类
     */
    @Aspect
    @Component
    public class LogAspect {
        @Before("execution(public int cn.tedu._05springaop.aop.CalculatorImpl.*(..))")
        public void beforeMethod(JoinPoint joinPoint) {
            System.out.println("LogAspect类中:前置通知,方法开始记录日志了...");
        }
    }
    
  • 第5步: 在测试类中进行测试

    @SpringBootTest
    class ApplicationTests {
        @Test
        void contextLoads() {
            ApplicationContext context = new AnnotationConfigApplicationContext("cn.tedu._05springaop");
            Calculator calculator = context.getBean(Calculator.class);
            int addResult = calculator.add(10, 20);
        }
    }
    

切面对象设计
Spring中通过代理对象(Proxy)调用切面对象(Aspect)的方法为目标对象(Target Service Obect)做功能增强,这个时候就需要我们定义一个切面对象,在切面对象中定义通知方法(这里的方法中我们要写扩展业务),为目标业务进行功能拓展。

3. AOP核心组件分析

3.1. AOP切入点表达式

对于AOP中的切入点表达式可以分成两大类,分别为粗粒度和细粒度表达式:

  • 粗粒度的切入点表达式,可以作用于类中所有方法,不能精确指定某个方法。
    • bean("bean的名字"),这里表示bean中的所有方法都是切入点方法。例如bean("calculatorImpl")
      @Before("bean('calculatorImpl')")
    • within("包名.类名表达式")指定包中指定类或所有类内部的方法都是切入点方法,例如 within("cn.tedu._05springaop.aop.*")

calculatorImpl在装配CalculatorImpl类时,Spring会自动生成一个小写字母开头的名称(如果前两个字母都是大写(DBean),那就是原类名),当然也可以指定@Component(Value="calculator")

  • 细粒度的切入点表达式,可以精确于类中指定的某个方法。
    • execution(权限标识符 方法返回值 方法所在包名.类名.方法名(参数列表))
      • @Before("execution(public int cn.tedu._05springaop.aop.CalculatorImpl.add(int,int)))
      • @Before("execution(* * cn.tedu..*.*(..))")

3.2. 切入点表达式语法

  • 权限标识符: * 表示权限任意
  • 方法返回值: * 表示返回值不限;
  • 包名部分: .. 表示包的层次和深度任意;
  • 类名部分: * 表示类名任意;
  • 方法名部分:* 表示方法名任意;
  • 方法参数列表部分:(..) 表示参数列表任意;

3.3. AOP通知方法

在AOP切面中可以有多个通知方法,这些方法可以使用指定注解进行描述:

  • @Before前置通知(此注解描述的方法,会在目标业务执行之前执行)
  • @After后置通知(目标业务方法执行结束侯执行,无论是否出现了异常)
  • @AfterReturning返回通知(目标业务方法没有异常时执行)
  • @AfterThrowing异常通知(目标业务方法抛出异常时执行)
  • @Around环绕通知(最重要,可以在此方法内部调用目标业务执行链,优先级最高)

3.4 示例

分析由如上描述的通知方法分别在什么时候执行。

package cn.tedu._05springaop.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * Aspect注解:表示这个类是一个切面类
 */
@Aspect
@Component
public class LogAspect {
    /*
        @Pointcut注解:定义切入点表达式方法,指定哪些方法需要被增强
     */
    @Pointcut("execution(int cn.tedu._05springaop.aop.CalculatorImpl.*(..))")
    public void doTime(){}

    /**
     * 1.Before注解: 前置通知,调用目标方法[add() sub() mul() div()]之前需要执行的方法;
     * 2.参数为:切入点表达式,指定哪些方法需要被拦截植入扩展业务逻辑;
     */
    @Before("doTime()")
    public void beforeMethod(JoinPoint joinPoint) {
        /*
            getSignature(): 获取连接点签名信息;
            getName(): 获取连接点名称;
            getArgs(): 获取连接点参数;
         */
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("LogAspect类中:前置通知,方法开始记录日志了..." + methodName + ":" + args);
    }
    /**
     * After:
     *   1.后置通知,目标方法执行结束[正常结束 || 异常结束]后执行此方法;
     *   2.后置通知没有权限获取目标方法的返回值;
     */
    @After("doTime()")
    public void aftherMethod(JoinPoint joinPoint){
        System.out.println("LogAspect类中:后置通知,方法开始记录日志了...");
    }
    /**
     * AfterReturning
     *   1.返回通知:目标方法正常结束后执行,异常结束则不会执行;
     *   2.返回通知有权限获取目标方法的返回值;
     */
    @AfterReturning(value = "doTime()", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        System.out.println("LogAspect类中:返回通知,方法开始记录日志了...");
    }
    /**
     * AfterThrowing
     *   1.异常通知: 目标方法抛出异常时执行;
     *   2.异常通知有权限拿到目标方法抛出的异常对象;
     */
    @AfterThrowing(value = "doTime()", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        System.out.println("LogAspect类中:异常通知,方法开始记录日志了...");
    }
    /**
     * Around
     *   1.环绕通知: 等价于 前置通知+后置通知+返回通知+异常通知;
     *   2.环绕通知: 有权限执行目标方法!!! 通常使用 try ... catch ... finally 包裹;
     */
    @Around("doTime()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
        Object result = null;
        try {
            System.out.println("环绕通知:目标对象方法执行之前");
            //目标对象方法的执行
            result = joinPoint.proceed();
            System.out.println("环绕通知:目标对象方法执行之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知:目标对象方法出现异常");
        } finally {
            System.out.println("环绕通知:目标对象方法执行完毕");
        }

        return result;
    }
}

3.5. 切面优先级

当有多个切面时,假如你希望这些切面方法的执行有先后顺序,需要通过@Order注解指定切面优先级,例如

@Order(10) //数字越大优先级越低
@Aspect
@Component
public class TimeAspect{
   //.....
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring需要使用的组件配置pom.xml 1.简介 AOP: 面向切面编程[底层就是动态代理]指程序在运行期间...
    王侦阅读 5,728评论 0 1
  • Spring的AOPAOP的基本概念基于注解的“零配置”方式定义切面Bean定义Before增强处理定义After...
    渐丶忘阅读 5,497评论 0 0
  • 一、 AOP 简介 1.1 什么是 AOP AOP (Aspect Orient Programming),直译过...
    GaaraZ阅读 1,716评论 0 0
  • AOP的定义 AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP ...
    Java并发编程阅读 4,442评论 0 0
  • 六、基于@Aspect注解编程(重点) 1、说明 Spring 使用了和AspectJ 一样的注解并使用Aspec...
    唯老阅读 3,848评论 0 1