当前位置: 首页 > news >正文

通知+注解的切点使用+AOP案例

通知+注解的切点使用+AOP案例(2.25)

一、通知

1.通知的类型:

环绕通知:Around

前置通知:Before

后置通知:AfterReturning

异常通知:AfterThrowing

最终通知:After

2.代码实战:

CarController:

@RestController
@RequestMapping("/car")
public class CarController {@Autowiredprivate CarService carService;@GetMapping("/all")@MyLogpublic List<Car> listGet() {List<Car> carList = null;//出现异常,后面不执行//System.out.println(1/0);carList = carService.findAll();System.out.println("carList:"+carList);return carList;}}

aop.TimeAspect:

@Component
@Aspect //当前类是个切面
public class TimeAspect {//1 切点逻辑 哪些类的哪些方法//execution([访问修饰符] 返回值类型 包名.类名.方法名(参数类型) [异常])//      任意返回值类型  包名.类名.方法名@Pointcut("execution(* cn.wolfcode.controller.*.*(..))")public void pointcut(){}//2 通知方法 Around 表示通知方式//@Around("pointcut()")public Object around(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();Object result = null; //目标方法调用try {//1.前置通知 beforepjp.proceed();//2.后置通知 afterreturning} catch (Throwable e) {e.printStackTrace();//3.异常通知 afterThrowing} finally {//4.最终通知 after}long end = System.currentTimeMillis();System.out.println("耗时:" + (end-start) + "ms");return result;}//@Before("pointcut()")public void before(JoinPoint joinPoint) {System.out.println("======" + joinPoint.getSignature());}//@AfterReturning(pointcut = "pointcut()", returning = "result")public void afterReturning(JoinPoint joinPoint, Object result) {System.out.println("======" + joinPoint.getSignature());System.out.println("======" + result);}//@AfterThrowing(pointcut = "pointcut()", throwing = "e")public void afterThrowing(JoinPoint joinPoint, Throwable e) {System.out.println("======" + e.getMessage());}//@After("pointcut()")public void after(JoinPoint joinPoint) {System.out.println("======" + joinPoint.getSignature());System.out.println("======" + joinPoint.getTarget());}
}

BoosApplicationTests:

@SpringBootTest
class BoosApplicationTests {@Autowiredprivate CarController carController;@Testvoid contextLoads() throws SQLException, FileNotFoundException {List<Car> carList = carController.listGet();}
}

(1)before通知:目标方法执行前执行

应用:参数校验、日志记录、权限检查

image-20260304153646890

(2)afterReturning:目标方法正常返回后执行,有异常不显示

应用:处理返回值、记录成功日志

例如在CarController中手写一个异常,就不会执行:

 public List<Car> listGet() {List<Car> carList = null;//出现异常,后面不执行System.out.println(1/0);carList = carService.findAll();System.out.println("carList:"+carList);return carList;}

image-20260304154637348

(3)afterThrowing:目标方法抛出异常后

应用:处理异常、记录错误日志

①若有异常:显示异常信息

image-20260304160829230

②删除异常后,则正常执行:

image-20260304161128171

(4)after:目标方法执行后(无论成功或失败)

应用:资源释放、清理工作

有没有异常都显示,例如手写一个异常后

image-20260304155435313

为什么异常信息在下面?

这是因为标准输出流(stdout)和标准错误流(stderr)的输出顺序是独立的,它们在控制台的显示顺序不一定和代码执行顺序一致。

(5)around:包裹目标方法的整个执行过程

应用:性能统计、事务控制、缓存控制

总结:

@Around 能实现 @Before@AfterReturning@AfterThrowing@After 的所有功能,是功能最完整的通知类型;

3.切面类的执行顺序:

①单个切面内不同通知类型的执行顺序:

  • 目标方法正常执行(无异常)

@Around(执行前逻辑)→ @Before → 目标方法执行 → @Around(执行后逻辑)→ @AfterReturning → @After

  • 目标方法抛出异常

@Around(执行前逻辑)→ @Before → 目标方法执行(抛异常)→ @Around(异常捕获逻辑)→ @AfterThrowing → @After

例如:

// 无异常时控制台输出顺序
Around前置:开始计时
Before:打印方法签名
目标方法:执行list()
Around后置:计算耗时
AfterReturning:打印返回值
After:打印目标对象// 有异常时控制台输出顺序
Around前置:开始计时
Before:打印方法签名
目标方法:执行list()(抛异常)
Around异常:捕获并打印异常
AfterThrowing:打印异常信息
After:打印目标对象

多个切面类之间的执行顺序:

当项目中有多个切面类(比如 LogAspect、TimeAspect、AuthAspect)时,默认执行顺序是不确定的(Spring 按 Bean 加载顺序执行),需要手动指定,有两种方式:

  • 使用 @Order 注解

例如:在切面类上添加@Order(数字),数字越小,切面越先执行:

@Order(1)
@Component
@Aspect
public class AuthAspect { // 权限校验切面@Before("execution(* cn.wolfcode.controller.*.*(..))")public void before() {System.out.println("1. 权限校验");}
}// 后执行
@Order(2)
@Component
@Aspect
public class LogAspect { // 日志记录切面@Before("execution(* cn.wolfcode.controller.*.*(..))")public void before() {System.out.println("2. 日志记录");}
}
  • 实现 Ordered 接口

例如:切面类实现Ordered接口,重写getOrder()方法返回数字(数字越小越先执行):

@Component
@Aspect
public class TimeAspect implements Ordered {@Overridepublic int getOrder() {return 3; // 最后执行}@Before("execution(* cn.wolfcode.controller.*.*(..))")public void before() {System.out.println("3. 耗时统计");}
}

多个切面的完整执行逻辑(无异常):

假设有 2 个切面:AuthAspect(@Order (1))、LogAspect(@Order (2)),每个切面都有 @Before 和 @After

AuthAspect@Before → LogAspect@Before → 目标方法 → LogAspect@After → AuthAspect@After

类似 “洋葱模型”:外层切面先执行前置,后执行后置

二、注解的切点使用:

1.当 AOP 通知需要作用于 controller 层全部方法时,可使用基于 execution 表达式的方式实现;

2.若想精准控制 AOP 通知作用于某一层的某个方法,或不同层的某些指定方法,就需要采用基于自定义注解的方式来实现。

3.代码实战:

①自定义注解annotation.MyLog:

@Target(ElementType.METHOD) //修饰方法 
@Retention(RetentionPolicy.RUNTIME) //运行时
@Documented //日志
public @interface MyLog {
}

②在CarController的listGet方法上贴上@MyLog注解:

@RestController
@RequestMapping("/car")
public class CarController {@Autowiredprivate CarService carService;@GetMapping("/all")@MyLogpublic List<Car> listGet() {List<Car> carList = null;//System.out.println(1/0);carList = carService.findAll();System.out.println("carList:"+carList);return carList;}
}

③修改aop.TimeAspect:

@Component
@Aspect //当前类是个切面
public class TimeAspect {// 1. 封装注解切点@Pointcut("@annotation(cn.wolfcode.annotation.MyLog)")public void pointcut(){}// 2. 通知绑定封装后的切点@Before("pointcut()")public void before1(JoinPoint joinPoint) {System.out.println("===MyLog===" + joinPoint.getSignature());}
}

执行结果:

image-20260304171448181

三、AOP案例(日志):

1.日志:

  • 系统日志:程序在运行过程中自己产生的数据 --->修复bug --->写在控制台日志文件
  • 业务日志:用户行为产生的日志 --->DML操作都需要写日志,重要的查询、登录、登出也要写日志 --->存储在数据库表

无论哪种日志,都会只增加不减少,只有insert和select,不会有update和delete,因为用户的行为是不可变的

2.需求:

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

  • 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。

操作日志信息包含:

  • 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

所记录的日志信息包括当前接口的操作人是谁操作的,什么时间点操作的,以及访问的是哪个类当中的哪个方法,在访问这个方法的时候传入进来的参数是什么,访问这个方法最终拿到的返回值是什么,以及整个接口方法的运行时长是多长时间。

3.分析:

可以使用AOP解决 ---> 环绕通知 ---> 使用annotation来描述表达式

4.代码实战:

①AOP起步依赖:

<!--AOP起步依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.46</version>
</dependency>

我们的项目中已经引入了Aop依赖,所以不必重新引入。

image-20260304231806087

②导入数据库表结构

create table operate_log(id bigint primary key auto_increment        comment 'ID',operate_user bigint     					comment '操作人',operate_time timestamp						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 Long id; //主键IDprivate Long operateUser; //操作人IDprivate Date operateTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private Object returnValue; //操作方法返回值private Long costTime; //操作耗时
}

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

⑤自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
}

⑥定义切面类:

@Component
@Aspect
public class LogAspect {@Autowiredprivate OperateLogMapper operateLogMapper;@Autowiredprivate HttpSession session;@Around("@annotation(cn.wolfcode.annotation.MyLog)")public Object saveLog(ProceedingJoinPoint joinPoint) throws Throwable {Employee user = (Employee)session.getAttribute("user");if(StringUtils.isEmpty(user)){throw new RuntimeException("请登录");}Object ret = null;try {long start = System.currentTimeMillis();ret = joinPoint.proceed();long end = System.currentTimeMillis();//保存日志OperateLog log = new OperateLog();//log.setId();log.setOperateUser(user.getId()); //登录用户的idlog.setOperateTime(new Date()); //当前系统时间log.setClassName(joinPoint.getTarget().getClass().getName()); //类的名称log.setMethodName(joinPoint.getSignature().getName()); //方法名称log.setMethodParams(Arrays.toString(joinPoint.getArgs())); //参数名称log.setReturnValue(ret); //方法返回值log.setCostTime(end-start); //操作耗时operateLogMapper.insert(log);} catch (Throwable e) {e.printStackTrace();}return ret;}
}

⑦贴注解:

在DepartmentServiceImpl中的添加、更新、删除方法上贴@MyLog

在EmployeeServiceImpl中的保存方法上贴@MyLog

小结:@MyLog也可以贴在类上,不过要进行修改

第一步:

修改 @MyLog 注解的 @Target,增加 ElementType.TYPE(允许修饰类 / 接口)

@Target({ElementType.METHOD, ElementType.TYPE}) // 同时支持方法、类

第二步:

调整 AOP 切面的切点,支持 类上有 @MyLog”或 方法上有 @MyLog

@Before("@within(cn.wolfcode.annotation.MyLog) || @annotation(cn.wolfcode.annotation.MyLog)")

第三步:

@MyLog 贴在实现类上,该类所有方法都会被拦截

四、Aop案例(统一异常处理)

①定义切面类ExceptionAspect:

@Component
@Aspect
public class ExceptionAspect {@Around("@annotation(cn.wolfcode.annotation.MyExceptionAdvice)")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object ret = null;try {ret = joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();return Result.fail(e.getMessage());}return ret;}}

②自定义注解MyExceptionAdvice:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExceptionAdvice {}

③贴注解:

在DepartmentController的saveOrUpdate()方法上贴@MyExceptionAdvice

 @PostMapping("/saveOrUpdate")@ResponseBody@RequireLogin@MyExceptionAdvicepublic Result saveOrUpdate(@RequestBody Department department){/* try {if(StringUtils.isEmpty(department.getId())){departmentService.save(department);}else {departmentService.update(department);}} catch (Exception e) {e.printStackTrace();return Result.fail(e.getMessage());}return Result.success();*/if(StringUtils.isEmpty(department.getId())){//System.out.println(1/0);departmentService.save(department);}else {departmentService.update(department);}return Result.success();}

在DepartmentController的delete()方法上贴@MyExceptionAdvice

@GetMapping("/delete")@ResponseBody@RequireLogin@MyExceptionAdvicepublic Result delete(Long id){/*    try {departmentService.delete(id);} catch (Exception e) {e.printStackTrace();return Result.fail(e.getMessage());}return Result.success();*/departmentService.delete(id);return Result.success();}
http://www.jsqmd.com/news/437364/

相关文章:

  • WinCHM Pro学习指南:如何合法使用试用版进行帮助文件创作(含官方下载链接)
  • Matlab AppDesigner实战:跨应用数据交互的优雅实现
  • 77GHz毫米波雷达在自动驾驶中的实战应用:从原理到代码实现
  • Qwen3-0.6B-FP8集成MySQL安装配置教程:自动化数据库部署与对话日志存储
  • 安卓开发避坑指南:如何在不同机型上统一显示最近任务栏应用名称(附完整代码)
  • 03-N8N教程-基于Docker与PostgreSQL的N8N高可用部署指南:从零搭建到性能优化
  • ZW3D二次开发_cvxEntGetAngle_获取两个实体间的角度
  • 互联网大厂 Java 核心面试题库(金三银四面试必备)
  • 【Makefile函数实战】5个高频函数解决工程编译难题
  • 收藏必备!小白程序员也能看懂的大模型自我进化秘籍:MEMRL框架深度解析
  • VS2019 + Xamarin实战:C#开发者如何快速上手Android App开发(附Genymotion配置技巧)
  • LiuJuan20260223Zimage重装系统后的恢复部署教程:环境快速重建
  • Linux下myBase安装避坑指南:解决xcb插件报错与试用期限制
  • Docusaurus + GitHub Pages:零基础打造极简个人技术博客
  • RP2040嵌入式八大外设速通:GPIO/PWM/ADC/IRQ/TIMER/UART/USB/双核
  • 别再手动敲空格了!用Word制表位3分钟搞定整齐的论文封面下划线
  • 使用UltraISO制作TranslateGemma离线安装U盘
  • 音乐分类系统开发环境搭建:Ubuntu系统配置指南
  • YOLO12边缘部署指南:树莓派5实时目标检测实战
  • IBM开源时间序列预测神器:Granite FlowState R1在温度监测场景中的应用
  • 深入 NEURAL MASK 模型内部:通过 C++ 文件读写操作进行中间特征可视化
  • Qwen-Image-2512-Pixel-Art-LoRA部署案例:魔搭社区开发者如何15秒加载模型至显存
  • ShardingSphere与达梦数据库分表实战:从配置到性能优化
  • Matlab二值图像骨架提取避坑指南:如何消除毛刺和优化结果
  • DeepSeek-OCR快速上手:Streamlit非对称界面三视图(预览/源码/骨架)操作指南
  • 边缘设备也能跑大模型?腾讯混元1.8B轻量化部署实战
  • ChatGLM3-6B-128K一文详解:Ollama环境中的位置编码机制、训练策略与推理表现
  • hot 100 第三十八题 39.二叉树的直径
  • 企业AI Agent的图神经网络在组织网络分析与优化中的应用
  • 海思SS528(22AP30)DVR芯片深度解析:多路编解码与智能分析实战指南