> 文档中心 > Spring - AOP详解

Spring - AOP详解

AOP详解

  • 一、AOP简介
      • 1、什么AOP
      • 2、核心概念
  • 二、AOP入门案例
      • 1、导入依赖
      • 2、定义BeanDao接口及其实现类
      • 3、在通知类中定义通知和切入点
      • 4、定义Spring容器的配置类
      • 5、编写测试方法测试
      • 6、测试结果
  • 三、AOP工作流程
      • 1、启动Spring容器
      • 2、读取配置类中配置了的切入点
      • 3、初始化bean
      • 4、获取bean
      • 补充:如何判断是原始对象还是代理对象
  • 四、AOP切入点表达式
      • 1、语法格式
      • 2、通配符
      • 3、例子
          • 1)示例一
          • 2)示例二
          • 3)示例三
          • 4)示例四
          • 5)示例五
          • 6)示例六
          • 7)示例七
          • 8)示例八
          • 9)示例九
      • 4、书写技巧
  • 五、通知类型
      • 1、5种类型
      • 2、案例演示
          • 1)通知类
          • 2)测试类
          • 3)测试结果

一、AOP简介

1、什么AOP

  • AOP(Aspect Oriented Programming)面向切面编程,和 OOP(Object Oriented Programming)一样,都是一种编程思想。
  • AOP 致力于在不改变原有代码的基础上对其进行增强操作。

2、核心概念

  • 连接点(JoinPoint):简单来说,每个方法都是一个连接点。
  • 切入点(Pointcut):匹配连接点的式子。可用于描述一个方法,也可以使用通配符描述多个方法,详细内容可接着往下看。
    • 连接点的范围要比切入点大,切入点一定是连接点,连接点不一定是切入点。
  • 通知(Advice):将多个方法所需要执行的共性功能提取出来,声明为一个方法,称为通知。
  • 通知类:定义通知的类,通知是以方法的形式呈现,方法需要定义在类中,于是就有了通知类,用于定义通知方法,一个通知类中可以定义多个通知方法。
  • 切面(Aspect):用于描述通知和切入点之间的关系。
  • 目标对象(Target):需要增强的类,即需要在某些方法上添加增强通知的类。
  • 代理(Proxy):目标对象无法在执行时加上通知内容,需要借助代理对象实现,代理就是目标对象在执行方法时,将增强的内容加进去的过程。

二、AOP入门案例

  • 作为一个理科生,我是非常讨厌看一些纯理论的东西,下面以一个例子带大家入门AOP。
  • 案例需求分析:使用AOP注解方式在方法执行前打印当前系统时间。

1、导入依赖

<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-context</artifactId>    <version>5.3.16</version></dependency><dependency>    <groupId>org.aspectj</groupId>    <artifactId>aspectjweaver</artifactId>    <version>1.9.9.1</version></dependency>
  • 因为 “spring-context” 包中已经导入了 AOP 所需 jar 包,所以无需再手动导入 AOP 包。
    Spring - AOP详解
  • aspectjweaver 包是进行 AOP 开发中需要使用的 jar 包,像 @Aspect、@Pointcut 等AOP 注解都来自于该包。

2、定义BeanDao接口及其实现类

public interface BeanDao {    public void update();    public void save();}
@Repositorypublic class BeanDaoImpl implements BeanDao {    public void update() { System.out.println("update ...");    }    public void save() { System.out.println("save ...");    }}
  • 此处只是为了测试 AOP ,所定义的方法并没有实际的意义。实际开发中可以根据具体需求编写。

3、在通知类中定义通知和切入点

  • 我们的需求是在每个方法执行前打印出当前的系统时间,所以通知方法就是输出系统时间语句。
@Component@Aspectpublic class MyAdvice {    @Pointcut("execution(void dao.BeanDao.*())")    private void pt(){}    @Before("pt()")    public void method(){ System.out.println("当前系统时间" + System.currentTimeMillis());    }}
  • 使用 @Component 注解定义bean,这个注解之前我们遇见过,如果不了解的可以移步到 @Component 注解 查看。
  • @Aspect 注解将该类标注为切面类。
  • @Pointcut 用于定义切入点,切入点的具体书写方法见下。
  • @Before 注解表示前置通知,绑定切入点 pt 。

4、定义Spring容器的配置类

@Configuration@ComponentScan({"dao","advice"})@EnableAspectJAutoProxy     // 开启注解格式的AOP功能public class SpringConfig {}
  • @EnableAspectJAutoProxy 这个注解第一次出现,使用该注解开启注解格式的AOP功能。

5、编写测试方法测试

public class App {    public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); BeanDao bean = context.getBean(BeanDao.class); bean.update();    }}

6、测试结果

Spring - AOP详解

  • 通过测试结果,可以看出在执行 update 方法之前,率先打印出了系统时间,但是在 update 的方法体中并没有定义该输出语句,说明是通知起作用了,这就是 AOP 的无入侵式特性,无需改变方法原有代码就可以在方法执行前输出所需语句。
  • 这个入门案例只是给大家简单演示了一下什么叫 AOP 的无入侵式特性,这里面还涉及有许多细节配置,可以接着往下看。

三、AOP工作流程

1、启动Spring容器

  • Spring 容器启动之后就会去加载 bean ,对于 AOP 来说,需要加载需要被增强的类(如上述入门案例中的 BeanDaoImpl),和通知类(如上述入门案例中的MyAdvice)。但是此时的 bean 对象还未完全创建成功。

2、读取配置类中配置了的切入点

@Component@Aspectpublic class MyAdvice {    @Pointcut("execution(void mapper.UserMapper.selectById(int))")    private void pt2(){} @Pointcut("execution(void dao.BeanDao.update())")    private void pt(){}    @Before("pt()")    public void method(){ System.out.println("当前系统时间" + System.currentTimeMillis());    }}
  • 上述代码中只会读取切入点" pt() ",因为定义了 method 方法使用了 @Before 注解和切入点绑定了。
  • 而切入点 " pt2() "只是定义了,并没有与任何的通知绑定,所以该切入点不会被读取。

3、初始化bean

  • 首先会判断 bean 对应类中的方法是否匹配到切入点。
  • 如果没有匹配到切入点,则说明该 bean 不需要增强,直接创建原始对象。
  • 如果匹配到了切入点,说明有方法需要被增强,则会创建该 bean 的代理对象。

4、获取bean

  • 如果获取的 bean 是原始对象,直接调用方法执行。
  • 如果获取的 bean 是代理对象,则根据代理对象的运行模式执行原始方法和增强的内容。

补充:如何判断是原始对象还是代理对象

  • 调用获取到 bean 对象的 getClass() 方法查看打印信息。
public class App {    public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); BeanDao bean = context.getBean(BeanDao.class); System.out.println(bean.getClass());    }}
  • 如果是代理对象,打印结果如下所示:
    Spring - AOP详解
  • 如果不是代理对象,打印结果如下所示:
    Spring - AOP详解
  • 两者的打印结果还是有明显的区别的,可以很清晰的进行区分。

四、AOP切入点表达式

  • 切入点:需要增强的方法
  • 切入点表达式:对需要增强方法的描述

1、语法格式

动作关键字 ( 访问修饰符 返回值 包名.类 / 接口名.方法名 ( 参数 ) 异常名 )

  • 动作关键字:描述切入点的行为动作,一般我们只会涉及到 execution 关键字,定义切入点时直接写 execution 即可。
  • 访问修饰符:该项可以省略不写,一般情况下都不写。访问修饰符就是 private,public等用于修饰权限的。
  • 返回值:如果没有返回值,则指定为 void ;有返回值直接指定为返回类型。
  • 包名、类名、方法名和参数这些就按照实际情况写即可。

2、通配符

我们很少为某一个具体的方法指定切入点,一般情况下,我们都会为同一个类型的方法指定切入点,这就需要借助通配符实现。

  • 星号 * :单个独立的任意符号,既可以在包名中使用,用于指定任意包;也可以在方法名中使用,指定任意方法,也能作为前缀和后缀使用。
  • 两个点 (不知道为什么打出两个点,CSDN编辑器显示三个,格式在例子中查看吧):多个连续的任意符号,常用于简化包名和参数的书写。
  • 加号 + :用于匹配子类类型,使用频率极低,简单了解一下就行。

3、例子

1)示例一

Spring - AOP详解

2)示例二

Spring - AOP详解

3)示例三

Spring - AOP详解

4)示例四

Spring - AOP详解

5)示例五

Spring - AOP详解

6)示例六

Spring - AOP详解

7)示例七

Spring - AOP详解

8)示例八

Spring - AOP详解

9)示例九

Spring - AOP详解

  • 例子有很多,只要懂了通配符的作用,对切入点表达式多加练习,就能轻松的掌握切入点表达式。

4、书写技巧

  • 切入点一般情况下都是描述接口,尽量不要描述实现类,因为使用实现类耦合性较高。
  • 针对接口开发时访问修饰符使用public。
  • 返回值类型对于增删改时使用精准类型加速匹配,对于查询类使用 * 通配符描述。
  • 包名尽量使用 * 来表示一层,不要使用双点表示多层效率低。
  • 接口名 / 类型使用 * 匹配,如:UserService 通常都会写成 *Service。
  • 方法名使用动词进行精准匹配,如:getById 、getByName 都使用 getBy* 进行匹配。
  • 异常类通常不作为匹配规则。

五、通知类型

1、5种类型

  • 前置通知:追加功能到方法执行前。
  • 后置通知:追加功能到方法执行后,无论方法在执行过程种有没有抛出异常都会执行,说白了就是一定执行的。
  • 环绕通知(重点):该通知功能非常强大,他可以追到功能到方法的前后,一般我们都使用该种通知类型。
  • 返回后通知(了解):追加功能到方法执行后,与后置通知不同,只有在方法正常执行结束后才能够执行,如果出现异常,则不再执行。
  • 抛出异常后通知(了解):只有方法抛出异常,才会执行。

2、案例演示

1)通知类
@Component@Aspectpublic class MyAdvice {    @Pointcut("execution(void dao.BeanDao.update())")    private void pt(){}    @Before("pt()")    public void before(){ System.out.println(" before ... ");    }    @After("MyAdvice.pt()")    public void after(){ System.out.println(" after ... ");    }    @Around("pt()")    public void around(ProceedingJoinPoint pjp) throws Throwable { System.out.println(" around before ..."); // 对原始方法的调用 pjp.proceed(); System.out.println(" around after ...");    }    @AfterReturning("pt()")    public void afterReturning(){ System.out.println(" afterReturning ... ");    }    @AfterThrowing("pt()")    public void afterThrowing(){ System.out.println(" afterThrowing ... ");    }}
  • @Before 注解表示前置通知。
  • @After 注解表示后置通知。
  • @AfterReturning 注解表示返回后通知。
  • @AfterThrowing 注解表示抛出异常后通知。
  • @Around 注解表示环绕通知,他需要借助形参 ProceedingJoinPoint 来实现对原方法的调用,如果不使用该方式调用原方法则会跳过对原方法的执行。
  • 如果原始方法没有返回值,则环绕通知的返回值类型为 void ;如果有,建议将环绕通知的返回值类型定义为 Object 类型,更为常用。
2)测试类
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = SpringConfig.class)public class BeanDaoTest {    @Autowired    private BeanDao beanDao;    @Test    public void test1(){ beanDao.update();    }}
3)测试结果

Spring - AOP详解

  • 通过测试结果不能发现,并没有执行异常通知,这是因为方法执行过程种并没有出现异常。
  • 上图也展示出了环绕通知的效果,一个通过就能够实现前置和后置两个通知的功能,这也是其为什么非常受欢迎的原因。

开发者涨薪指南 Spring - AOP详解 48位大咖的思考法则、工作方式、逻辑体系UCloud