AOP
基础
Spring AOP( Aspect-Oriented Programming )
面向切面编程、允许开发者将横切关注点( Cross-Cutting Concerns )
从业务逻辑中分离出来,从而提高代码的模块化、可维护性和可重用性。
什么是 AOP
?
AOP
是一种编程范式,旨在解决传统面向对象编程(OOP
)中难以优雅处理的一些问题。在 OOP
中,业务逻辑通常集中在核心类和方法中,但某些功能(称为横切关注点)会分散在多个地方,例如:
-
日志记录 -
事务管理 -
权限验证 -
性能监控
入门程序
1、导入依赖:在pom.xml
中引入AOP
的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
2、编写AOP
程序:针对于特定的方法根据业务需要进行编程
package com.itheima.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// 交给IOC容器publicclassRecordTimeAspect{@Around("execution(* com.itheima.service.impl.*.*(..))")// execution(返回值类型 包名.类名.方法名(参数列表))public Object recordTime(ProceedingJoinPoint pjp)throws Throwable {// 1. 记录方法运行的开始时间long start = System.currentTimeMillis();// 2. 执行原始方法 Object result = pjp.proceed();// 3. 记录方法运行的结束时间、记录耗时long end = System.currentTimeMillis(); log.info("方法 {} 执行耗时: {}ms", pjp.getSignature(), end - start);return result; }}
核心概念
-
连接点: JoinPoint
,可以被AOP
控制的方法( 暗含方法执行时的相关信息 ) -> 所有方法都可以是连接点( 都可被AOP
控制 ) -
通知: Advice
,指那些重复的逻辑,也就是共性功能( 最终体现为一个方法 ) -> 举例子: 求时间差( 通知 )、所有方法都计算 -
切入点: PointCut
,匹配连接点的条件,通知仅会在切入点方法执行时被应用 -> 切入点表达式中决定了哪些方法是切入点 -
切面: Aspect
,描述通知于切入点的对应关系( 通知+切入点 ) -
目标对象: Target
,通知所应用的对象
AOP
执行流程
SpringAOP底层基于动态代理 -> 为目标对象生成代理对象 -> 代理对象与目标对象实现统一接口 -> 代理对象内实现 【 通知 和 执行原始方法 】 的逻辑运行逻辑:( 原始方法前后就是 通知 的内容 ),( 切入点表达式决定了哪些作为 目标对象 ) 运行 -> 到切面类 -> 到原始方法 -> 到切面类
AOP
进阶
通知类型
-
@Around
:环绕通知,此注解标注的通知方法在目标方法前、后都被执行 -
@Before
:前置通知,此注解标注的通知方法在目标方法前被执行 -
@After
:后置通知,此注解标注的通知方法在目标方法后被执行、无论是否有异常都会执行 -
@AfterReturning
:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会被执行 -
@AfterThrowing
:异常后通知,此注解标注的通知方法发生异常后执行
注意:
@Around
环绕通知需要自己调用ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行
@Around
环绕通知方法的返回值:必须指定为Object
,来接收原始方法的返回值
代码示例:
package com.itheima.aop;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Slf4j@Aspect@ComponentpublicclassMyAspect1{// 前置通知 目标方法运行之前运行@Before("execution(* com.itheima.service.impl.*.*(..))")publicvoidbefore(){ log.info("before..."); }@Around("execution(* com.itheima.service.impl.*.*(..))")public Object around(ProceedingJoinPoint pjp)throws Throwable { log.info("around ... before ..."); Object result = pjp.proceed(); log.info("around ... after ...");return result; }@After("execution(* com.itheima.service.impl.*.*(..))")publicvoidafter(){ log.info("after..."); }@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")publicvoidafterReturning(){ log.info("afterReturning..."); }@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")publicvoidafterThrowing(){ log.info("afterThrowing..."); }}
进行测试 -> 在Apifox中查询所有部门 进行测试 -> Success
在服务类中给出 1/0 的异常 -> 进行针对 @AfterThrowing 的测试
切入点表达式优化
@PointCut
:将公共的切点表达式抽取出来,需要用到时引用该切点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")publicvoidpt(){}// 返回值:// private:仅在当前切面类// public:在其他类中也可用
通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
执行顺序:
不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行 -
目标方法后的通知方法:字母排名靠前的后执行
用@Order(数字)
加在切面类上来控制顺序
-
目标方法前的通知方法:数字小的先执行 -
目标方法后的通知方法:数字小的后执行
package com.itheima.aop;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Slf4j@Order(5)@Component@AspectpublicclassMyAspect2{//前置通知@Before("execution(* com.itheima.service.impl.*.*(..))")publicvoidbefore(){ log.info("MyAspect2 -> before ..."); }//后置通知@After("execution(* com.itheima.service.impl.*.*(..))")publicvoidafter(){ log.info("MyAspect2 -> after ..."); }}
切入点表达式
描述切入点方法的一种表达式
用来决定项目中的哪些方法需要加入通知
常见形式:
-
execution(返回值类型 包名.类名.方法名(参数列表))
:根据方法的签名来匹配 -
@annotation(全限定注解类名)
:根据注解匹配
execution
execution( <访问修饰符> 返回值 <包名.类名.>方法名(方法参数) <throws 异常> )
-
访问修饰符、包名.类名、throws 异常 可以省略 -> 不建议 包名.类名 省略 -
可以使用通配符描述切入点
-
*
:单个独立的任意符号,可以通配任意一个参数,也可以通配其中的一部分 -
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
@Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
方法名尽量命名规范基于接口描述、而不是直接描述实现类 -> 增强扩展性尽量缩小切入点的匹配范围
@annotation
切入点表达式,用于匹配标识有特定注解的方法
自定义注解:构建一个注解类
给该自定义注解类加标记
@Target(ElementType.METHOD)
// 注解只能用在方法上@Retention(RetentionPolicy.RUNTIME)
// 注解在运行时依然可用(用于AOP)
package com.itheima.anno;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD) // 作用于方法上@Retention(RetentionPolicy.RUNTIME) // 运行时生效public@interface LogOperation {}
然后在切面上通过@Annotation来进行切入点表达式 内容就写 自定义注解的全类名
package com.itheima.aop;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;@AspectpublicclassMyAspect5{@Before("@annotation(com.itheima.anno.LogOperation)")publicvoidbefore(){ System.out.println("MyAspect5.before()"); }}
在你想要标识为连接点的方法进行 加入
@LogOperation
注解
@LogOperation@Overridepublicvoidsave(Dept dept){ dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept);}
连接点
在Spring
中用JoinPoint
抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
-
对于 @Around
通知,获取连接点信息只能使用ProceedingJoinPoint
-
对于其他四种通知,获取连接点信息只能使用 JoinPoint
,它是ProceedingJoinPoint
的父类型
示例代码
package com.itheima.aop;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;import java.util.Arrays;@Slf4j@Aspect@ComponentpublicclassMyAspect6{@Before("execution(* com.itheima.service.*.*(..))")publicvoidbefore(JoinPoint joinPoint){ log.info("before ...");// 1. 获取目标对象 Object target = joinPoint.getTarget(); // 获取目标对象 log.info("获取目标对象: {}", target);// 2. 获取目标类 String className = joinPoint.getTarget().getClass().getName(); log.info("获取目标类: {}", className);// 3. 获取目标方法 String methodName = joinPoint.getSignature().getName(); log.info("目标方法: {}", methodName);// 4. 获取目标方法参数 Object[] args = joinPoint.getArgs(); log.info("目标方法参数: {}", Arrays.toString(args)); }@Around("execution(* com.itheima.service.*.*(..))")public Object around(ProceedingJoinPoint pjp)throws Throwable { log.info("around ... before ..."); Object result = pjp.proceed(); log.info("around ... after ...");return result; }}
AOP
案例
将案例中增、删、改相关接口的操作日志记录到数据库中
// 实体类package com.itheima.pojo;import lombok.Data;import java.time.LocalDateTime;@DatapublicclassOperateLog{private Integer id; //IDprivate Integer operateEmpId; //操作人IDprivate LocalDateTime operateTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private String returnValue; //操作方法返回值private Long costTime; //操作耗时}
-- sql 数据表结构-- 操作日志表createtable operate_log(idintunsigned primary key auto_increment comment'ID', operate_emp_id intunsignedcomment'操作人ID', operate_time datetime comment'操作时间', class_name varchar(100) comment'操作的类名', method_name varchar(100) comment'操作的方法名', method_params varchar(2000) comment'方法参数', return_value varchar(2000) comment'返回值', cost_time bigintunsignedcomment'方法执行耗时, 单位:ms') comment'操作日志表';
// 自定义注解package com.itheima.anno;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interface Log {}
// aop 切面类package com.itheima.aop;import com.itheima.mapper.OperateLogMapper;import com.itheima.pojo.OperateLog;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.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.Arrays;@Slf4j@Aspect@ComponentpublicclassOperationLogAspect{@Autowiredprivate OperateLogMapper operateLogMapper;@Around("@annotation(com.itheima.anno.Log)")public Object logOperation(ProceedingJoinPoint pjp)throws Throwable {long startTime = System.currentTimeMillis(); // 获取开始时间 Object result = pjp.proceed(); // 开始执行long endTime = System.currentTimeMillis(); // 获取结束时间long costTime = endTime - startTime;// 构建日志实体 OperateLog olog = new OperateLog(); olog.setOperateEmpId(getCurrentUserId()); // 根据实际情况完成 olog.setOperateTime(java.time.LocalDateTime.now()); olog.setClassName(pjp.getTarget().getClass().getName()); olog.setMethodName(pjp.getSignature().getName()); olog.setMethodParams(Arrays.toString(pjp.getArgs())); olog.setReturnValue(result != null ? result.toString() : "void"); olog.setCostTime(costTime);// 保留日志 log.info("记录日志: {}", log); operateLogMapper.insert(olog);return result; }private Integer getCurrentUserId(){return1; }}
ThreadLocal
ThreadLocal
并不是一个Thread
,而是Thread
的局部变量。
ThreadLocal
为每个线程提供了一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰
-
ThreadLocal
常用方法
publicvoidset(T value)// 设置当前线程的线程局部变量的值public T get()// 返回当前线程所对应的线程局部变量的值publicvoidremove()// 移除当前线程的线程局部变量
测试方法
package com.itheima;publicclassThreadLocalTest{privatestatic ThreadLocal<String> local = new ThreadLocal<>();publicstaticvoidmain(String[] args){ local.set("Main Message"); System.out.println(Thread.currentThread().getName() + ":" + local.get()); local.remove(); System.out.println(Thread.currentThread().getName() + ":" + local.get()); }}
// 当前线程存的,只有当前线程可以 删 或 取package com.itheima;publicclassThreadLocalTest{privatestatic ThreadLocal<String> local = new ThreadLocal<>();publicstaticvoidmain(String[] args){ local.set("Main Message");// 创建线程new Thread(new Runnable(){@Overridepublicvoidrun(){ local.set("Sub Message"); System.out.println(Thread.currentThread().getName() + ":" + local.get()); } }).start(); System.out.println(Thread.currentThread().getName() + ":" + local.get()); local.remove(); System.out.println(Thread.currentThread().getName() + ":" + local.get()); }}
代码示例 - 获取当前登录员工
// 工具类package com.itheima.utils;publicclassCurrentHolder{privatestaticfinal ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();publicstaticvoidsetCurrentId(Integer employeeId){ CURRENT_LOCAL.set(employeeId); }publicstatic Integer getCurrentId(){return CURRENT_LOCAL.get(); }publicstaticvoidremove(){ CURRENT_LOCAL.remove(); }}
// 部分filter内容.......try { Claims claims = JwtUtils.parseToken(token); Integer empId = Integer.valueOf(claims.get("id").toString()); CurrentHolder.setCurrentId(empId); log.info("当前登录员工ID: {}, 将其存入ThreadLocal", empId); } catch (Exception e) { log.info("令牌非法, 响应401"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return; }........// 删除 ThreadLocal 中的数据 CurrentHolder.remove();
// aop类 中的 获取ID的方法名private Integer getCurrentUserId(){return CurrentHolder.getCurrentId(); }}
原文始发于微信公众号(夜风Sec):【Java学习】 - AOP基础&进阶&案例
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论