SpringAOP-基本概念-AOP入门程序-核心概念-通知类型-通知顺序-切入点表达式-连接点joinpoint-记录操作日志-获取当前登录员工

目录

SpringAOP:

AOP快速入门:

AOP核心概念:

AOP进阶:

通知类型:

注意事项:

方法实现:

@PointCut

AOP通知顺序:

执行顺序:不同切面类中,默认按照切面类的类名字母排序。

用@Order(数字)加在切面类上来控制顺序

AOP切入点表达式:

切入点表达式-execution:

切入点表达式-@annotation:

可以使用通配符描述切入点:

AOP连接点:

AOP案例:

将案例中增,删,改相关接口的操作日志记录到数据库表中

ThreadLocal:

通过ThreadLocal动态获取当前登陆员工的信息:

ThreadLocal的应用场景:


SpringAOP:

AOP快速入门:

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;
}
}

AOP核心概念:

连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)。

通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法),可以被AOP接管的方法。

切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用,实际被AOP接管的方法。

切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。

目的对象:Target,通知所应用的对象。

AOP执行流程:

动态代理为目标对象生成代理对象,调用代理对象方法。

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 ....");
}
}

@PointCut

该注解的作用是将公共的切点表达式抽取出来,需要时引用该切点表达式即可。

方法实现:

@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,在其他外部的切面类中也可以引用该表达式。

AOP通知顺序:

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

执行顺序:
不同切面类中,默认按照切面类的类名字母排序。

目标方法前的通知方法:字母排名靠前的先执行。

目标方法后的通知方法:字母排名靠前的后执行。

用@Order(数字)加在切面类上来控制顺序

目标方法前的通知方法:数字小的先执行。

目标方法后的通知方法:数字小的后执行。

AOP切入点表达式:

介绍:描述切入点方法的一种表达式。

作用:用来决定项目中的哪些方法需要加入通知。

常见形式:

1.execution(...):根据方法的签名来匹配。

2.@annotation:根据注解匹配。

切入点表达式-execution:

@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:

@annotation切入点表达式,用于匹配标识有特定注解的方法:

写一个注解类:

//方法上生效
@Target(ElementType.METHOD)
//运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}


copy注解类的全类名,写在切入点表达式里面:

//把全类名写在后面
@Before(@annotation(com.itheima.anno.LogOperation))
//底下再写方法

然后在每个方法上加入注解@LogOperation类名即可使用通知。

AOP连接点:

在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();
}

AOP案例:

将案例中增,删,改相关接口的操作日志记录到数据库表中:

由于方法太多,选择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为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同线程之间不会相互干扰

ThreadLocal常用方法:

public void set(T value)       //设置当前线程的线程局部变量

public T get()                       //返回当前线程所对应的线程局部变量的值

public void remove()           //移除当前线程的线程局部变量

通过ThreadLocal动态获取当前登陆员工的信息:

获取请求头的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();
}
}

ThreadLocal的应用场景:

在同一个线程/同一个请求中,进行数据共享。

你可能感兴趣的:(java,开发语言,spring,mvc,数据库)