Spring中的AOP面向切面编程

1,104 阅读9分钟

AOP 概述

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

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

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

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

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

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

AOP 有什么好处?

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

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

aop01.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("最终通知:总是会被执行的方法");
}