AOP
- AOP概述:
(1)AOP是什么:
是面向切面编程、面向方面编程,其实就是面向特定方法编程。
面向一个或者是多个方法编程就叫做面向切面编程。
- 场景:
对业务方法进行功能的扩展,或者是更改些许功能的情形的时候;由于一个业务的功能有很多,方法也有很多,当我们在每个业务功能里面都扩展功能的话会造成代码的冗余,降低效率。
这个时候我们就需要抽取出一个模版,将这么多的功能放在模版当中。这个过程就可以使用AOP的技术。
注意:当我们运行业务时候,程序就不会去调用业务程序功能了,而是直接调用AOP模版了。
例子:比如统计部门和员工当中所有方法的执行耗时。
- 动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
- AOP快速入门
- 导入依赖:在pom.xml中导入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 编写AOP程序:针对于特定方法根据业务需要进行编程
我们就拿统计方法的执行时间来举例:
@Component
@Aspect
@Slf4j
public class TimeAspect {
//切入点表达式,用来指定如下的方法是作用在哪些路径的方法里
@Around("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取方法运行的开始时间
long begin = System.currentTimeMillis();
//2.调用运行原始方法
//那我们怎么调用原始方法呢,我们就需要调用AOP给我们提供的ProceedingJoinPoint这个API
//result就代表原始方法的返回值
//其实在JoinPoint中就封装了原始方法的相关信息,然后调用proceed就将元素信息展示了出来
Object result = joinPoint.proceed();
//3.获取方法运行结束时间,计算执行耗时
long end = System.currentTimeMillis();
//可以使用joinPoint中的getSignature方法获取签名,
// 那么我们就会知道是哪个原始方法被调用了
log.info(joinPoint.getSignature()+"执行耗时 : {} ms", (end-begin));
return result;
}
}
- AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)那么衍生出基本上所有的方法都是AOP的连接点
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用,就是那个被注解Around修饰的那个东西
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- AOP的执行流程
当我们使用AOP之后,我们就不会去执行原始对象,而是去执行基于目标对象生成的代理对象去执行了。
当目标对象(此处为 DeptserviceImpl )功能需要被增强时,并且我们使用AOP方式定义了增强逻(在Aspect类中)
Spring会为目标对象自动生成一个代理对象,并在代理对象对应方法中,结合我们定义的AOP增强逻辑完成功能增强。
- AOP进阶
- 通知类型
- Before:在目标方法之前执行
- Around:在目标方法之前之后都执行
- After:在目标方法之后执行,有异常则不执行
- AfterReturning:在目标方法返回结果后执行
- AfterThrowing:在目标方法抛出异常后执行,不抛异常不执行
例子:
切面类:
@Aspect//被Aspect注解修饰的类就是AOP类了
@Component
@Slf4j
public class TestAspect {
//before在目标方法前被执行
@Before("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")//切入点
public void before() {
log.info("before...执行了");
}
//Arrount在目标方法前、后都被执行
@Around("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")//切入点
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around...执行1");
Object result = joinPoint.proceed();
log.info("around...执行2");
return result;
}
//after在目标方法后被执行,无论是否有异常
@After("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")//切入点
public void after() {
log.info("after...执行了");
}
//afterReturning在目标方法后被执行,有异常不会执行
@AfterReturning("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")//切入点
public void AfterReturning() {
log.info("afterreturning...方法执行了!");
}
//afterThrowing方法发生异常后执行
@AfterThrowing("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")//切入点
public void afterThrowing() {
log.info("afterThrowing...执行了!");
}
}
目标类:但是我们只使用PostMan 对目标类当中的一个方法进行测试(任选一个)
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
public List<Dept> getAllDeptInfo() {
List<Dept> selectAllDeptInfo = deptMapper.selectAllDeptInfo();
return selectAllDeptInfo;
}
//启用事务回滚完成对应部门的相关删除
@Override
@Transactional//将这个方法交由事务进行管理
public void deleteDeptById(Integer id) {
//删除部门
deptMapper.deleteDept(id);
//int i = 1 / 0;
//删除员工
empMapper.deleteBydept_id(id);
}
@Override
public void saveDept(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
dept.setCreateTime(LocalDateTime.now());
deptMapper.insertDept(dept);
}
//回显部门信息
@Override
public Dept getById(Integer id) {
Dept dept= deptMapper.getById(id);
return dept;
}
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}
注意事项:
@Around环绕通知需要自己调用ProCeedingJoinPoint.proceed()来让原始类执行,其他通知不需要考虑目标方法执行。
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
引申:
我们不难发现,每一个通知后面都跟上了相同的切面表达式,造成了代码的冗余,也不方便后期的更改,那么我们应该怎么样去去重呢
我们可以创建一个方法空的方法,在方法上面用切入点构建一个切面表达式,之后就可以直接在其他的通知上面引入:例如
@Aspect//被Aspect注解修饰的类就是AOP类了
@Component
@Slf4j
public class TestAspect {
@Pointcut("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")
public void pt() {
}
//before在目标方法前被执行
@Before("pt()")//切入点
public void before() {
log.info("before...执行了");
}
//Arrount在目标方法前、后都被执行
@Around("pt()")//切入点
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around...执行1");
Object result = joinPoint.proceed();
log.info("around...执行2");
return result;
}
//after在目标方法后被执行,无论是否有异常
@After("pt()")//切入点
public void after() {
log.info("after...执行了");
}
//afterReturning在目标方法后被执行,有异常不会执行
@AfterReturning("pt()")//切入点
public void AfterReturning() {
log.info("afterreturning...方法执行了!");
}
//afterThrowing方法发生异常后执行
@AfterThrowing("pt()")//切入点
public void afterThrowing() {
log.info("afterThrowing...执行了!");
}
}
注:我们使用的这个用@PointCut注解使用的切入点,也可以被其他的切面类使用,但是需要注意的是,我们需要将这个连接点对应的类设置成public才可以使用
该注解的作用是将公共的切点表达式抽取出来,需要用到时引用改切点表达式即可。
- 通知的执行顺序
1.不同切面类中,默认按照切面类的类名字母排序
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
2. 用 @Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
- 切入点表达式
切入点表达式是描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
Execution(...):根据方法的签名来匹配
@annotation(...):根据注解匹配
- Execution
主要是根据方法的返回值、包名、类名、方法名、参数等信息来匹配,语法:
Execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数)throws异常?)
其中:带?的表示可以省略的部分
访问修饰符:可省略(比如:public、protected)
包名.类名:可省略
Throws 异常:可省略(注意是方法上声明的异常,不是实际抛出的异常)
@Before(“execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))”)
Public void before(JoinPoint joinPoint)
- @annotation
自定义一个注解,想使用哪个方法就在该方法上面的通知后加上@annotation(com.itheima.anno.Log)
将自定义注解加入到该方法上面,在通知后加上自定义注解的全路径
@Retention用来注解这个注解在什么时候生效,设置为RUNTIME意思是在运行时生效
@Target用来注解这个注解作用在什么地方,设置为METHOD意思是注解在方法上有效。
- 连接点
可以被AOP控制的方法暗含其延伸的对象
在spring 中用 JinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型、方法实际参数等等。
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型
@Slf4j
@Aspect
@Component
public class TestAspect2 {
@Pointcut("execution(* com.itheima.servise.impl.DeptServiceImpl.*(..))")
public void pt() {
}
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取 目标对象的类名
String name = joinPoint.getTarget().getClass().getName();
//2.获取目标方法的方法名
String methodName = joinPoint.getSignature().getName();
//3.获取目标方法运行时传入的参数
Object[] args = joinPoint.getArgs();
//4.放行 目标方法执行
Object result = joinPoint.proceed();
//5.获取目标对象运行的返回值
log.info("目标对象运行的返回值:{}",result);
return null;
}
}