spring-第十三章 AOP
spring
文章目录
- spring
- 前言
- 1.AOP介绍
- 2.AOP七大术语
- 3.切点表达式
- 4.使用spring的AOP
- 4.1概述
- 4.2准备工作
- 4.3基于注解方式使用AOP
- 4.3.1准备目标类和目标方法
- 4.3.2编写配置类
- 4.3.3编写通知类
- 4.3.4编写测试类
- 4.3.5通知类型
- 4.3.6切面的先后顺序
- 4.3.7@PointCut注解通用切点
- 4.4基于XML方式使用AOP
- 4.5AOP案例:事务处理
- 4.6AOP案例:安全日志
- 总结
前言
介绍完代理模式后,我们来看看它在spring中的应用——AOP。
1.AOP介绍
一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
- 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
使用AOP可以很轻松的解决以上问题。
用一句话总结AOP:将与核心业务无关的代码(交叉业务)独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
- 第一:代码复用性增强。
- 第二:代码易维护。
- 第三:使开发者更关注业务逻辑。
上一章中介绍JDK动态代理和CGLIB动态代理,是因为spring中就是用这两个技术实现AOP的
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
2.AOP七大术语
在使用spring的AOP之前,我们需要先知道AOP中的七个概念:
- 连接点:在整个业务流程中,可以插入额外功能的****位置。
- 切点:如果我们为原业务的某方法前或后插入新功能,则该方法就是切点。即,切点是我们将插入额外功能的原方法。
- 通知:指的是我们具体的我们要添加的额外功能。根据插入位置的不同可以分为:前置通知、后置通知、环绕通知、异常通知、最终通知。
- 切面:切点+通知就是一个切面,也就是原方法与额外功能结合形成的新逻辑。
- 织入:把通知应用到目标对象上的过程。
- 代理对象:由目标对象织入通知后产生的新对象。
- 目标对象:被织入通知的对象。
3.切点表达式
AOP会对原有的方法进行功能增强,那么我们在代码中如何找到要添加功能的原方法?答案就是切点表达式。
切点表达式用来定义通知(Advice)往哪些方法上切入,其格式如下:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问权限控制符:
- 可选项
- 省略就是四个权限(private、protected、default、public)都包括
- 可以填写具体权限符表示只应用于权限等级匹配的方法
返回值类型:
- 必填项
- 填写*****表示任意返回类型
全限定类名:
- 因为可能存在同名方法,所以可以额外填写全限定类名进一步确认
- 可选项
- …表示范围为当前包以及子包下的所有类
- 省略表示所有的类
方法名:
- 必填项
- *****表示所有方法
- **set***表示所有set方法
形式参数列表
- 必填项
- ()表示无参方法
- (…)表示任意类型、任意个数参数
- (*)表示只有一个参数
- (*,String)表示第一个参数任意,第二个参数是字符串
异常
- 可选项
- 省略表示任意异常类型
下面来举几个具体的例子:
service包下所有类中以delete开始的所有方法
execution(public * com.powernode.mall.service..delete(…))
mall包下所有类的所有方法
execution(* com.powernode.mall…*(…))
所有类的所有方法
execution(* *(…))
4.使用spring的AOP
4.1概述
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。
4.2准备工作
要使用AOP功能,我们先要导入aop和aspects的依赖,同时为了保证AOP功能的完善可以额外导入aspectjweaver依赖包。
所以我们需要导入以下依赖包:
org.springframework
spring-context
6.1.12
org.springframework
spring-aspects
6.1.10
org.aspectj
aspectjweaver
1.9.22.1
org.junit.jupiter
junit-jupiter
RELEASE
test
4.3基于注解方式使用AOP
4.3.1准备目标类和目标方法
先准备我们需要使用的目标类和目标方法,这里准备了一个OrderService类,且其中有一个generate()方法作为目标方法。代码如下:
package org.example.service;
import org.springframework.stereotype.Service;
//目标类
@Service("orderService")
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
需要使用注解让该类能够被IOC容器管理,这一步不要忽略。
4.3.2编写配置类
因为是使用注解的方式来进行使用,所以我们需要提供配置类来代替配置文件,配置类代码如下:
package org.example.conf;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan({"org.example.service","org.example.aspect","org.example.conf"})
public class AspectConf {
}
- 添加**@Configuration**表示该类为配置类
- 使用**@EnableAspectJAutoProxy注解开启自动动态代理功能。前面说过spring中使用了JDK动态代理和CGLIB动态代理。这里设置proxyTargetClass**属性值为true——表示指定使用CGLIB动态代理。如果不设置,默认值为false——表示在代理接口时使用JDK代理,代理类时使用CGLIB。
- 使用**@ComponentScan**注解添加包扫描路径,让IOC能够正确扫描我们所有需要的类并管理成bean
4.3.3编写通知类
我们需要把通知(增强功能)放到通知类里面,并在里面进行设置,决定该通知究竟要为哪一个目标方法进行补充,以及这些通知具体要在什么时候执行。
具体代码如下:
package org.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogAspect {
@Before("execution(* org.example.service.OrderService.generate())")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知");
System.out.println("Before " + joinPoint.getSignature().getName());
}
@After("execution(* org.example.service.OrderService.generate())")
public void after(JoinPoint joinPoint) {
System.out.println("后置通知");
}
}
- 使用**@Component**注解将通知类管理成bean
- 使用**@Aspect**注解把当前类标注为一个通知类
- 使用**@Before和@After**注解把方法标注为一个通知,同时决定其相较目标方法的执行时机,像这样能够标注通知并决定执行时机的注解共有五个后面会具体说。
- 在@Before和@After注解的参数中书写切入点表达式,确认当前通知作用于哪些目标方法。
4.3.4编写测试类
我们编写测试类,来验证当我们执行目标方法时,通知中的方法是否也会执行。
单元测试代码如下:
import org.example.conf.AspectConf;
import org.example.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AspectTest {
@Test
public void testAspect() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspectConf.class);
OrderService orderService = context.getBean("orderService", OrderService.class);
orderService.generate();
}
}
执行结果
执行结果正确。
4.3.5通知类型
前面我们使用@Before和@After注解来标注通知,但其实我们还可以用其他注解来标注通知。如下:
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
环绕通知
其他通知类型都是直接在方法上添加注解后在方法体内编写增强逻辑即可,但是环绕通知需要在编写增强逻辑的过程中调用目标方法,这里重点说一说。
环绕通知中我们有一个参数——ProceedingJoinPoint类型参数,该参数对象中有一个proceed()方法能够让我们在编写增强逻辑时调用目标方法,由此来决定环绕通知中各个额外功能代码相对于目标方法的位置。
如下:
@Around("execution(* org.example.service.OrderService.generate())")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法。
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
最终的运行效果就会让环绕通知的代码分开在目标方法的前后执行
执行顺序
各种不同类型的通知的执行顺序如下:
无异常时:环绕通知前部分-》前置通知-》目标方法-》后置通知-》最终通知-》环绕通知后部分
有异常时:环绕通知前部分-》前置通知-》目标方法-》异常通知-》最终通知
4.3.6切面的先后顺序
前面研究了不同通知类型的运行顺序,但那只是针对单个通知类的情况,当有多个通知类时不同类之间的不同通知又会按照什么顺序执行?
当拥有多个通知类时,我们可以在通知类上使用**@Order**注解来指定它们之间的执行顺序。
为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
假设有现在有两个通知类:LogAspect、LogAspect2,
其代码如下:
LogAspect
package org.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(1)
public class LogAspect {
@Pointcut("execution(* org.example.service.OrderService.generate())")
public void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知,Aspect1");
}
@After("pt()")
public void after(JoinPoint joinPoint) {
System.out.println("最终通知,Aspect1");
}
@Around("pt()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知开始,Aspect1");
// 执行目标方法。
joinPoint.proceed();
System.out.println("环绕通知结束,Aspect1");
}
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("后置通知,Aspect1");
}
}
LogAspect2
package org.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(2)
public class LogAspect2 {
@Pointcut("execution(* org.example.service.OrderService.generate())")
public void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知,Aspect2");
}
@After("pt()")
public void after(JoinPoint joinPoint) {
System.out.println("最终通知,Aspect2");
}
@Around("pt()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知开始,Aspect2");
// 执行目标方法。
joinPoint.proceed();
System.out.println("环绕通知结束,Aspect2");
}
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("后置通知,Aspect2");
}
}
我们分别使用@Order注解设置了优先级:LogAspect(1),LogAspect2(2)。
运行后结果如下
4.3.7@PointCut注解通用切点
在前面的例子中,我们明明所有通知都是作用于同一个目标方法,但是却要在每个通知上方都分别写同样的切点表达式。这样太麻烦。
于是spring中提供**@PointCut**注解,让我们遇到多个通知需要作用于同一个目标方法的情况下,只要写一次切点表达式即可。
- 单独用一个方法来使用@Pointcut注解,并书写切点表达式
- 其他通知原本写切点表达式的地方换为传入使用了@Pointcut注解的方法
4.4基于XML方式使用AOP
这种方式比较麻烦,也少有人用,这里先埋坑。
4.5AOP案例:事务处理
事务的工作十分适合使用AOP进行处理,比如事务开启操作可以由一个前置通知完成,事务提交可以用后置通知完成,最后还可以使用异常通知来进行事务回滚,这样一来很好的解决了事务代码和业务代码杂糅的问题。
这种我们自己使用AOP功能并编写事务代码来完成事务管理的方式,就是编程式事务管理。
而后面spring提供了一系列注解和xml配置项来完成事务功能,这种叫做声明式事务管理。
4.6AOP案例:安全日志
需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。
总结
本章我们介绍了spring中的重要概念——AOP