SSM-回炉重造-Spring AOP

AOP

AOP:Aspect Oriented Programming: 面向切面编程

在程序运行期间,将某段代码 动态的切入指定方法指定位置进行运行(不侵入原有代码)

动态代理创建使用时复杂,且如果目标对象没有实现任何借口,则不能创建代理对象

SpringAOP底层也是使用的动态代理

常见术语

  • 横切关注点
  • 通知方法
  • 切面类
  • 连接点
  • 切入点

    众多连接点中我们感兴趣的地方就是切入点

  • 切入点表达式

Spring中使用AOP

注解实现步骤

  1. 导入SpringAOP的包
  2. 将切面类加入ioc容器,并且添加@Aspect注解

    @Aspect
    @Component
    public class LogUtils {
        ...
    }
  3. 使用注解告诉Spring 什么方法在是么时候运行

    • @Before: 目标方法运行前 (前置通知)
    • @After: 目标方法运行后 (后置通知)
    • @AfterReturning: 目标方法正常返回之后 (返回通知)
    • @AfterThrowing: 目标方法抛异常之后 (异常通知)
    • @Around: 环绕 (环绕通知)
  4. 写切入点表达式

    @Aspect
    @Component
    public class LogUtils {
    
        @Before("execution(public int com.oylong.impl.MathCalculator.*(int, int))")
        public static void logStart(){
            System.out.println("日志记录开始");
        }
    
    
        @AfterReturning("execution(public int com.oylong.impl.MathCalculator.*(int, int))")
        public static void logReturn(){
            System.out.println("方法正常执行完成");
        }
    
        @AfterThrowing("execution(public int com.oylong.impl.MathCalculator.*(int, int))")
        public static void logThrowing(){
            System.out.println("方法执行抛出异常");
        }
    
        @After("execution(public int com.oylong.impl.MathCalculator.*(int, int))")
        public static void logEnd(){
            System.out.println("日志记录结束");
        }
    }
    
  5. 在配置中开启基于注解的aop

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
        <context:component-scan base-package="com.oylong"></context:component-scan>
    </beans>
    

    添加aop:aspectj-autoproxy这个标签

  6. 通过接口拿到对象

    public class AOPTest {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
    
        @Test
        public void test(){
            Calculator calculator = ioc.getBean(Calculator.class);
    
            System.out.println(calculator.add(1, 2));
        }
    }

    输出:

    日志记录开始
    日志记录结束
    方法正常执行完成
    3

注意事项

  • 使用aop时,获取ioc容器中的对象需要使用其接口类型获取对象
  • 获取的对象实际上是一个代理对象,如下代码:

    @Test
    public void test(){
        Calculator calculator = ioc.getBean(Calculator.class);
    
        System.out.println(calculator.getClass());
    }

    返回:

    class com.sun.proxy.$Proxy12
  • 也可以通过id去获取bean对象

    @Test
    public void test(){
        Calculator calculator = (Calculator) ioc.getBean("mathCalculator");
    
        calculator.add(1, 2);
    
        System.out.println(calculator.getClass());
    }
    日志记录开始
    日志记录结束
    方法正常执行完成
    class com.sun.proxy.$Proxy12

无实现接口类使用AOP

如下:

package com.oylong.impl;

import com.oylong.inter.Calculator;
import org.springframework.stereotype.Component;

/**
 * @ProjectName: springaop
 * @Description:
 * @Author: OyLong
 * @Date: 2020/9/15 15:41
 */
@Component
public class MathCalculator/* implements Calculator*/ {
    //    @Override
    public int add(int a, int b) {
        return a+b;
    }

    //    @Override
    public int sub(int a, int b) {
        return a-b;
    }

    //    @Override
    public int mul(int a, int b) {
        return a*b;
    }

    //    @Override
    public int div(int a, int b) {
        return a/b;
    }
}

直接获取其实现类即可

@Test
public void test01() {
    MathCalculator bean = ioc.getBean(MathCalculator.class);

    bean.add(1, 2);

    System.out.println(bean.getClass());
}

返回结果:

日志记录开始
日志记录结束
方法正常执行完成
class com.oylong.impl.MathCalculator$$EnhancerByCGLIB$$29873912

可以看到,aop功能实现了,同时由CGLib去帮我们创建了代理对象

总结: 有实现接口时,通过实现接口获取bean对象,没有实现接口则可以直接通过类获取bean对象

切入点表达式

固定格式: execution(访问限定符 返回值类型 方法全类名(参数列表))

通配符:

  1. *

    • 匹配一个或者多个字符
    • 匹配任意一个参数,如下:

      @Before("execution(public int com.oylong.impl.MathCalcul*.*(int, *))")
      public static void logStart(){
      System.out.println("日志记录开始");
      }

      当函数重载时可用

    • 匹配一层路径
    • 在权限位置不能写*(直接留空)
    • 可表示任意返回值类型
    • 可用一个*表示所有路径
  2. ..

    • 匹配任意多个参数,任意类型参数,比如:

      @Before("execution(public int com.oylong.impl.MathCalcul*.*(..))")
      public static void logStart(){
          System.out.println("日志记录开始");
      }

      上面这个表达式表示对应所有类型参数的方法

    • 匹配多层路径

      @AfterReturning("execution(public int com..MathCalculator.*(int, int))")
      public static void logReturn(){
          System.out.println("方法正常执行完成");
      }
  3. 最模糊与最精确

    • 最模糊的:execution(* *.*(..)) 但是不能再实际生产使用
    • 最精确的:execution(public int com.oylong.impl.MathCalculator.add(int, int))
  4. &&

    同时满足多个表达式

  5. ||

    满足任意一个

  6. !

    不满足

通知方法执行顺序

  1. 正常执行

    • @Before
    • @After
    • @AfterReturning (正常返回)
  2. 异常执行

    • @Before
    • @After
    • @AfterThrowing

获取目标方法的详细信息

方法信息

添加 JoinPoint类型的参数即可,JoinPoint封装了目标方法的详细信息

@Before("execution(public int com.oylong.impl.MathCalcul*.*(..))")
public static void logStart(JoinPoint joinPoint){
    Object[] args = joinPoint.getArgs();
    System.out.print("方法参数: ");
    System.out.println(Arrays.asList(args));

    Signature signature = joinPoint.getSignature();
    System.out.println("方法名: "+ signature.getName());

    System.out.println("日志记录开始");
}

返回结果

@AfterReturning(value = "execution(public int com.oylong.impl.MathCalculator.*(int, int))", returning = "result")
public static void logReturn(JoinPoint joinPoint, Object result){
    System.out.println("返回结果:" + result);
    System.out.println("方法正常执行完成");
}

需要再添加一个属性值用于接收结果,并且需要再@AfterReturning注解后面添加returning属性,将其值设定为参数的属性名

异常

@Test
public void test01() {
    MathCalculator bean = ioc.getBean(MathCalculator.class);

    bean.div(1, 0);
}

上面的代码有一个除0异常,然后需要在通知方法添加接受异常的属性,同时添加@AfterThrowing注解的throwing属性指定谁来接收异常值

@AfterThrowing(value = "execution(public int com.oylong.impl.MathCalculator.*(int, int))", throwing = "exception")
public static void logThrowing(JoinPoint joinPoint, Exception exception){
    System.out.println(joinPoint.getSignature().getName() + " 方法执行抛出异常");
    System.out.println(exception);
}

返回结果:

方法参数: [1, 0]
方法名: div
日志记录开始
日志记录结束
div 方法执行抛出异常
java.lang.ArithmeticException: / by zero

Spring对通知方法的要求不严格,可以任意返回值,任意权限修饰,但是方法上的每一个参数都需要让spring知道是什么作用

抽取公共切入点表达式

假如需要让所有的通知方法使用统一的表达式,为了便于统一修改,可以使用@Pointcut注解标注在某个没有返回值的空方法上,将其value的值设置为统一的表达式即可,如下:

@Pointcut(value = "execution(public int com.oylong.impl.MathCalculator.*(int, int))")
public void myPoint(){
}

使用时,只需要将切入点表达式改为函数名即可,如下:

@AfterThrowing(value = "myPoint()", throwing = "exception")
public static void logThrowing(JoinPoint joinPoint, Exception exception){
    System.out.println(joinPoint.getSignature().getName() + " 方法执行抛出异常");
    System.out.println(exception);
}

@After("myPoint()")
public static void logEnd(){
    System.out.println("日志记录结束");
}

环绕通知(狠强大)

使用@Around注解表示使用环绕通知

如下代码,我们可以使用类似动态代理的方式,对方法执行前后进行处理

@Around("myPoint()")
public Object myAround(ProceedingJoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    Object proceed = null;
    try {
        //前置通知 @Before
        System.out.println("方法开始:" + joinPoint.getSignature().getName());
        proceed = joinPoint.proceed(args);
        System.out.println("方法返回,返回值:"+ proceed);
        //返回通知 @AfterReturning
    } catch (Throwable throwable) {
        //异常通知 @AfterThrowing
        System.out.println("异常通知");
        throwable.printStackTrace();
    } finally {
        //后置通知  @After
        System.out.println("后置通知");
    }
    return proceed;
}

其中proceed = joinPoint.proceed(args);这一句表示方法执行,返回值即为方法的返回结果,我们必须要将测结果通过环绕通知的返回结果返回出去,这个结果就是我们所能得到的最终结果

测试运行结果如下:

@Test
public void test01() {
    MathCalculator bean = ioc.getBean(MathCalculator.class);

    bean.add(1, 1);
}
方法开始:add
方法返回,返回值:2
后置通知
  • 如果即使用了环绕通知,也使用了普通的通知方法,那么环绕通知优先于普通通知执行

    @Pointcut(value = "execution(public int com.oylong.impl.MathCalculator.*(int, int))")
    public void myPoint() {
    }
    
    @Before(value = "myPoint()")
    public static void logStart(JoinPoint joinPoint) {
        System.out.println("普通前置");
    }
    
    
    @AfterReturning(value = "myPoint()", returning = "result")
    public static void logReturn(JoinPoint joinPoint, Object result) {
        System.out.println("普通返回");
    }
    
    @AfterThrowing(value = "myPoint()", throwing = "exception")
    public static void logThrowing(JoinPoint joinPoint, Exception exception) {
        System.out.println("普通异常");
    }
    
    @After("myPoint()")
    public static void logEnd() {
        System.out.println("普通后置");
    }
    
    @Around("myPoint()")
    public Object myAround(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Object proceed = null;
        try {
            //前置通知 @Before
            System.out.println("环绕前置");
            proceed = joinPoint.proceed(args);
            System.out.println("环绕返回");
            //返回通知 @AfterReturning
        } catch (Throwable throwable) {
            //异常通知 @AfterThrowing
            System.out.println("环绕异常");
            throwable.printStackTrace();
        } finally {
            //后置通知  @After
            System.out.println("环绕后置");
        }
        return proceed;
    }

    上述代码的运行结果如下:

    环绕前置
    普通前置
    环绕返回
    环绕后置
    普通后置
    普通返回

    可以看到稍微有点特别的就是普通返回通知并不在环绕返回通知后面,抛异常时结果如下:

    环绕前置
    普通前置
    环绕异常

    可以看到,到环绕异常就停了,如果需要让普通异常通知也接收到异常,那么我们需要在环绕通知里面继续抛异常,如下代码:

    @Around("myPoint()")
    public Object myAround(ProceedingJoinPoint joinPoint) throws Exception {
        Object[] args = joinPoint.getArgs();
        Object proceed = null;
        try {
            //前置通知 @Before
            System.out.println("环绕前置");
            proceed = joinPoint.proceed(args);
            System.out.println("环绕返回");
            //返回通知 @AfterReturning
        } catch (Throwable throwable) {
            //异常通知 @AfterThrowing
            System.out.println("环绕异常");
            throw new Exception();
        } finally {
            //后置通知  @After
            System.out.println("环绕后置");
        }
        return proceed;
    }

    运行结果:

    环绕前置
    普通前置
    环绕异常
    环绕后置
    普通后置
    普通异常

环绕通知与普通通知的不同就是,能影响方法,能在方法执行前,改变参数,能在方法运行后,改变返回结果

多切面

XML配置AOP

  1. 将bean注册进ioc容器

    可以使用component-scan+@Component的方式,也可以使用直接注册bean的形式

    <bean id="mathCalculator" class="com.oylong.impl.MathCalculator"></bean>
    <bean id="logUtils" class="com.oylong.utils.LogUtils"></bean>
  2. 配置aop:config

    <aop:config>
        <aop:aspect ref="logUtils">
    
            <aop:pointcut id="myPointCut" expression="execution(public int com.oylong.impl.MathCalculator.*(int, int))"/>
    
            <aop:before method="logStart" pointcut="execution(public int com.oylong.impl.MathCalculator.*(int, int))"></aop:before>
            <aop:after-returning method="logReturn" pointcut-ref="myPointCut" returning="result"></aop:after-returning>
            <aop:after method="logEnd" pointcut-ref="myPointCut"></aop:after>
            <aop:after-throwing method="logThrowing" pointcut-ref="myPointCut" throwing="exception"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="myPointCut"></aop:around>
    
        </aop:aspect>
    </aop:config>

    可以看到,与注解的配置大同小异,只是将注解和参数都使用标签和属性值来替代了

    我们可以使用aop:pointcut标签来统一配置切入点表达式

    运行结果:

    普通前置
    环绕前置
    环绕返回
    环绕后置
    普通后置
    普通返回

    小细节:在方法执行完之后,对于后置和返回通知方法的执行顺序,总是环绕通知先执行,而前置则谁先配置谁先执行

    如下配置:

    <aop:around method="myAround" pointcut-ref="myPointCut"></aop:around>
    <aop:before method="logStart" pointcut="execution(public int com.oylong.impl.MathCalculator.*(int, int))"></aop:before>
    <aop:after-returning method="logReturn" pointcut-ref="myPointCut" returning="result"></aop:after-returning>
    <aop:after method="logEnd" pointcut-ref="myPointCut"></aop:after>
    <aop:after-throwing method="logThrowing" pointcut-ref="myPointCut" throwing="exception"></aop:after-throwing>

    结果:

    环绕前置
    普通前置
    环绕返回
    环绕后置
    普通返回
    普通后置
    <aop:before method="logStart" pointcut="execution(public int com.oylong.impl.MathCalculator.*(int, int))"></aop:before>
    <aop:after-returning method="logReturn" pointcut-ref="myPointCut" returning="result"></aop:after-returning>
    <aop:after method="logEnd" pointcut-ref="myPointCut"></aop:after>
    <aop:after-throwing method="logThrowing" pointcut-ref="myPointCut" throwing="exception"></aop:after-throwing>
    <aop:around method="myAround" pointcut-ref="myPointCut"></aop:around>

    将环绕通知放到后面后,结果:

    普通前置
    环绕前置
    环绕返回
    环绕后置
    普通后置
    普通返回

    可以看到,环绕返回和后置总是在普通返回和后置通知方法之前,而前置通知方法会随xml配置的顺序改变

注解与xml的选择

  • 注解:快速方便
  • 配置:功能完善,配置与代码分离,更容易区分查看
  • 因此重要的用配置,不重要的使用注解

AOP使用场景

  • 加日志,保存至数据库
  • 做权限验证
  • 安全检查
  • 事务控制
Last modification:September 17, 2020
If you think my article is useful to you, please feel free to appreciate