一.什么是AOP,由浅极深的感受AOP的好处
AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
举例:讲解AOP的用处
比如我们要我们计算每一个业务的耗时时间,我们是不是会在每一个业务之前加上业务开始时间,和业务结束时间。这样如果我们要计算新增,删除,修改的业务时间,是不是得每个方法加上开始时间和结束时间代码,这样是不是显得很繁琐。
所以AOP的作用是增强方法。
二:AOP核心概念
1. 连接点:JoinPoint,可以被AOP控制的方法 例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
5. 目标对象:Target,目标对象指的就是通知所应用的对象,我们就称之为目标对象。
三:AOP的进阶
1.通知类型(重要的@Around环绕通知的使用)
-
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
-
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
-
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
-
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
-
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
2. 通知顺序
通知的执行顺序为:前置通知 -> 环绕通知 -> 目标方法执行 -> 后置返回通知或后置异常通知 -> 最终通知。在环绕通知中,可以控制目标方法的执行,并在执行前后添加额外逻辑,因此它的执行顺序包含了整个通知流的生命周期。
3.切入点表达式
从AOP的入门程序到现在,我们一直都在使用切入点表达式来描述切入点。
第一种表达式 execution(……):根据方法的签名来匹配
-
省略方法的修饰符号
execution(void com.gyb.service.impl.DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替返回值类型execution(* com.gyb.service.impl.DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替包名(一层包使用一个*
)execution(* com.gyb.*.*.DeptServiceImpl.delete(java.lang.Integer))
-
使用
..
省略包名execution(* com..DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替类名execution(* com..*.delete(java.lang.Integer))
-
使用
*
代替方法名execution(* com..*.*(java.lang.Integer))
-
使用
*
代替参数execution(* com.gyb.service.impl.DeptServiceImpl.delete(*))
-
使用
..
省略参数execution(* com..*.*(..))
第二种@annotation使用例子(推荐这一种)
自定义注解:MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
业务类:DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Around("@annotation(com.itheima.anno.MyLog)")
public void log(ProceedingJoinPoint pjp){
Object result = pjp.proceed();
return result;
}
4.连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
-
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
-
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
四:AOP案例 写一个操作日志的案例
1.AOP起步依赖
<!--AOP起步依赖-->
<!--AOP起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.第二部创建表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
3.定义数据库实体类
//操作日志实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //主键ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
4.自定义注解
/**
* 自定义Log注解
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
5.对需要增强的代码进行增强
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
@Log
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间
empMapper.update(emp);
}
@Override
@Log
public void save(Emp emp) {
//补全数据
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//调用添加方法
empMapper.insert(emp);
}
}
6.定义切面类,完成操作日志代码的逻辑
@Slf4j
@Component
@Aspect //切面类
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//操作人ID - 当前登录员工ID
//获取请求头中的jwt令牌, 解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");
//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作类名
String className = joinPoint.getTarget().getClass().getName();
//操作方法名
String methodName = joinPoint.getSignature().getName();
//操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();
//调用原始目标方法运行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
//方法返回值
String returnValue = JSONObject.toJSONString(result);
//操作耗时
Long costTime = end - begin;
//记录操作日志
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志: {}" , operateLog);
return result;
}
}
7.mapper层相关代码
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}