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;
}
}
posted @   jhhhred  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示