【Java学习】 - AOP基础&进阶&案例

admin 2025年4月8日17:03:42评论5 views字数 10824阅读36分4秒阅读模式
【Java学习】 - AOP基础&进阶&案例
AOP

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,通知所应用的对象
【Java学习】 - AOP基础&进阶&案例
aop00

AOP执行流程

SpringAOP底层基于动态代理 -> 为目标对象生成代理对象 -> 代理对象与目标对象实现统一接口 -> 代理对象内实现 【 通知 和 执行原始方法 】 的逻辑运行逻辑:( 原始方法前后就是 通知 的内容 ),( 切入点表达式决定了哪些作为 目标对象 ) 运行 -> 到切面类 -> 到原始方法 -> 到切面类

AOP进阶

通知类型

  1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  2. @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After:后置通知,此注解标注的通知方法在目标方法后被执行、无论是否有异常都会执行
  4. @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会被执行
  5. @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
【Java学习】 - AOP基础&进阶&案例
aop01
在服务类中给出 1/0 的异常 -> 进行针对 @AfterThrowing 的测试
【Java学习】 - AOP基础&进阶&案例
aop02

切入点表达式优化

@PointCut:将公共的切点表达式抽取出来,需要用到时引用该切点表达式

@Pointcut("execution(* com.itheima.service.impl.*.*(..))")publicvoidpt(){}// 返回值:// private:仅在当前切面类// public:在其他类中也可用
【Java学习】 - AOP基础&进阶&案例
aop03

通知顺序

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

执行顺序:

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

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行
【Java学习】 - AOP基础&进阶&案例
aop04

@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 ...");    }}
【Java学习】 - AOP基础&进阶&案例
aop05

切入点表达式

描述切入点方法的一种表达式

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

常见形式:

  1. execution(返回值类型 包名.类名.方法名(参数列表)):根据方法的签名来匹配
  2. @annotation(全限定注解类名):根据注解匹配

execution

execution( <访问修饰符> 返回值 <包名.类名.>方法名(方法参数) <throws 异常> )
  • 访问修饰符、包名.类名、throws 异常 可以省略 -> 不建议 包名.类名 省略
  • 可以使用通配符描述切入点
  1. *:单个独立的任意符号,可以通配任意一个参数,也可以通配其中的一部分
  2. ..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
@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);}
【Java学习】 - AOP基础&进阶&案例
aop06

连接点

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;    }}
【Java学习】 - AOP基础&进阶&案例
aop07

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(100comment'操作的类名',    method_name varchar(100comment'操作的方法名',    method_params varchar(2000comment'方法参数',    return_value varchar(2000comment'返回值',    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());    }}
【Java学习】 - AOP基础&进阶&案例
aop08
// 当前线程存的,只有当前线程可以 删 或 取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());    }}
【Java学习】 - AOP基础&进阶&案例
aop09

代码示例 - 获取当前登录员工

// 工具类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();    }}
【Java学习】 - AOP基础&进阶&案例
aop10

原文始发于微信公众号(夜风Sec):【Java学习】 - AOP基础&进阶&案例

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月8日17:03:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【Java学习】 - AOP基础&进阶&案例https://cn-sec.com/archives/3930389.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息