什么是AOP?
-
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,就是 需要 某个通用的方法时,可以创建一个模板,模板里面就有这些通用的方法,然后再把需要这些方法的方法们嵌套进去运行,很像动态代理
同时需要增加一个方法,改动原来的代码很麻烦,所以直接使用一个模板来调用原来的代码,模板里面就有需要增加的方法:
这么说可能还有点抽象,举个例子,现在需要给项目中逻辑层每个方法添加一个记录运行耗时的功能,如果在每个方法里面都敲上这么一段新增的代码即麻烦又显得代码臃肿,这个时候就可以使用AOP:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Slf4j
@Aspect //声明为AOP类
@Component
public class TimeAspect {@Around("execution(* com.zeyu.service.*.*(..))") //切入点表达式,声明要生效的范围public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//1、记录开始时间long begin = System.currentTimeMillis();//2、调用原始方法运行Object result = joinPoint.proceed();//3、记录结束时间,计算方法执行耗时long end = System.currentTimeMillis();log.info(joinPoint.getSignature() + "方法执行耗时{}ms", end - begin);return result;}}
如上述代码,在执行逻辑层每个方法前都会先进行记录开始时间的操作,然后再执行目标方法,目标方法结束之后,再记录结束时间,再输出耗时,这样不仅节省了代码量,也更加便于维护管理
AOP的核心概念:
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
3.切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
4.切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
5.目标对象:Target,通知所应用的对象
AOP的运用
第一步:导入依赖
在pom.xml文件中添加AOP的起步依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步,编写程序:
建议新建一个aop包专门放aop程序
一个aop程序的规范有如下几点
- 类声明:必须在类上添加@Aspect注解声明当前类为切面类
- 方法声明:必须在方法上添加@通知类型注解(切入点表达式),声明通知类型和切入点范围
- 如果通知类型为Around,需要定义ProceedingJoinPoint joinPoint形参(形参名可自定义),并使用joinPoint的process方法调用连接点
- 如果同通知类型的aop程序有多个,可以添加@Order()注解设置执行优先级,直接在括号里面填写数字,数字越小越先执行
AOP的通知类型:
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
AOP的切入点表达式:
一、@execution
主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)其中带
?
的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
@Before("execution(void com.zeyu.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
-
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 -
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
-
方法的访问修饰符可以省略
-
返回值可以使用
*
号代替(任意返回值类型) -
包名可以使用
*
号代替,代表任意包(一层包使用一个*
) -
使用
..
配置包名,标识此包以及此包下的所有子包 -
类名可以使用
*
号代替,标识任意类 -
方法名可以使用
*
号代替,表示任意方法 -
可以使用
*
配置参数,一个任意类型的参数 -
可以使用
..
配置参数,任意个任意类型的参数
注意事项:
-
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式
execution(* com.zeyu.service.DeptService.list(..)) || execution(* com.zeyu.service.DeptService.delete(..))
切入点表达式的书写建议:
-
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
二、@annotation
适用于无规则匹配
需要:
编写自定义注解
在作为连接点的方法上添加自定义注解
自定义注解不需要再添加特别的注解,只需要声明作用范围和生效时间即可
例:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OPLog {
}
像这样定义好一个自定义注解好,再去需要使用aop的连接点业务方法上添加自定义注解,例:
@OPLog@Overridepublic List<Dept> list() {return deptMapper.list();}//rollbackFor = Exception.class 表示所有异常都会触发回滚,rollbackFor = ? 指定什么异常会回滚事务,默认是运行时异常将回滚@OPLog@Transactional(rollbackFor = Exception.class) //事务管理Transactional 如果打上该注解的方法、对象、接口出现异常,就会进行回滚@Overridepublic void delete(Integer id) {try {deptMapper.deleteById(id); //根据id删除部门empService.deleteByDeptId(id); //根据部门id删除员工} finally {DeptLog deptlog = new DeptLog();
// deptlog.setCreateTime(LocalDateTime.now());
// deptlog.setOperation("执行了解散部门的操作,此次解散的是" + id + "号部门");
// deptLogService.insert(deptlog);}}@OPLog@Overridepublic void add(Dept dept) {dept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());deptMapper.insert(dept);}@OPLog@Overridepublic void upadte(Dept dept) {dept.setUpdateTime(LocalDateTime.now());deptMapper.update(dept);}@OPLog@Overridepublic Dept getById(Integer id) {Dept dept = deptMapper.getById(id);return dept;}
打上注解之后,再在AOP程序上的通知类型注解里面使用@annotation注解声明范围,例:
@Pointcut("@annotation(com.zeyu.aop.OPLog)")public void log()
如此,该通知的切入点范围就是那些添加了自定义注解的业务方法
@Pointcut
如果有多个通知的切入点范围都是同一个切入点表达式,则可以把该切入点表达式提取出来:
创建一个方法,不需要方法体,在其上添加@Pointcut注解,注解value值为提取出来的切入点表达式,然后在需要使用该切入点表达式的通知注解里写上该方法名即可,示例:
连接点
前面有提到,在Around类型通知里,获取连接点信息需要在形参列表里定义一个ProceedingJoinPoint类型的形参,如果需要调用连接点,则使用该形参的process方法
而在其它类型通知里,获取连接点信息需要的是JoinPoint类型的形参,它是ProceedingJoinPoint的父类型
通过该形参,可以获取连接点的各种信息
下面是一个记录日志的例子,其中就用到了很多连接点信息:
package com.zeyu.aop;import com.alibaba.fastjson.JSONObject;
import com.zeyu.pojo.OperateLog;
import com.zeyu.service.OperateLogService;
import com.zeyu.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.Map;@Slf4j
@Aspect
@Component
public class OperateLogAspect { //操作日志@Autowiredprivate HttpServletRequest request;@Autowiredprivate OperateLogService operateLogService;@Pointcut("@annotation(com.zeyu.aop.OPLog)")public void log(){}@Around("log()")public Object record(ProceedingJoinPoint JoinPoint) throws Throwable {log.info("开始记录本次操作...");//记录日志OperateLog operateLog = new OperateLog();//操作人IDString jwt = request.getHeader("token");Map<String, Object> claims = JwtUtils.parseJWT(jwt);operateLog.setOperateUser((Integer) claims.get("id"));//操作类名operateLog.setClassName(JoinPoint.getTarget().getClass().getName());//操作方法名operateLog.setMethodName(JoinPoint.getSignature().getName());//操作方法参数operateLog.setMethodParams(Arrays.toString(JoinPoint.getArgs()));long start = System.currentTimeMillis();//执行目标方法Object proceed = JoinPoint.proceed();long end = System.currentTimeMillis();//方法返回值operateLog.setReturnValue(JSONObject.toJSONString(proceed));//操作耗时operateLog.setCostTime(end - start);operateLogService.insert(operateLog);return proceed;}
}