目录
SpringAOP:
AOP快速入门:
AOP核心概念:
AOP进阶:
通知类型:
注意事项:
方法实现:
@PointCut
AOP通知顺序:
执行顺序:不同切面类中,默认按照切面类的类名字母排序。
用@Order(数字)加在切面类上来控制顺序
AOP切入点表达式:
切入点表达式-execution:
切入点表达式-@annotation:
可以使用通配符描述切入点:
AOP连接点:
AOP案例:
将案例中增,删,改相关接口的操作日志记录到数据库表中
ThreadLocal:
通过ThreadLocal动态获取当前登陆员工的信息:
ThreadLocal的应用场景:
AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),可简单理解为面向特定方法编程。
1.导入依赖:在pom.xml中引入AOP的依赖(基于AI)。
2.编写AOP程序:针对特定的方法根据业务需要进行编程。
方法实现:
@Slf4j
@Aspect //标识当前是一个AOP类,也叫切面类
@Component
public class RecordTimeAspect{
@Aroud("execution(*com.itheima.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp)throws Throwable{
//1.设置方法运行的开始时间
long begin = System.currentTimeMillis();
//2.执行原始的方法
Object result = pjp.proceed();
//3.记录方法运行的结束时间,记录时间
long end = System.currentTimeMillis();
//pjp.getSignature方法拿到方法签名就可以获得是哪个方法耗时
long.info("方法{}执行耗时:{}ms",pjp.getSignature(),end-begin);
return result;
}
}
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)。
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法),可以被AOP接管的方法。
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用,实际被AOP接管的方法。
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。
目的对象:Target,通知所应用的对象。
AOP执行流程:
动态代理为目标对象生成代理对象,调用代理对象方法。
1.@Around:环绕通知,此注解标注的通知方法在目标方法前,后都被执行。
2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行。
3.@After:后置通知,此注解标注的通知方法在目标方法后被执行,论有没有异常都会被执行。
4.@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
5.@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。
1.@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行。
2.@Around环绕通知方法的返回值,必须指定为Object,来接受原始方法的返回值。
@Slf4j
@Aspect
@Component
public class MyAspect1{
//前置通知 - 目标方法运行之前运行
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before(){
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.*.*(..))")
public void after(){
log.info("after....");
}
//返回后通知 - 目标方法运行之后运行,如果出现异常不会运行
@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")
public void afterReturning(){
log.info("afterReturning....");
}
//异常后通知 - 目标方法运行之后运行,只有出现异常才会运行
@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")
public void afterThrowing(){
log.info("afterThrowing ....");
}
}
该注解的作用是将公共的切点表达式抽取出来,需要时引用该切点表达式即可。
方法实现:
@Slf4j
@Aspect
@Component
public class MyAspect1{
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt(){}
//前置通知 - 目标方法运行之前运行
@Before("pt()")
public void before(){
log.info("before....");
}
//环绕通知 - 目标方法运行之前,后运行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("around...before....");
Object result = pjp.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 ....");
}
}
如果pt方法的权限修饰符为private,仅能在当前切面类中引入该表达式。
如果pt方法的权限修饰符为public,在其他外部的切面类中也可以引用该表达式。
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
目标方法前的通知方法:字母排名靠前的先执行。
目标方法后的通知方法:字母排名靠前的后执行。
目标方法前的通知方法:数字小的先执行。
目标方法后的通知方法:数字小的后执行。
介绍:描述切入点方法的一种表达式。
作用:用来决定项目中的哪些方法需要加入通知。
常见形式:
1.execution(...):根据方法的签名来匹配。
2.@annotation:根据注解匹配。
@Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint)
execution 主要根据方法的返回值,包名,类名,方法名,方法参数等信息来匹配,语法为:
execution(访问权限修饰符? 返回值 包名.类名?方法名(方法形参) throws 异常?)
其中带 ? 的表示可以省略的部分
1.访问修饰符:可省略(比如:public,protected)。
2.包名.类名:可省略。
3.throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)。
1. * :单个独立的任意符号,可以通配任意返回值,包名,类名,方法名,任意类型的一个参数,也可以统配包,类,方法名的一部分。
execution(*com.*.service.*.update*(*))
2. .. :多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数。
execution(*com.itheima.DeptService.*(..))
@annotation切入点表达式,用于匹配标识有特定注解的方法:
写一个注解类:
//方法上生效
@Target(ElementType.METHOD)
//运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}
copy注解类的全类名,写在切入点表达式里面:
//把全类名写在后面
@Before(@annotation(com.itheima.anno.LogOperation))
//底下再写方法
然后在每个方法上加入注解@LogOperation类名即可使用通知。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如方法名,目标类名,方法参数等。
对@Around通知,获取连接点信息只能使用ProceedingJoinPoint。
对于其他四种通知,获取连接点信息只能使用JointPoint,它是ProceedingJoinPoint的父类型。
环绕通知:
@Around("execution(*com.iteheima.service.impl.DeptService.*(..))")
public Object around(ProccedingJoinPoint 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(JoinPont joinPoint){
//获取目标类名
String className = joinPoint.getTarget().getClass().getName();
//获取目标方法签名
Signature signature = joinPoint.getSignature();
//获取目标方法名
String methodName = joinPoint.getSignature().getName();
//获取目标方法运行参数
Object[] args = joinPoint.getArgs();
}
由于方法太多,选择annotation写切入点表达式:
1.引入AOP的依赖(AI生成)。
2.定义实体类OperateLog:
@Data
public class OperateLog{
private Integer id; //ID
private Integer operateEmpId //操作人ID
private LocalDateTime operateTime; //操作类时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
3.创建日志表结构:
create table operate_log(
id int unsigned primary key auto_increment comment'ID',
operate_emp_id int unsigned comment'操作人ID',
operate_time datatime comment'操作时间',
class_name varchar(100) comment'操作的类名',
method_name varchar(200) comment'操作的方法名',
method_params vachar(2000) comment''方法参数,
return_value varchar(2000) comment'返回值',
cost_time bigint unsigned comment'方法执行耗时,单位ms'
)comment'操作日志表';
4.定义一个注解类:
@Target(ElementType.METHOD)
@Retention(RenentionPolicy.RUNTIME)
public @interface Log{
}
5.Mapper层插入日志数据:
@Mapper
public interface OperateLogMapper{
//插入日志信息
@Insert("insert into operate_log(operate_emp_id,operate_time,class_name,method_name,method_params,return_values)"values (#{operateEmpId},#{operateTime},#{className},#{methodName},#{methodParams},#{returnValues}))
public void insert(OperateLog log);
}
6.在切面类中写环绕通知,切面类中写方法直接操作数据库:
@Slf4j
@Aspect
@Component
public class OperationLogAspect{
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint)throws Throwable{
long startTime = System.currenetTimeMillis();
//执行目标方法
Object result = joinPoint.proceed();
//计算耗时
long endTime = System.currentTimeMills();
long costTime = endTime - startTime;
//构建日志实体,因为log重名所以需要加个o区分
OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentUserId());//这里需要根据实际情况获取当前用户ID
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);
//保存日志
log.info("记录操作日志:{}",olog);
operateLogMapper.insert(olog);
return result;
}
}
然后在执行通知的方法上加上@Log。
ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同线程之间不会相互干扰
ThreadLocal常用方法:
public void set(T value) //设置当前线程的线程局部变量
public T get() //返回当前线程所对应的线程局部变量的值
public void remove() //移除当前线程的线程局部变量
获取请求头的jwt令牌,解析jwt令牌:
具体操作步骤:
1.定义ThreaLocal操作的工具类,用于操作当前登陆员工ID:
public class CurrentHolder{
private static final ThreadLocal CYRRENT_LOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId){
CURRENT_LOCAL.set(employeeId);
}
public static Integer getCurrentId(){
return CURRENT_LOCAL.get();
}
public static viud remove(){
CURRENT_LOCAL。remove();
}
}
2.在TokenFilter中,解析完当前登录员工ID,将其存入ThreadLocal(用完将其删除):
@Slf4j
public class TokenFilter implments Filter{
@Override
public void doFilter(ServletResquest servletRequest,ServletResponse servletResponse,Filterchain Filterchain)throws IOException, ServletException{
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpservletResponse) servletResponse;
//1.获取到请求操作
String requestURI = request.getRequestURI();
//2.判断是否是登录请求,如果路径中包含/login,说明是登陆操作,放行
if(requestURI.contain("/login")){
log.info("登录请求,放行");
filterChain.doFilter(request,response);
return;
}
//3.获取请求头中的token
String token = request.getHeader("token");
//4.判断token是否存在,如果不存在,说明用户没有登陆,返回错误信息(响应401状态码)
if(token == null || token.isEmpty()){
log.info("令牌为空,响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
//5.如果token存在,校验令牌,如果校验失败 -> 返回错误信息(响应401状态码)
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);
rturn;
}
//6.校验通过,放行
log.info("令牌合法,放行");
//7.删除ThreadLocal中的数据
CurrentHolder.remove();
}
}
3.在AOP程序中,从ThreadLocal中获取当前登陆员工的ID:
@Slf4j
@Aspect
@Component
public class OperationLogAspect{
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint)throws Throwable{
long startTime = System.currenetTimeMillis();
//执行目标方法
Object result = joinPoint.proceed();
//计算耗时
long endTime = System.currentTimeMills();
long costTime = endTime - startTime;
//构建日志实体,因为log重名所以需要加个o区分
OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentUserId());//这里需要根据实际情况获取当前用户ID
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);
//保存日志
log.info("记录操作日志:{}",olog);
operateLogMapper.insert(olog);
return result;
}
private Integer getCurrentUserId(){
return CurrentHolder.getCurrentId();
}
}
在同一个线程/同一个请求中,进行数据共享。