目录
AOP介绍(来自百度)
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑
的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。
简单比喻
Spring AOP(面向切面编程)是一个强大的编程范式,它允许开发者在不修改现有代码的情况下,增加额外的功能。
-
想象你有一个餐厅:
- 餐厅的厨师(目标对象)专注于制作美味的菜肴(业务逻辑)。
- 但是,餐厅还需要一些额外的服务,比如在上菜前检查食物是否安全(日志记录或安全检查),或者在顾客用餐后询问满意度(性能监控)。
-
AOP就像餐厅的经理:
- 经理(AOP框架)不会直接做厨师的工作,但他会在适当的时候介入,确保餐厅的服务质量。
- 比如,经理会在厨师完成一道菜后,检查食物是否符合卫生标准(前置通知)。
- 当顾客用餐时,经理可能会过来询问顾客是否需要额外的调料或者服务(环绕通知)。
- 用餐结束后,经理可能会再次出现,询问顾客对食物的评价(后置通知)。
-
切面就像是一个服务清单:
- 这个清单上写着在哪些情况下需要提供额外服务,比如“每次上菜前”或“顾客用餐后”。
-
通知就像具体的服务动作:
- 前置通知就像是在上菜前检查食物的卫生。
- 后置通知就像是在顾客用餐后询问满意度。
- 环绕通知就像是在顾客用餐过程中随时提供帮助。
-
织入就像是经理的日常工作:
- 经理根据服务清单(切面),在适当的时机提供服务(织入通知)。
-
切点就是服务的时机:
- 比如“每次上菜前”或“顾客用餐后”,这些都是服务的时机。
AOP概述
AOP核心概念
AOP执行流程
SpringAOP的底层是基于动态代理技术实现的,在程序运行的时候,会自动的基于动态代理技术为这个目标对象生成一个对应的代理对象,在这个代理对象中就会对目标对象当中的原始方法进行功能增强,具体如何增强,增强的逻辑就是我们所定义的通知。
以这个案例为例,首先在方法运行开始之前,先记录方法运行的开始时间,然后再调用原始的方法执行,上图原始的方法就是那个list()方法,接下来在代理对象中就会来执行原始的list()方法当中的逻辑,原始方法运行完毕后,接下来再来记录方法运行的结束时间,最后再来统计方法执行的耗时。
所以,在代理对象中就已经对目标方法进行了增强。最终程序运行时,controller层中注入就不再是这个目标对象,而是代理对象。调用方法是也调用的是代理对象中的list()方法。
一旦进行AOP开发,那最终运行的就不再是原始的目标对象,而是基于目标对象所生成的代理对象。
AOP进阶
通知类型
@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Before("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info("before...");
}
@Around("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around after...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("around after...");
return result;
}
@After("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
public void after(){
log.info("after...");
}
//原始方法执行完毕正常返回才会运行这个通知
@AfterReturning("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
public void afterReturning(){
log.info("afterReturning...");
}
//秒表方法执行出现异常时才会执行
@AfterThrowing("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
public void afterThrowing(){
log.info("afterThrowing...");
}
}
大量切入点表达式重复了,直接将切入点表达式抽取出来
@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Pointcut("execution(* com.mikey.service.impl.DeptServiceImpl.*(..))")
private void pt(){
}
@Before("pt()")
public void before(){
log.info("before...");
}
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("around after...");
return result;
}
@After("pt()")
public void after(){
log.info("after...");
}
//原始方法执行完毕正常返回才会运行这个通知
@AfterReturning("pt()")
public void afterReturning(){
log.info("afterReturning...");
}
//秒表方法执行出现异常时才会执行
@AfterThrowing("pt()")
public void afterThrowing(){
log.info("afterThrowing...");
}
}
这个切入点表达式不仅可以在当前这个切面类当中来引用,也可以在其他的切面类当中来引用,private改为public就行。
public class TimeAspect {
//@Around("execution(* com.mikey.service.*.*(..))")//切入点表达式
@Around("com.mikey.aop.MyAspect1.pt()")
@PointCut
通知顺序
切入点表达式
1.execution
2.@annotation
如果要匹配两个不同的切入点方法,用||拼接很繁琐,可以用特定注解,简化。
连接点
@Around("execution(* com.itheima.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
Object res = joinPoint.proceed(); //执行原始方法,获取返回值(环绕通知)
return res;
}
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
}
案例
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
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 '操作日志表';
@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; //操作耗时
}
//自定义注解
@Retention(RetentionPolicy.RUNTIME)//指定当前这个注解什么时候有效,现在是运行时有效
@Target(ElementType.METHOD)//指定当前注解能作用在哪些地方 作用在方法上 不是类上也不是接口上
public @interface Log {
}
@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);
}
@Component
@Aspect//切面类
@Slf4j
public class LogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private HttpServletRequest request;
@Around("@annotation(com.mikey.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 rerurnValue = JSONObject.toJSONString(result);
//操作耗时
Long costTime = end-begin;
//记录操作日志
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,rerurnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志:{}",operateLog);
return result;
}
}
/*
删除部门
*/
@Log
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门",id);
//调用service删除部门
deptService.delete(id);
return Result.success();
}
/*
* 新增部门
* */
@Log
@PostMapping
public Result add(@RequestBody Dept dept){
log.info("新增部门:{}",dept);
//调用service新增部门操作
deptService.add(dept);
return Result.success();
}
/*
* 修改部门信息
* */
@Log
@PutMapping
public Result update(@RequestBody Dept dept){
log.info("更新部门信息");
deptService.update(dept);
return Result.success();
}
@Log
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){
log.info("批量删除操作,ids:{}",ids);
empService.delete(ids);
return Result.success();
}
@Log
@PostMapping
public Result save(@RequestBody Emp emp){
log.info("新增员工,emp:{}",emp);
empService.save(emp);
return Result.success();
}
@Log
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("更新员工信息:{}",emp);
empService.update(emp);
return Result.success();
}
获取当前登录对象挺关键的,需要从请求头中获取到用户的jwt令牌,然后解析令牌取出用户的id。