Spring - AOP详解
AOP详解
- 一、AOP简介
-
-
- 1、什么AOP
- 2、核心概念
-
- 二、AOP入门案例
- 三、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 包。
- 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、测试结果
- 通过测试结果,可以看出在执行 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()); }}
- 如果是代理对象,打印结果如下所示:
- 如果不是代理对象,打印结果如下所示:
- 两者的打印结果还是有明显的区别的,可以很清晰的进行区分。
四、AOP切入点表达式
- 切入点:需要增强的方法
- 切入点表达式:对需要增强方法的描述
1、语法格式
动作关键字 ( 访问修饰符 返回值 包名.类 / 接口名.方法名 ( 参数 ) 异常名 )
- 动作关键字:描述切入点的行为动作,一般我们只会涉及到 execution 关键字,定义切入点时直接写 execution 即可。
- 访问修饰符:该项可以省略不写,一般情况下都不写。访问修饰符就是 private,public等用于修饰权限的。
- 返回值:如果没有返回值,则指定为 void ;有返回值直接指定为返回类型。
- 包名、类名、方法名和参数这些就按照实际情况写即可。
2、通配符
我们很少为某一个具体的方法指定切入点,一般情况下,我们都会为同一个类型的方法指定切入点,这就需要借助通配符实现。
- 星号 * :单个独立的任意符号,既可以在包名中使用,用于指定任意包;也可以在方法名中使用,指定任意方法,也能作为前缀和后缀使用。
- 两个点 (不知道为什么打出两个点,CSDN编辑器显示三个,格式在例子中查看吧):多个连续的任意符号,常用于简化包名和参数的书写。
- 加号 + :用于匹配子类类型,使用频率极低,简单了解一下就行。
3、例子
1)示例一
2)示例二
3)示例三
4)示例四
5)示例五
6)示例六
7)示例七
8)示例八
9)示例九
- 例子有很多,只要懂了通配符的作用,对切入点表达式多加练习,就能轻松的掌握切入点表达式。
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)测试结果
- 通过测试结果不能发现,并没有执行异常通知,这是因为方法执行过程种并没有出现异常。
- 上图也展示出了环绕通知的效果,一个通过就能够实现前置和后置两个通知的功能,这也是其为什么非常受欢迎的原因。
开发者涨薪指南
48位大咖的思考法则、工作方式、逻辑体系UCloud