美文网首页
Spring AOP简介

Spring AOP简介

作者: LENN123 | 来源:发表于2020-04-11 23:08 被阅读0次
    为什么需要AOP?

    AOP(面向切面编程)和OOP(面向对象编程)一样,也是一种编程思想。具体来说,AOPOOP的一种有效补充,以求解决OOP中的一些弊端。在OOP的思想下,我们可以很轻松的将一些业务需求抽象成一个个类,形成可重用的模块。但是遇到系统需求时,往往捉襟见肘,造成大量的重复代码,比如我们最常见的打印日志和权限验证的需求。

    横切关注点
    上图中上,Class AClass BClass C这三个不同的类,却都需要在某个方法执行前进行权限验证,在执行后进行日志记录。这样横跨了多个类的共同需求,我们称为横切关注点。在这里显然varify()log()在多个类中重复,当然重复代码还不是最主要的问题,当我们需要修改verify()log()方法时,我们要在A、B、C三个类中都进行修改,当类的数目越来越多,就会牵一发而动全身。那么有人会说,我们可以把verify()log()抽象成一个类,如果需要进行修改时,就在这个类中进行。这个方案似乎可行,但是仍然存在问题
    • 因为很多时候横切关注点的逻辑和业务逻辑纠缠在一起,并不是很好的进行抽取。
    业务逻辑横切逻辑纠缠
    • 假设我们想将log()调整到方法执行之前,或者说在方法执行前也添加log()打印日志,那我们还是需要去大量的类中手动添加代码,这个方法治标不治本。
    • 如果能把所有的横切关注点的逻辑直接抽离出来,让程序员专注于业务代码就好了,这样子代码的可读性也会大大提高。
      AOP就是为了帮助我们解决上述问题而生的,具体来说就是
    1. 帮助我们把横切关注点从多个类中抽取出来,形成Aspect(切面)
    2. 程序运行时/编译时,帮我们把这些横切逻辑重新插入到每个类中对应的位置(pointcut),这个过程叫做weaver(织入)。

    这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(AOP)。AOP是一种编程思想,而Spring AOP则是AOP思想的具体实现。

    Spring AOP的使用

    在具体应用之前,让我们先熟悉AOP下的一些术语

    术语 解释
    jointpoint 系统运行前,AOP的功能模块需要织入到OOP的功能模块中去,jointpoint就是指能够进行织入操作的执行点
    pointcut 切点,一次织入过程中, 具体的jointpoint信息,比如要在A()方法处织入横切逻辑,那么A()就是pointcut
    advice 通知,代表具体的横切逻辑,可以类比OOP中的method,注意:advice还指明了执行横切逻辑的时间的,比如在A()执行方法之前执行,还是在其之后执行等
    aspect 切面,point + advice = aspect, 在哪些切点(切点是个集合)上执行何种横切逻辑(比如打印日志)就是一个切面

    在不同的AOP实现中,jointpoint的粒度不同,在Spring AOP中,这个jointpoint是方法级别的,也就是只提供方法拦截,但即便这样,也足以满足80%的业务需求了。advice除了定义了横切逻辑,还定义了横切逻辑执行的时机,在Spring AOP中有前置、后置、返回、异常、环绕五种Advice,例如前置型Advice,表示在pointcut前执行横切逻辑,下面会举例详细说明。

    前置Advice

    首先让我们定义一个People类,它包含一个eatFruit表示吃水果的这个行为,我们将尝试以这个访问为pointcut,来进行织入工作。然后我们来定义Advice,在Spring AOP中,Advice是实现了对应接口的类,如果我们要实现一个前置型的Advice,就要实现MethodBeforeAdvice中的方法。在这里我们定义了一个名为BeforeEat的前置型Advice,表示吃之前要执行的横切逻辑。

    • people 类
    public class People {
        public void eatFruit(){
            System.out.println("正在吃水果");
        }
    }
    
    • BeforeEat 类
    public class BeforeEat implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("eat方法的前置通知: 我要开始吃了!");
        }
    }
    

    接下来让我们把这两个类注入到Spring IOC容器中,交由Spring管理。

        <bean id="people" class="aop.People">
        </bean>
    
        <bean id="beforeEat" class="aop.BeforeEat">
        </bean>
    

    之后最重要的是告诉Springpointcut是哪些方法?,和pointcut关联Advice是哪一个,让我们完善aop config

        <aop:config>
            <aop:pointcut expression="execution(public void aop.People.eatFruit())" id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    

    <aop:pointcut>表示pointcutPeople类的eatFruit方。之前我们有提到过point + advice = aspect,而<aop:advisor>标签中的就可以理解为aspect,它关联了与advice对应的pointcut。下面让我们调用下People类的eatFruit()方法看看是什么效果。

    执行前需要先导入aspectJweaver.jar包

    • 调用earFruit()方法
        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            People people = (People)applicationContext.getBean("people");
            people.eatFruit();
        }
    
    • 执行结果
    eat方法的前置通知: 我要开始吃了!
    正在吃水果
    

    可以发现横切逻辑在方法执行前被调用了。
    之前我们说过, pointcut在这里可以看作要被织入横切逻辑的具体位置(方法)的集合,因此pointcut内部可以包含多种方法,让我们在People类中添加一个drinkSomething方法。

    public class People {
    
        public void eatFruit(){
            System.out.println("正在吃水果");
        }
        public void drinkSomething(String sth){
            System.out.println("正在喝"+sth);
        }
    }
    

    把这个方法也加入到当前的pointcut中去。

        <aop:config>
            <aop:pointcut expression="execution(public void aop.People.eatFruit()) or
                    execution(public void aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    

    pointcut中两个方法用or连接。运行结果是在这2个方法调用前都会执行横切逻辑BeforeEat

    eat方法的前置通知: 我要开始吃了!
    正在吃水果
    eat方法的前置通知: 我要开始吃了!
    正在喝牛奶
    
    Process finished with exit code 0
    

    可以看到pointcut中的expression是支持集合的交并补运算的,此外还支持通配符的方式,来指代一类方法。比如我们可以修改<aop:config>为:

        <aop:config>
            <aop:pointcut expression="execution(public void * (String))" id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="beforeEat" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    

    就表示任何以String为参数(不限方法名)的方法,在这里也就只有drinkSomething(String sth)满足条件,尝试运行发现也的确只在这个方法前执行了横切逻辑。通过通配符和集合运算的方式,可以容易的指定一类具体的的方法为pointcut

    正在吃水果
    eat方法的前置通知: 我要开始吃了!
    正在喝牛奶
    
    Process finished with exit code 0
    

    现在让我们再回到Advice类的定义上,看看接口方法中的参数都代表了什么。

    public class BeforeEat implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println(method+" " + Arrays.toString(objects) + " " + o);
            System.out.println("eat方法的前置通知: 我要开始吃了!");
        }
    }
    

    执行结果

    public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@424e1977
    eat方法的前置通知: 我要开始吃了!
    正在喝牛奶
    
    Process finished with exit code 0
    

    可以发现method即与横切逻辑advice关联的具体方法,在这里就是public void aop.People.drinkSomething(java.lang.String), Object[] objects则是传入该方法的参数,object则是执行横切逻辑的方法所属的对象实例,这里就是IOCid=people的这个bean

    后置Advice

    后置型Advice与前置型Advice正相反,表示在pointcut之后执行横切逻辑。我们编写一个名为AfterEat的后置型Advice

    public class AfterEat implements AfterReturningAdvice {
        @Override
        public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
            System.out.println("吃完了,洗洗手。");
        }
    }
    

    为其编写xml配置。

        <bean id="afterEat" class="aop.AfterEat">
        </bean>
        <aop:config>
            <aop:pointcut expression="execution(public void aop.People.eatFruit()) or
                    execution(public void aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="afterEat" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    

    执行结果

    public void aop.People.eatFruit() [] aop.People@1190200a
    eat方法的前置通知: 我要开始吃了!//前置
    正在吃水果
    吃完了,洗洗手。//后置
    public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@1190200a
    eat方法的前置通知: 我要开始吃了!//前置
    正在喝牛奶
    吃完了,洗洗手。//后置
    

    注意到AfterReturningAdvice接口中的afterReturning方法中的参数与前置Advice有差别,让我们尝试打印一下。

    public class AfterEat implements AfterReturningAdvice {
        @Override
        public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
            System.out.println(method+" " + Arrays.toString(objects) + " " + o + " " + o1);
            System.out.println("吃完了,洗洗手。");
        }
    }
    

    输出结果

    public void aop.People.eatFruit() [] null aop.People@1190200
    

    可以看到o1输出的是对象实例,而o输出的值是null, 那么o代表什么呢?让我们修改drinkSomething(String)的返回值为int,再打印一次

        public int drinkSomething(String sth){
            System.out.println("正在喝"+sth);
            return 0;
        }
    
    public int aop.People.drinkSomething(java.lang.String) [牛奶] 0 aop.People@1190200a
    

    发现o的值变为0,也就是说其代表了横切逻辑执行前这个方法的返回值。

    异常Advice

    异常Advice指的是当pointcut中的方法抛出异常时,将会执行的横切逻辑。

    • 编写异常Advice
    public class WhenException implements ThrowsAdvice {
    /*
     * <pre class="code">public void afterThrowing(Exception ex)</pre>
     * <pre class="code">public void afterThrowing(RemoteException)</pre>
     * <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, Exception ex)</pre>
     * <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)</pre>
    */
    
    }
    

    ThrowsAdvice这个接口并没有要求我们实现任何接口方法,而是在文档里给出了一些示例,还告诉我们Method method, Object[] args, Object target,这3个打包在一起的参数是可选的,如果你想获得更详细的信息,就加上它们。

    • 实现异常Advice
    public class WhenException implements ThrowsAdvice {
        public void afterThrowing(Exception ex) {
            System.out.println("异常Advice : 发生了异常");
            System.out.println(ex.getMessage());
        }
    }
    

    编写app config

        <bean id="whenException" class="aop.WhenException"></bean>
        <aop:config>
            <aop:pointcut expression="execution(public void aop.People.eatFruit()) or
                    execution(public int aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="whenException" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    

    再在drinkSomething()方法里故意引起一个异常。

        public int drinkSomething(String sth){
            System.out.println("正在喝"+sth);
            int a = 1 / 0;
            return 0;
        }
    

    执行结果

    public int aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@4c39bec8
    eat方法的前置通知: 我要开始吃了!
    正在喝牛奶
    异常Advice : 发生了异常
    / by zero
    
    环绕Advice

    截至目前为止,我们已经实验了前置后置异常三种Advice。它们执行的时机如下。

    advice
    环绕型Advice,可以实现以上三种Advice的所有功能,即可以同时在上述的所有位置执行横切逻辑。
    • 实现一个环绕型Advice
    public class AroundEat implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            try {
    
                System.out.println("环绕Advice: 方法执行前" );// 前置
    
                Object result = invocation.proceed();// pointcut中方法的执行
    
                System.out.println("环绕Advice: 方法执行后" );// 后置
    
            } catch (Exception e) {
                System.out.println("环绕Advice: 发生异常");
            }
    
            return null;
        }
    }
    

    这里的关键是Object result = invocation.proceed();,这里就相当于执行我们定义在pointcut中的方法,因此在这行语句前面执行的逻辑,相当于前置advice,在这行语句后面执行的逻辑,相当于后置advice。捕捉到异常后实现的逻辑就相当于异常advice
    为其配置aop,进行验证。

        <bean id="aroundEat" class="aop.AroundEat"></bean>
    
        <aop:config>
            <aop:pointcut expression="execution(public void aop.People.eatFruit()) or
                    execution(public int aop.People.drinkSomething(String)) " id="pointcut"></aop:pointcut>
            <aop:advisor advice-ref="aroundEat" pointcut-ref="pointcut"></aop:advisor>
        </aop:config>
    
    • 运行结果
    环绕Advice: 方法执行前
    正在吃水果
    环绕Advice: 方法执行后
    环绕Advice: 方法执行前
    正在喝牛奶
    环绕Advice: 发生异常
    
    利用注解的形式实现AOP

    Spring AOP,也提供了基于注解的形式实现AOP, 较XML配置的方法更加简单直观,我们来利用注解实现AOP,以前置Advice为例,将之前的BeforeEat改进为基于注解的方式。

    @Component("beforeEatAnnotation")
    @Aspect
    public class BeforeEatAnnotation {
        @Before("execution(public void aop.People.eatFruit())") //定义切点
        void before(){
            System.out.println("采用注解形式实现的前置通知");
        }
    
        @AfterReturning("execution(public void aop.People.eatFruit())")
        void after(){
            System.out.println("采用注解形式实现的后置通知");
        }
    }
    

    和我们之前基于XML的配置一样,我们要定义具体的pointcut并且把其和关联的Advice绑定起来,在这个类里我们可以在任意方法前加上@Before注解,表示该方法是一个前置advice,然后在其括号内注明pointcut,这样pointcutadvice很自然的关联在一起了,所以也无需之前的<aop:advisor>来指明两者关系了。@Aspect代表这个类表示一个切面。@Component把这个类交由Spring管理,注意配置自动扫描。
    最后,我还需要在xml中配置aop自动代理。

        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    

    实验结果

    采用注解形式实现的前置通知
    正在吃水果
    采用注解形式实现的后置通知
    
    Process finished with exit code 0
    

    之前利用接口的方式来实现AOP可以很容易的获得目标对象,方法名、参数等信息,利用注解的方式也可以实现,这里需要借助一个特殊的JoinPoint类。

    @Component("adviceByAnnotation")
    @Aspect
    public class AdviceByAnnotation {
        @Before("execution(public void aop.People.eatFruit())") //定义切点
        void before(JoinPoint joinPoint){
            System.out.println(joinPoint.getTarget() + " " + Arrays.toString(joinPoint.getArgs()) + " " + joinPoint.getSignature());
            System.out.println("采用注解形式实现的前置通知");
        }
    
        @AfterReturning(pointcut="execution(public void aop.People.eatFruit())", returning = "returningValue")
        void after(JoinPoint joinPoint, Object returningValue){
            System.out.println("返回值为" + returningValue);
            System.out.println("采用注解形式实现的后置通知");
        }
    }
    
    

    可以发现pointcut中的特定方法的有关信息都已经被包装到JoinPoint类中去了。对于以@AfterReturning标注的后置Advice,还可以指明获取返回值。
    实验结果如下

    aop.People@140c9f39 [] void aop.People.eatFruit()
    采用注解形式实现的前置通知
    正在吃水果
    返回值为null
    采用注解形式实现的后置通知
    

    类似的我们还可以实现基于注解的异常Advice环绕Advice以及最终Advice

        @After("execution(public int aop.People.drinkSomething(String))")
        void after(){
            System.out.println("最终通知,无论有没有发生异常,都会执行");
        }
        //异常通知
        @AfterThrowing("execution(public int aop.People.drinkSomething(String))")
        void afterException(){
            System.out.println("采用注解形式的异常通知");
        }
        //环绕通知
        @Around("execution(public int aop.People.drinkSomething(String))")
        void around(ProceedingJoinPoint proceedingJoinPoint) {
            try {
                System.out.println("采用注解形式的环绕通知[前置]");
                proceedingJoinPoint.proceed();
                System.out.println("采用注解形式的环绕通知[后置]");
    
            }catch (Throwable e) {
                System.out.println("采用注解形式的环绕通知[异常]");
            } finally {
                System.out.println("采用注解形式的环绕通知[最终]");
            }
        }
    

    环绕Advice里,proceedingJoinPoint.proceed();就是真正执行了pointcut集合中某个具体方法。注意这里区别最终和后置的区别,后置Advice如果发生异常则不会被执行,而最终Advice是一定会被执行的。
    执行结果如下

    采用注解形式的环绕通知[前置]
    正在喝牛奶
    采用注解形式的环绕通知[异常]
    采用注解形式的环绕通知[最终]
    

    相关文章

      网友评论

          本文标题:Spring AOP简介

          本文链接:https://www.haomeiwen.com/subject/lewymhtx.html