Spring-AOP
AOP(Aspect-Oriented Programming,面向切面编程,面向特定方法编程)
AOP是一种编程范式,它旨在解决软件开发中常见的横切关注点(cross-cutting concerns)问题。在传统的面向对象编程(OOP)中,横切关注点(如日志记录、性能监控、事务管理等)往往散布在多个类中,导致代码的重复和耦合度增加,维护成本变高。AOP的引入是为了将这些横切关注点从核心业务逻辑中分离出来,以模块化的方式处理,从而降低系统的复杂度和提高代码的可维护性。
为什么会有AOP?
随着软件系统变得越来越复杂,横切关注点的代码开始在多个模块中重复出现,这违反了DRY(Don't Repeat Yourself)原则。此外,这些关注点的代码往往与核心业务逻辑紧密耦合,修改或扩展这些关注点可能会影响业务逻辑的正确性。AOP的出现就是为了将这些关注点提取出来,封装成独立的组件,即切面(Aspects),以便在不影响业务逻辑的前提下,方便地添加、修改或删除这些功能。
AOP是用来解决什么问题的?
AOP主要用来解决以下几类问题:
代码重复:减少在多个类中重复编写相同功能代码的情况。
耦合度高:将横切关注点从业务逻辑中解耦,使得业务逻辑更加纯粹,也更易于理解和维护。
可维护性差:通过将横切关注点模块化,使得修改或添加此类功能变得更加容易和安全。
AOP如何解决问题?
- 实现:动态代理是面向切面编程最主流的实现
AOP通过以下几个关键概念来实现其目标:
Aspect(切面):封装了横切关注点的模块。一个切面可以包含多个通知(advice)和一个或多个切入点(pointcut)。
Joinpoint(连接点):程序执行过程中的某个点,如方法调用或异常抛出,这是切面可以插入的位置。
Pointcut(切入点):定义了切面在哪些连接点上应用的规则或条件。
Advice(通知):在切入点定义的连接点上执行的代码,它可以是前置通知(before)、后置通知(after)、环绕通知(around)等。
AOP框架(如Spring AOP)使用这些概念在运行时(或编译时,取决于AOP实现)将切面代码“织入”(weave)到应用程序的代码中,从而实现了横切关注点的模块化和解耦。
总结
AOP通过提供一种新的角度来组织代码,使得开发者可以将横切关注点与业务逻辑分离,从而简化了软件开发和维护。在Spring框架中,AOP的实现允许开发者通过XML配置、注解或Java配置来定义切面,进而实现对应用程序的增强。
主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。
简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。
应用场景
- 日志记录
- 事务管理
- 权限验证
- 性能监测
好文章:https://developer.aliyun.com/article/1357215
代码例子:
一、写一个检测类运行时间的功能
- 建包aop
- 建类TimeAspect
Component @Aspect @Slf4j public class TimeAspect { @Around("execution(* com.ljh.main.ScopeTask.Controller.*.*(..))") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object obj = joinPoint.proceed(); long end = System.currentTimeMillis(); log.info(joinPoint.getSignature()+"执行耗时:{}ms",end-start); return obj; } }
AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行的相关信息)
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:通知所应用的对象
AOP执行流程
- 一旦我们进行AOP开发,最终运行的不再是原始的目标对象,而是基于目标对象生成的代理对象
通知类型
- @Around,环绕通知,在目标方法前、后都被执行
- @Before:前置通知,在目标方法执行前被执行
- @After:后置通知,在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,在目标方法后执行,有异常不会执行
- @AfterThrowing:异常后通知,目标方法发生异常后执行
当切入点表达式存在大量重复一样代码时
- 抽取出来
@Pointcut("execution(...)") private void pt(){}
- 再在切面中把切入点表达式替换成"pt()"即可
通知执行顺序
- 通过注解@Order()
- 对于原始方法执行前的,括号中数字越小,则越先执行
- 对于原始方法执行后的,括号中数字越大,则越先执行
@annotation 写切入点表达式
- 先写注解类
@Retention(RetentionPolicy.RUMETIME) @Target(ElementType.METHOD) public @interface MyLog{ }
- 在相关业务方法上加上@MyLog
- 写切入点表达式@annotation(... .Mylog)
操作日志案例
- 导入aop依赖
- 创建数据库表
CREATE TABLE `operate_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `operate_user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `operate_time` datetime DEFAULT NULL, `class_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `method_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `method_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `return_value` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `cost_time` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
- 创建anno包,创建Log注解类
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Log { }
- mapper包
@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); }
- aop包
@Slf4j @Component @Aspect public class LogAspect { @Autowired private HttpServletRequest req; @Autowired private HttpServletResponse resp; @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(com.ljh.main.anno.Log)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable{ //获取当前登录的用户名,即操作人名,operateUser String operateUser = JWTUtils.getUsername(req, resp); //当前的操作时间 LocalDateTime operateTime = LocalDateTime.now(); //操作的类名 String className = joinPoint.getTarget().getClass().getName(); //操作的方法名 String methodName = joinPoint.getSignature().getName(); // 操作的参数 StringBuilder methodParamsBuilder = new StringBuilder(); Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { if (i > 0) { methodParamsBuilder.append(", "); } methodParamsBuilder.append(args[i].toString()); } // 将StringBuilder转换为String String methodParams = methodParamsBuilder.toString(); //原始方法运行开始之前时间 Long startTime = System.currentTimeMillis(); //调用原始目标方法运行 Object result = joinPoint.proceed(); //原始方法运行之后时间 Long endTime = System.currentTimeMillis(); //返回值 String returnValue = JSONObject.toJSONString(result); //操作耗时 Long costTime = endTime - startTime; //记录操作日志 OperateLog operateLog = new OperateLog(operateUser,operateTime,className,methodName,methodParams,returnValue,costTime); operateLogMapper.insert(operateLog); log.info("AOP记录操作日志:{}", operateLog); return result; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律