AOP详解

141 阅读6分钟

1. 什么是AOP

全称是 Aspect Oriented Programming 即:面向切面编程。是OOP的延续,也是Spring框架中的一个重要内容,是函数式编程的一种衍生泛型。简单的说他就是把我们程序重复的代码抽取出来,在需要执行的时候使用动态代理技术在不修改源码的基础上,对我们的已有方法进行增强。(AOP用于简化动态代理的使用)

2. AOP的作用

  • 作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
  • 优势:减少重复代码,提高开发效率,降低耦合度方便维护

3. AOP底层

实际上,Sping的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。

  • 常用的动态代理技术有
    • JDK的动态代理:基于接口实现的
    • cglib的动态代理:基于子类实现的
  • Spring的AOP采用了那种代理方式?
    • 如果目标对象有接口,就采用JDK的动态代理技术
    • 如果目标对象没有接口,就采用cglib技术

4. AOP相关概念

  • 目标对象(Target):要代理的/要增强的目标对象
  • 代理对象(Proxy):目标对象被AOP织入增强后,就得到了一个代理对象
  • 连接点(JoinPoint):能够被拦截的点,在Spring里指的是方法。(能增强的方法)
  • 切入点(PointCut):要对哪些连接点进行拦截的定义。(要增强的方法)
  • 通知/增强(Advice):拦截到连接点之后要做的事情。(要如何增强,额外添加上去的功能和能力)
  • 切面(Aspect):是切入点和通知的结合。(告诉Sping的AOP:要对那个方法,做什么样的增强)
  • 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程

5. AOP使用步骤:

  1. 添加依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  2. 编写切面类

    • 类上加@Aspect注解:声明一下这个类是切面类
    • 类上加@Component注解:标注一个类为Spring容器的Bean
    • 类里编写通知方法:要额外增加上去的功能
    • 方法上加注解:@通知类型("切入点表达式") 如:@Around("切入点表达式")

6. 通知类型

通知类型注解作用
前置通知@Before通知方法将在目标方法之前 先执行
后置通知@AfterReturning通知方法将在目标方法正常结束之后 再执行
异常通知@AfterThrowing通知方法将在目标方法抛出异常之后 再执行
最终通知@After通知方法将在目标方法之后 必定执行,无论有没有抛出异常
环绕通知@Around有最大自主性的通知方法,如何调用目标方法及如何增强,完全由我们自己决定

7. 切入点表达式

7.1 execution

用于根据方法的名称进行模糊匹配

语法:execution(权限修饰符 返回值类型 全限定类名.方法名(形参列表))

  • *:通配符来表示模糊匹配
  • ..:用于形参列表里,表示任意个任意形参;用于其他地方,表示任意层级的package包
  • 权限修饰符写public,通常省略不写

示例:

  • public void com.sdf.service.XxService.delete(Integer)
  • void com.sdf.service.XxService.delete(Integer)
  • * com.sdf.service.XxService.delete(Integer)
  • * com.sdf.service.*.delete(Integer)
  • * com.sdf..*.delete(Integer)
  • * com.sdf..*.*(Integer)
  • * com.sdf..*.*(Integer, String)
  • * com.sdf..*.*(Integer, *)
  • * com.sdf..*.*(..)

7.2 @annotation

根据注解选择要增强的方法

语法:@annotation(注解的全限定类名),会选择所有加了指定注解的方法,进行增强

使用步骤:

  1. 自定义注解
  2. 创建切面配置类
  3. 给要增强的方法上加自定义的注解

示例:

  1. 自定义注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnno {
    }
    
  2. 创建切面配置类

    @Aspect
    @Component
    public class Demo03Aspect {
    
        @Before("@annotation(com.sdf.aop.MyLog)")
        public void saveLogs(){
            System.out.println("========保存日志信息======");
        }
    }
    
  3. 给要增强的方法上加自定义的注解

     @MyAnno
     @Override
     public List<Dept> queryAllDepts() {
         return deptMapper.selectAll();
     }
    

8. 抽取公用切入点表达式

语法

  • 定义公用的切入点表达式:在切面类里创建一个方法,方法上加注解@Pointcut("公用的切入点表达式")
  • 引用公用的切入点表达式:
    • 完整用法:Before("com.sdf.aop.Demo02Aspect.pc()")
    • 简写形式:Before("pc()"),仅适合于 切入点方法 和 通知方法在同一个类里
@Aspect
@Component
public class Demo02Aspect {

    /**这个方法仅仅作为切入点表达式的载体*/
    @Pointcut("execution(* com.sdf.service.*.*(..))")
    public void pc(){}
    
    @Before("pc()")
    public void before(){
        System.out.println("=====Demo02Aspect.before执行了======");
    }
    @After("pc()")
    public void after(){
        System.out.println("=====Demo02Aspect.after执行了======");
    }
}

9. 通知执行顺序

image-20240319180643921.png

10. 示例练习

需求:

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

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

操作日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

分析:

使用AOP技术实现:

  • 如果不使用AOP,就需要修改源码,给大量的方法增加保存日志的代码。代码重复,日志和业务功能耦合到了一起
  • 使用AOP,可在不修改源码的情况下,给Service层方法进行增强。

首先要明确切入点和通知

  • 切入点:对那些方法增强

    哪个方法需要保存日志,就在哪个方法上加一个自定义注解

    使用注解切入点表达式@annotation(自定义注解)

  • 通知:

    通知方法:获取当前用户、当前时间、当前方法的类名和方法名、方法的实参、返回值,运行耗时,把这些信息保存到日志表。

    通知类型: 使用@Around环绕通知

实现:

1.自定义注解MyLog

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

2.给增删改查方法添加注解

image-20240319173129072.png

3.创建切面类

@Aspect
@Component
public class LogAspect {
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private OperateLogMapper operateLogMapper;

   @Around("@annotation(com.sdf.anno.MyLog)")
    public Object logAspect(ProceedingJoinPoint pjp) throws Throwable {

       long begin = System.currentTimeMillis();
       Object res = pjp.proceed();
       long end = System.currentTimeMillis();

       OperateLog log = new OperateLog();

       //获取当前用户
       //通过request对象后去token,解析token得到用户id
       String token = request.getHeader("token");
       Claims claims = JwtUtils.parseJWT(token);
       Integer userId = claims.get("id", Integer.class);
       log.setOperateUser(userId);
       // 当前时间、
       LocalDateTime now = LocalDateTime.now();
       log.setOperateTime(now);
       // 当前方法的类名
       String clazzName = pjp.getSignature().getDeclaringType().toString();
       log.setClassName(clazzName);
       //当前方法方法名、
       String methodName = pjp.getSignature().getName();
       log.setMethodName(methodName);
       // 方法的实参、
       String methodParams = pjp.getArgs().toString();
       log.setMethodParams(methodParams);
       // 返回值,
       String returnValue = res.toString();
       log.setReturnValue(returnValue);
       // 运行耗时,
       log.setCostTime(begin-end);
       operateLogMapper.insert(log);
       return res;

   }
}

通知方法里获取目标方法信息

不同通知方法里,获取被调用的目标方法:

  • 可以直接给通知方法,直接加形参JoinPoint
  • 如果是环绕通知方法,则要加形参ProceedingJoinPoint,继承了JoinPoint

JoinPoint常用方法:

  • getArgs():获取调用方法时的实参值
  • getTarget():获取被调用的目标对象(原始目标对象)
  • getThis():获取当前代理对象
  • getSignature():获取被调用的方法信息。Signature对象的方法:
    • getName():获取被调用的方法名。比如:queryAllDepts

    • getDeclaringType():获取被调用方法所属的类Class

    • getDeclaringTypeName():获取被调用方法所属的类的全限定类名

    • toLongString():获取方法的完整名称。

    • toShortString:获取方法的简短名称。

    • toString:获取方法信息。