Spring中的AOP面向切面编程

 2023-02-15
原文作者:励志买套华侨城苏荷湾大平层 原文地址:https://juejin.cn/post/7096780005295783973

AOP 概述

  • AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态的角度考虑程序的运行过程。

    这是 Spring 框架中的一个重要的内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑之间的耦合度降低,提高程序的复用性,同时提高了开发效率。

  • AOP 底层,就是采用了动态代理的模式来实现的。 其中有两种代理:JDK 的动态代理、CGLIB 的动态代理

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面植入到主业务逻辑中。所谓的“交叉业务”,就是指通用的、与主业务逻辑无关的代码。例如:日志信息、安全检查、事务、缓存、设置字符编码、发短信 等。

若不使用 AOP 。则会出现代码纠缠不清。不重要的业务功能和重要的业务功能代码混杂咋一起,使得整个程序的结构混乱不清。

例如,转账功能。在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑。 而这些业务逻辑和主要的业务逻辑之间并没有直接的关系。但是他们的代码量能达到总代码量的一半甚至更多! 他们的存在,不仅产生了大量“冗余”的代码,还大大干扰了主业务逻辑的结构——转账

AOP 有什么好处?

  1. 减少重复
  2. 专注业务

注意:AOP 这是面向对象编程的一种补充

202301012021165331.png

不使用 AOP 的开发方式 (实例)

(待补充)

AOP 术语 (掌握)

切面(Aspect)

切面泛指交叉业务逻辑。上述的事务处理、日志功能等,就可以理解为是切面,常用的切面是通知(Advice)(也可以理解为切入到目标代码的时间点)。

实际上就是对主业务逻辑的一种增强

连接点(JoinPoint)

连接点指可以被切面植入的具体方法,通常业务接口中的方法均为连接点

切入点(Pointcut)

切入点指的是 声明的一个或者多个连接点的集合。通过切入点指定的 一组方法

注意: 被标记为 final 的方法是不能作为连接点与切入点的,因为 final 是不能被修改、不能被增强的、

目标对象(Target)

目标对象指的是要被增强的对象,即包含主业务逻辑的类的对象。例如 StudentServiceImpl 的对象若被增强,则该类被称为目标类,该对象被称为目标对象。 若不能增强也就无所谓目标不目标的

通知(Advice)

通知,表示切面的执行时间,Advice 也叫增强。例如:MyInvocationHandler 就可以理解为是一种通知。

换个角度来说, 通知定义了增强代码切入到目标代码的时间点: 是目标方法执行前执行、还是目标方法执行后执行……

通知类型不同,切入的时间也不同。 切入点定义了切入的位置,通知定义了切入的时间

AspectJ 对 AOP 的实现 (掌握)

步骤

定义业务接口类和实现类

    //接口类
    public interface SomService{
        void doSome(String name,int age);
    }
    ​
    //实现类
    public class SomeServiceImpl implements SomService{
        @Override
        public void doSome(String name,int age){
            System.out.priontln("执行了业务方法doSome");
        }
    }

定义切面类

    //类中定义了若干普通方法,用来增强功能
    //@Aspectj 是aspectj框架的注解,表示当前类是切面类
    @Aspect
    public class MyAspect{
        /*
        @Before:前置通知
        属性:value,值 = 切入点表达式,表示切面的执行位置
        位置:方法之上
        */
        public void myBefore(){
            System.out.println("前置通知:在目标方法之前执行,例如打印日志信息");
        }
    }

XML 文件的配置

在定义好切面 Aspect 后,要通知 Spring 容器,让容器生成 “目标类” + “切面” 的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,它就会自动扫描到 @Aspect 注解,并按照通知类型与切入点,将其植入,并生成代理

    <!-- 声明目标类对象 -->
    <bean id="someServiceTarget" class="com.gg.service.SomeServiceImpl" />
    <!-- 声明切面类对象 -->
    <bean id="myAspect" class="com.gg.aspect.MyAspect" />
    ​
    <!-- 注册AspectJ的自动代理 -->
    <!-- 声明自动代理生成器,创建代理 -->
    <aop:aspectj-autoproxy />

aop:aspectj-autoproxy 的底层是由 AnnotationAwareAspectJAutoProxyCreator 这个类实现的。

从名字就可以看出,是基于AspectJ 的注解适配自动代理生成器

它的工作原理是,aop:aspectj-autoproxy 通过扫描找到 @Aspect 定义的切面类,再 由切面类根据切入点表达式 找到目标类的目标方法,再由通知类型找到切入的时间点。

@Before 前置通知,方法有 JoinPoint 参数

被 @Before 标记的增强方法,在目标方法执行之前执行。被注接为前置通知的方法,可以包含一个 JoinPoint 类型的参数。该类型的对象本身,就是切入点表达式。通过这个参数,可以获取切入点表达式、方法签名、目标对象等。

补充: 不光是前置通知的方法可以包含 JoinPoint 类型的参数,所有的通知方法都可以包含 JoinPoint 这个参数

    /*
        1、通知方法:使用了通知注解修饰的方法
        2、通知方法可以有参数,但参数不是任意
        3、JoinPoint:表示连接点方法
           3.1、该参数只能出现在形参的第一位,其他形参可以跟在后面
           3.2、任何通知的方法中都可以包含该参数
    */
    ​
    @Before(value="execution(* *..SomeServiceImpl.do*(..))")
    public void myBefore(JoinPoint jp){
        
        //通过JoinPoint获取方法签名,方法定义,方法参数等
        System.out.println("连接点的方法定义" + jp.getSignature);
        System.out.println("连接点方法参数个数" + jp.getArgs().length);
        
        //获取方法参数信息
        Object[] args = jp.getArgs();
        for(Object arg:args){
            System.out.println(arg);
        }
        
        //切面代码功能,例如日志输出,事务处理
        System.out.println("前置通知:输出日志");
    }

@AfterReturning 后置通知 - 注解带有 returning 属性

被 @AfterReturning 注解标记的方法是后置通知,在目标方法执行之后执行。

由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。(此参数用于指定变量名,这个变量名就代表方法的返回值)

所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值参数的变量,并做出修改。 该变量最好使用 Object 类型 ,因为目标方法中的返回值可能是任何类型。

定义接口与实现类

    //新增接口中的方法
    public interface SomService{
        void doSome(String name,int age);
        String doOther(String name,int age); //新增的方法
    }
    ​
    //实现类
    public class SomeServiceImpl implements SomService{
        @Override
        public String doOther(String name,int age){
            System.out.priontln("执行了业务方法doOther");
            return "abcd";
        }
    }

定义切面类中方法

    @AfterReturning(value="execution(* *..SomeServiceImpl.doOther(..))",returning="result")
    public void myAfterReturning(){
        //修改目标方法中的执行结果
        if(result != null){ //此处的result就是returning属性的参数名(自定义参数名),result代表的是:目标方法的返回值,可能是任意类型
            String s = (String)result;
            result = s.toUpperCase();
        }
        System.out.println("后置通知:在目标方法执行之的功能增强,例如事务处理。" + result);
    }

@Around 环绕通知-增强法有 ProceedingJoinPoint 参数

被 @AroundProceedingJoinPoint 注解标记的增强方法, 在目标方法执行的前后均执行 。被注解为环绕通知的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 里面有一个 proceed() 方法,用于执行目标方法。

若目标方法有返回值,则该方法的返回值就是目标方法的返回值。

最后,环绕增强方法将其返回值返回,实际上就是拦截了目标方法的执行

定义接口与实现类

    //新增接口中的方法
    public interface SomService{
        void doSome(String name,int age);
        String doOther(String name,int age); 
        String doFirst(String name,ing age); //新增的方法
    }
    ​
    //实现类
    public class SomeServiceImpl implements SomService{
        @Override
        public String doFirst(String name,int age){
            System.out.priontln("执行了业务方法doFirst");
            return "doFirst";
        }
    }

定义切面

    @Around(value="execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
        Object obj = null;
        
        //增强功能
        System.out.println("环绕通知:在目标方法执行之前执行,例如输出日志");
        
        //执行目标方法的调用,等同于method.invoke(target,args)
        obj = pjp.proceed();
        
        //增强功能
        System.out.println("环绕通知:在目标方法之后执行的内容。如事务");
        
        return obj;
    }

@AfterThrowing 异常通知 - 注解中有 throwing 属性(了解内容)

被 @AfterThrowing 标记的方法,在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。

定义接口与实现类

    //新增接口中的方法
    public interface SomService{
        void doSome(String name,int age);
        void doAfterThrowing(); //新增的方法
    }
    ​
    //实现类
    public class SomeServiceImpl implements SomService{
        @Override
        public void doAfterThrowing(){
            System.out.println("执行了业务方法doAfterThrowing" + 10/0);
        }
    }

定义切面类

    @AfterThrowing(value="execution(* *..SomeServiceImpl.doAfterThrowing(..))",throwing="ex")
    public void myAfterThrowing(Throwable ex){
        //把异常发生的时间、位置、原因 记录到数据库,日志文件等地方
        //可以在异常发生时,把异常信息通过短信、邮件发送给开发人员
        System.out.println("异常通知:在目标方法抛出时异常执行,异常原因:" + ex.getMessage());
    }

@After 最终通知(了解内容)

无论目标方法是否抛出异常,该增强均会被执行

定义接口与实现类

    //新增接口中的方法
    public interface SomService{
        void doSome(String name,int age);
        void doAfter(); //新增的方法
    }
    ​
    //实现类
    public class SomeServiceImpl implements SomService{
        @Override
        public void doAfter(){
            System.out.println("执行了业务方法doAfter" + (10/0));
        }
    }

定义切面类

    @After(value="execution(* *..SomeService.doAfter(..))")
    public void myAfter(){
        System.out.println("最终通知:总是会被执行的方法");
    }

@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护都比较麻烦。AspectJ 提供了 @Pointcut 注解,用于定义 execution 切入点表达式。

用法:将 @Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性均可使用该方法名作为切入点。代表的就是 @Pointcut 定义的切入点。

这个使用 @Pointcut 注解的方法一般使用 private 的标识方法,即,没有实际作用的方法。

    @Pointcut(value="execution(* *..SomeService.doThird(..))")
    private void myPointcut{
        // 此工具方法内无需代码
    }
    ​
    @After(value="myPointcut()")
    public void myAfter(){
        System.out.println("最终通知:总是会被执行的方法");
    }