Spring AOP 第一篇-初识Spring AOP

目录

前言

AOP 的几个概念

代码样例

几种通知的常用场景

关于@Pointcut

总结


前言

        前一篇文章讲了一些关于动态代理和拦截器链的内容,Spring-动态代理和拦截器。因为动态代理就是 Spring AOP 的基础,所以可以先看下上篇文章,这样能更好的理解本篇文章。我们通过动态代理可以实现增强被代理对象的功能,Spring AOP 的作用也是为了增强被代理对象的,只不过它使得被代理对象的增强变得简单,更有利于开发使用。

AOP 的几个概念

        我首先把 Spring 官网的解释拿过来,然后再尝试用我的理解进行一番解释。

                AOP 官方文档

        

  • 通知(Advice)

        其实就是我们需要增强的功能。比如标记了 @Before, @After 等注解的方法就是一个通知,它定义了在要在切点进行的一些行为,即定义了我们要如何增强。

  • 连接点(JoinPoint)

        连接点就是程序允许通知增强的地方,比如在执行方法前,执行方法后或者是跑出异常的时候。Spring AOP 只支持方法连接点,即只能以方法的维度进行增强。而不像其他的框架还能在构造器或者属性赋值的时候进行增强。

  • 切点(Pointcut)

        连接点是一个概念性的东西,比如连接点的概念告诉你 Spring AOP 可以以方法的维度进行增强。但是可以做并不是一定会做。因为我们不可能对所有的方法都进行增强,而切点就是要说明具体要在哪个连接点,即要对哪个方法进行增强。

  • 切面(Aspect)

        在 Spring AOP 中,如果一个类上面标注了 @Aspect 注解,那么它就是一个切面。其实想很好的理解切面,首先得理解切点和通知,切面是切点和通知的结合,其实如果看了上篇文章的同学应该意识到,它跟 Spring AOP 中的 Advisor 很像, 只不过 Advisor 只包含一个切点和通知,而切面可以定义很多切点和通知,不过最后 Spring 最后都会将其解析成一个个 Advisor, 其实它像是一个概念性的东西,比如说是一个公司,公司本身不能干活,是公司里的员工在干活。切面负责管理,真正做事情的是它包含的切点和通知。

  • 引入 (Introduction)

        引入是可以在现有的接口中引入其他的接口,以增强当前接口的功能

  • 目标(target)

        即被代理对象,需要被增强的对象

  • 代理(proxy)

        目标对象被代理后就会成为一个代理对象。相信看了上篇文章的同学都能理解

  • 织入(Weaving) 

        织入可以理解为把切面应用到目标对象创建代理的过程,有些框架是编译时织入,即直接在被代理对象的代码附近修改源代码,有的框架是在运行时织入,比如Spring AOP 的代理动态代理方式,它不会改变被代理对象的代码。

代码样例

  • 引入需要的依赖
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.6</version>
            </dependency>
  • 测试接口和实现类
    public interface IOrder {
    
        Integer query(String type);
    }
    
    @Service
    public class OrderService implements IOrder {
    
        @Override
        public Integer query(String type) throws Exception {
            System.out.println("查询类型为" + type +  "订单数量");
            //模拟查询过程中出错
            try {
                //int a = 1 / 0;
            } catch (Exception ex) {
                throw new Exception("我错了");
            }
    
            return 1;
        }
    
    }
    
  • 添加切面类
    @Aspect
    public class QueryAspect {
        @Pointcut("execution(* com.proxy.IOrder.query(..))")
        public void pointCut(){};
    
        @Before(value = "pointCut()")
        public void methodBefore(JoinPoint joinPoint) throws Exception {
            System.out.println("调用目标方法前 @Before 可以提前获取到参数信息:" + Arrays.toString(joinPoint.getArgs()));
            //模拟 before 出错
            //int a = 1 / 0;
        }
    
        @After(value = "pointCut()")
        public void methodAfter(JoinPoint joinPoint) {
            System.out.println("调用目标方法后 @After");
            // 模拟 After 异常
            int a = 1 / 0;
        }
    
        @AfterReturning(value = "pointCut()", returning = "result")
        public void methodAfterReturning(JoinPoint joinPoint, Object result) {
            System.out.println("目标方法返回后 @AfterReturning, 此时可以获取到返回结果 " + result);
            //模拟 AfterReturning 出错
            //int a = 1 / 0;
        }
    
        @AfterThrowing(value = "pointCut()", throwing = "ex")
        public void methodAfterThrowing(JoinPoint joinPoint, Exception ex) {
            System.out.println("抛出异常后, @AfterThrowing " + ex);
        }
    
        //@Around(value = "pointCut()")
        public Object methodAround(ProceedingJoinPoint pdj) {
            Object result = null;
            System.out.println("调用目标方法前 @Around ");
            try {
                //调用目标方法
                result = pdj.proceed();
            } catch (Throwable ex) {
                System.out.println("捕捉到异常信息:" + ex);
            }
            System.out.println("调用目标方法后 @Around ");
            return result;
        }
    }
  • 添加配置类,配置切面 bean
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(value = {"com.proxy"})
    public class AopConfig {
        @Bean
        public QueryAspect queryAspect() {
            return new QueryAspect();
        }
    }
    
  • 编写测试类
    public class ProxyMain {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
            IOrder proxy = (IOrder) applicationContext.getBeanFactory().getBean("orderService");
            try {
                Object result = proxy.query("鸿星尔克");
                System.out.println("查询结果为:" + result);
            } catch (Exception e) {
                System.out.println("测试类捕捉到错误:" + e);
            }
        }
    }
  • 先注释掉@Around的通知,查看运行结果
  • 1:当没有异常的时候
        调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克]
        执行目标方法,查询类型为鸿星尔克订单数量
        目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1
        调用目标方法后 @After
        查询结果为:1
    
    2:当目标方法有异常的时候
        调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克]
        执行目标方法,查询类型为鸿星尔克订单数量
        抛出异常后, @AfterThrowing java.lang.Exception: 我错了
        调用目标方法后 @After
        测试类捕捉到错误:java.lang.Exception: 我错了
        
        可见,当目标方法有异常的时候,@AfterReturning 的通知就不会执行了,但是 @After 的通知还会继    续执行
    
    3: 当 @Before 通知异常的时候
        调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克]
        测试类捕捉到错误:java.lang.ArithmeticException: / by zero
        
        可见,@AfterThrowing 没有捕获 @Before 通知的异常,@After 和 @AfterReturning 的通知都没有执行
    
    4: 当 @AfterReturning 通知异常的时候
        调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克]
        执行目标方法,查询类型为鸿星尔克订单数量
        目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1
        调用目标方法后 @After
        测试类捕捉到错误:java.lang.ArithmeticException: / by zero
    
        可见,@AfterThrowing 没有捕获 @AfterReturning 通知的异常,且 @After 通知会继续执行
    
    5: 当 @After 通知异常的时候
        调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克]
        执行目标方法,查询类型为鸿星尔克订单数量
        目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1
        调用目标方法后 @After
        测试类捕捉到错误:java.lang.ArithmeticException: / by zero
        
        可见,三个通知都会执行
    
  • 由上面的执行结果,我们可以发现
  1. @AfterThrowing 只可以捕捉到切点的异常。
  2. @Before 通知一定会执行,且如果执行过程中有异常,就不会再进行下面的步骤了。
  3. @After 通知在 @AfterReturning 通知之后再执行,且如果切点方法抛出异常的话,那么@AfterReturning 通知就不会执行了。
  • 这三个通知的执行过程大概如下所示
    // @Before 通知先执行
    try {
        try {
            //@Before 通知如果没有异常,就会在此时执行目标方法
        } catch (Throwable throwable) {
            //目标方法发生异常,会在此时执行 @AfterThrowing 通知
            throw throwable;
        }
        //如果目标方法没有跑出异常,此时执行 @AfterReturn 的通知
    } catch (Exception exception) {
                    
    } finally {
        //只要@Before 不抛出异常,就会执行 @After 通知
    }
  • 上述三个通知,想必大家都知道怎么使用了,那么 @Around 通知呢?我们把其他三个通知都注释掉,只保留@Around通知观察一下执行结果
    @Around(value = "pointCut()")
        public Object methodAround(ProceedingJoinPoint pdj) {
            Object result = null;
            System.out.println("调用目标方法前 @Around ");
            try {
                //调用目标方法
                result = pdj.proceed();
            } catch (Throwable ex) {
                System.out.println("捕捉到异常信息:" + ex);
            }
            System.out.println("调用目标方法后 @Around ");
            return result;
        }
  • 执行结果

    1:当切点方法没有异常的时候
        调用目标方法前 @Around 
        执行目标方法,查询类型为鸿星尔克订单数量
        调用目标方法后 @Around 
        查询结果为:1
    
    2: 当切点方法有异常的时候
        调用目标方法前 @Around 
        执行目标方法,查询类型为鸿星尔克订单数量
        捕捉到异常信息:java.lang.Exception: 我错了
        调用目标方法后 @Around 
        查询结果为:null
  •  可以看到,其实 @Around 通知的功能比另外三个强大一些,它把切点方法都切入到通知中,可以在切点方法的各个地方进行增强。可以说是具备上述三个通知的所有功能。而上面三个通知各司其职,只能完成某一个增强功能。所以说。如果增强的需求很简单,只需要在某个点增强。那么选择上面的通知就好了。其优点在于简单高效。如果需要增强的功能很复杂需要在多个连接点进行增强,那么就需要使用 @Around 了大展身手了。其优点在于功能性强。 

几种通知的常用场景

  • @Before 通知可以提前获取到目标方法的参数信息,故可以用作权限认证(某些参数可以接着往下走,而有些就到此为止)。也可以用作保存接口访问纪录(因为这个通知一定会执行,其他的如果出了异常可能就保存不了这个访问纪录了)
  • @AfterReturning 通知可以获取到目标方法的执行结果,故它可以对获取到的结果进行增强,比如可以对结果添加一些信息或者删除一些信息。也可以将目标方法的结果保存一份或者发送给其他系统一份。主要是为了对结果进行增强。
  • @After 通知,因为无论目标方法无论失败与否都会执行该通知,所以可以用它执行一些关闭资源的操作。
  • @AfterThrowing 通知,主要是对异常情况进行一些处理,比如可以进行数据备份与还原
  • @Around 通知,主要在一些需要比较复杂的增强的时候使用。

关于@Pointcut

        其实从上面我们也应该已经知道了,它是定义一个切点的,如上例中

@Pointcut("execution(* com.proxy.IOrder.query(..))")
public void pointCut(){};

        其中 @Pointcut 指定了切点方法,pointCut() 方法是个签名,签名的作用只是为了使用切点时候的简化,不用一遍一遍写 execution(* com.proxy.IOrder.query(..)) 

        @Pointcut 中除了execution 还有其它的可选项,比如 within, target, args, annotation 等,而execution 是最常用的匹配方法的标签,本文只介绍这一个。

        excecution()的语法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
  • modifier-pattern: 表示方法的修饰符,如 public, private等。
  • ret-type-pattern: 表示方法的返回值类型,* 表示任何类型返回值
  • declaring-type-pattern: 表示方法所在的类的路径。
  • name-pattern: 表示方法名,可以用完整方法名,也可以用query*表示匹配所有query开头的方法
  • param-pattern: 表示参数类型,可以指定多个参数类型,用逗号隔开,也可以用 * 匹配任意类型的参数,比如 (int, *) 表示匹配第一个参数类型为 int,第二个为任意类型的方法。也可以用(..) 来匹配零个或多个任意的方法的参数。
  • throws-pattern: 表示方法抛出的异常

        其中带?的修饰符不是必填项,多个execution之间还可以使用 &&(与),|| (或),! (非表达式) 

  1.  @Pointcut("execution(Integer com.proxy.IOrder.query(..))") : 表示匹配 Integer 类型的query方法,但是方法的参数不限制。
  2. @Pointcut("execution(* com.proxy.IOrder.query(int))"): 表示匹配任意类型的query方法,但是参数列表中只能有一个 int 类型的参数。
  3. @Pointcut("execution(Integer com.proxy.IOrder.query(String)) || execution(Integer com.proxy.IOrder.query(int))"):表示匹配参数类型为 String 或 int 的 query 方法

总结

        本文介绍了AOP的一些概念,且用实例介绍了 Spring AOP 定义切面,切点和通知的方式,还介绍了不同通知的使用场景。本文作为 Spring AOP 的入门篇,主要侧重于代码使用。旨在看了该文章之后知道怎么使用 Spring AOP。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值