Spring AOP的需求引入与特定情况下的失效

一、前言

二、介绍一下AOP(面试语言组织)

第一,AOP是对OOP的补充,它也是封装可重用模板,日志打印、安全性检查、异常处理、事务处理,这些东西太过分散了,提供一种封装

总述句:AOP(Aspect Oriented Programming),即面向切面编程,其是OOP(Object Oriented Programming,面向对象编程)的补充和完善。在面向对象编程的世界中,我们很容易理解OOP的思想,简单来说,OOP引入封装、继承、多态等概念来建立一种对象层次结构,这种层次结构是纵向的。虽然OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能关系不大,对于其他类型的代码,如安全性检查、异常处理、事务处理等也都是如此,这种散布在各处的重复的代码被称为横切逻辑,在OOP设计中,它导致了大量代码的重复,不利于各个功能模块的重用。

最简单的理解(面试语言组织):日志打印、安全性检查、异常处理、事务处理,这些东西太过分散了,提供一种封装,这就是AOP

第二,具体地,使用横切技术封装可重用模板,这个可重用模板就是切面Aspect

AOP技术则恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块之中,并将其命名为"Aspect",即切面。所谓"切面",简单说就是将那些被多个业务模块所共同调用的逻辑封装起来,以达到减少重复代码,降低模块之间的耦合度,并提高系统的可维护性的目的。

第三,接第二,横切技术将整个系统分为两部分:核心关注点和横切关注点

使用"横切"技术,AOP把整个软件系统分为两个部分:核心关注点和横切关注点。

业务处理的主要流程是核心关注点,与业务逻辑关系不大的部分是横切关注点。

横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务管理等。

AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

金手指:“AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来”解释
(1)总算搞懂什么是核心关注点?什么是横向关注点?
定义:没有绝对划分,业务处理的主要流程是核心关注点,与业务逻辑关系不大的部分是横切关注点。
例如一个登录逻辑,核心关注点是账号和密码验证和登录成功的业务逻辑,横向关注点是服务端日志打印,
(2)总算搞懂AOP将核心关注点和横向关注点分离开来?
不使用AOP,要想给登录加上日志,就一定要加上修改登录的逻辑代码,但是使用AOP之后,可以不修改登录的逻辑代码,这就是将核心关注点和横向关注点分离开来,这样修改与业务逻辑关系不大的横向关注点就方便多了。
(3)为什么AOP使用代理模式?【博客:Spring中的设计模式】
哪个是抽象接口,哪个是实体类,哪个是代理类

三、举例:AOP封装日志打印功能

一般来说,日志打印是非常混乱的,这里打印一点,那里打印一点,但是使用AOP之后,在不修改原来登录类的前提下给登录逻辑加上的日志打印。

金手指:一句阐述AOP的好处(学习的时候,要想知道一个东西的作用,可以试试没有这个东西的时候,情况会怎样)
这就是AOP的好处,不使用AOP,要想给登录加上日志,就一定要加上修改登录的逻辑代码,但是使用AOP之后,可以不修改登录的逻辑代码 good

接下来我们就用Spring AOP来实现简单的日志记录功能吧。假如我们已经有了一个功能完善的用户登陆接口,现在我们需要在用户调用登陆接口的前后记录下用户的登录行为日志。要实现该功能,最简单的方法就是在原有的登录逻辑里加入日志记录代码,但是这样一来势必需要对原有的登录逻辑进行修改,容易引入新的bug,因此我们决定使用AOP来实现日志记录的功能。在本实例中,我们搭建一个Spring Boot工程,并引入Spring AOP依赖,pom文件依赖关系如下:

在这里插入图片描述

接着我们实现最原始的登录逻辑:
在这里插入图片描述

登录逻辑十分简单,首先判断用户是否为合法用户,如果是则可以正常登录,如果不是则禁止登录。

由于我们要在登录逻辑前后加入日志功能,所以我们需要编写一个环绕通知:

可以看到,我们的环绕增强针对login方法进行横切逻辑的织入,在调用目标对象的前后,分别对用户登录日志进行记录。

接下来写一个测试类看一下效果:

结果输出如下:

可以看到,我们通过AOP很方便地实现了日志记录功能。

四、同一个类中method1()调用method2()

接下来假如我们又有了一个新需求,就是要对不合法用户做些特殊的处理,比如说统计下不合法用户调用登陆接口的次数。由于直接修改原有的登录逻辑有很多弊端,所以我们还是选择通过AOP来实现该功能。这可以通过编写一个返回增强来实现:

我们对isLegal方法进行增强,先拿到isLegal方法的返回值,再根据该返回值决定是否需要累加登录次数。

接下来我们还是用上一节的测试类来测试一下,我们直接看结果:

这个时候诡异的事情发生了。明明user_1为非法用户,但是为何没有对其登录次数进行累加呢?AOP为何会失效呢?下文将为你解开谜团。

五、AOP为何失效

之所以会出现上述AOP失效的现象,归根到底是由于AOP的实现机制导致的。Spring AOP底层采用代理的方式实现AOP(金手指:这个要看源码才知道,底层使用代理模式实现AOP,核心关注点是实际类,横向关注点向核心流程注入代码,就是代理类),我们编写的横切逻辑被添加到动态生成的代理对象中,只要我们调用的是代理对象,则可以保证调用的是被增强的代理方法。

但是,在代理对象中,不管你的横切逻辑是怎样的,也不管你增加了多少层的横切逻辑,有一点可以确定的是,你终归会调用目标对象的同一方法来调用原始的业务逻辑。

如果目标对象中的原始方法依赖于其他对象,那么Spring会注入所依赖对象的代理对象,从而保证依赖的对象的横切逻辑能够被正常织入。

但是,一旦目标对象调用的是自身的其他方法时,问题就来了,这种情况下,目标对象调用的并不是代理对象的方法,故被调用的方法无法织入横切逻辑。

如上图所示,method1和method2方法是同个类中的方法,当外部通过代理对象调用method1时,最终会调用目标对象的method1方法,而在目标对象的method1方法中调用method2方法时,最终调用的是目标对象的method2方法,而不是代理对象的method2方法,故而针对method2的AOP增强失效了。

金手指:就是同一个类中被调用方法method2的AOP失效了

六、如何避免AOP失效

要解决上述Spring AOP失效的问题,有两个方法,

一个是将isLegal方法跟login方法写在不同的类里,这样一来,当login方法调用isLegal方法时,Spring会注入相应的代理对象,从而可以调用到isLegal方法的代理逻辑。

另一个方法是在调用isLegal方法时先获取当前上下文的代理对象,再通过该代理对象调用被增强了的isLegal方法,这样一来也能解决AOP失效的问题。

第一个方法简单但是不优雅,将被调用的方法test1()放到另外一个类中;
第二个方法优雅但是不简单,需要加一个步骤:先获取当前上下文的代理对象,再通过该代理对象调用被增强了的isLegal方法。

实际上Spring AOP为我们提供了获取当前上下文代理对象的方法,使用起来非常方便,

第一,在AOP配置里暴露代理对象,在Spring Boot中可以通过注解@EnableAspectJAutoProxy(exposeProxy = true)进行配置:

也可xml配置

    <aop:aspectj-autoproxy expose-proxy="true"/>

第二,修改login方法,通过AopContext获取当前上下文代理对象,再通过该代理对象调用isLegal方法:

第三,我们运行测试类看下效果:

在这里插入图片描述
可以看到,现在已经可以实现对非法用户的登录次数进行累加了,这样就解决了上述AOP失效的问题。

七、Spring AOP失效模拟 + 源码解析

7.1 Spring AOP失效模拟

7.1.1 第一种模拟方式

 interface DTDL  {
    public void test(String name);
    public void test1();
}
public class ProxyDemo implements DTDL{

    public static void main(String[] args) {
        ProxyDemo proxyDemo=new ProxyDemo();
        DTDL d= (DTDL) Proxy.newProxyInstance(proxyDemo.getClass().getClassLoader(),
                proxyDemo.getClass().getInterfaces(), new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("qian");
                        method.invoke(proxyDemo,args);
                        System.out.println("hou");
                        return proxy;
                    }
                });
        d.test("test");
    }

    public void test(String name){
        System.out.println(name);
        test1();
    }
    public void test1(){
        System.out.println("name1");
    }
}

输出结果,test1()的AOP失效了
在这里插入图片描述

7.1.2 第二种模拟方式

 interface DTDL2  {
     public void test(String name);
     public void test1();
}
public class ProxyDemo2 implements DTDL2  {  // 两种写法不同仅仅在于第二个参数   new Class[]{com.example.demo.DTDL2.class}  是接口字节码   proxyDemo.getClass().getInterfaces() 也是接口字节码

    public static void main(String[] args) {
        ProxyDemo2 proxyDemo=new ProxyDemo2();
        DTDL2 d= (DTDL2)Proxy.newProxyInstance(proxyDemo.getClass().getClassLoader(),
                new Class[]{com.example.demo.DTDL2.class}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("qian");
                        method.invoke(proxyDemo,args);
                        System.out.println("hou");
                        return proxy;
                    }
                });
        d.test("test");
    }

    public void test(String name){
        System.out.println(name);
        test1();
    }
    public void test1(){
        System.out.println("name1");
    }
}

test()和test1()在同一个类中,test()调用test1(),都是使用代理对象的引用调用,则被调用方test1()的模拟AOP(“qian” 和 “hou”)失效。

输出结果,test1()的AOP失效了

在这里插入图片描述

7.2 Spring AOP源码解析

接上面,当代理类调用test方法的时候,会进入到InvocationHandler的invoke方法中,执行完增强逻辑后,接着执行

method.invoke(proxyDemo, args),proxyDemo就是我们被代理的类,之后的方法就是在proxyDemo实例中执行,所以test1方法是不会再进入到InvocationHandler的invoke方法中的。

springAOP中的切面失效也是同样的道理。

在这里插入图片描述
进入到invokeJoinpointUsingReflection方法可以看到,是通过反射执行target中的相应方法。

在这里插入图片描述
进入到proceed方法中,我们可以看到,就是递归的执行拦截器链上的方法,执行到最后的时候执行invokeJoinpoint方法。

在这里插入图片描述
invokeJoinpoint方法其实就是通过反射调用target中的相应方法。

在这里插入图片描述
所以根据上述的分析,我们知道,如果一开始调用的方法没有进行切面增强的话,其内部再调用有切面增强的方法,这时切面是不会生效的,因为此时调用有切面增强方法的类不是代理类,而是被代理类本身。

八、Demo2:AOP失效

问题: 同一个类中的方法互相调用,被调用方aop失效,比如下列代码,
serviceA()和serviceB()在同一个类中,serviceA()中调用serviceB(),则加在serviceB()上的事务注解(这是一个基于AOP的注解)会失效

@Service
public class AServiceImpl implements AService {
    @Transactional
    public void serviceA() {
        System.out.println("serviceA");
        //db
        serviceB();

    }

    @Transactional
    public void serviceB() {
        System.out.println("serviceB");
        //db

    }
}

原理:
spring事务依赖的是SpringAop 动态代理,执行原理见之前的文章
无论是cglib,还是jdk代理,增强逻辑执行到最后,执行目标方法代码如下,此时,代码里面的target为目标对象,不是代理对象,嵌套方法中的内层方法是用目标对象直接调用的,没有经过aop代理对象。所以失效

ReflectiveMethodInvocation.proceed();

public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
            throws Throwable {

        // Use reflection to invoke the method.
        try {
            ReflectionUtils.makeAccessible(method);
            return method.invoke(target, args);
        }

解决方案(将serviceB()方法放到另外一个类中也可以,但是不优雅):
第一步,不要嵌套调用,将需要一个类中嵌套调用的方法,移到另一个类中
第二步,使用expose-proxy ,在代码中使用AopContext获取到代理对象,使用代理对象进行嵌套调用。

<aop:aspectj-autoproxy expose-proxy="true"/>

强制用代理对象进行嵌套调用

@Service
public class AServiceImpl implements AService {
    @Transactional
    public void serviceA() {
        System.out.println("serviceA");
        //db
        AService proxy=(AService) AopContext.currentProxy();
        proxy.serviceB();

    }

    @Transactional
    public void serviceB() {
        System.out.println("serviceB");
        //db

    }
}

九、面试金手指()

9.1 现实需求引入AOP

第一,AOP是对OOP的补充,它也是封装可重用模板,日志打印、安全性检查、异常处理、事务处理,这些东西太过分散了,提供一种封装

总述句:AOP(Aspect Oriented Programming),即面向切面编程,其是OOP(Object Oriented Programming,面向对象编程)的补充和完善。在面向对象编程的世界中,我们很容易理解OOP的思想,简单来说,OOP引入封装、继承、多态等概念来建立一种对象层次结构,这种层次结构是纵向的。虽然OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能关系不大,对于其他类型的代码,如安全性检查、异常处理、事务处理等也都是如此,这种散布在各处的重复的代码被称为横切逻辑,在OOP设计中,它导致了大量代码的重复,不利于各个功能模块的重用。

最简单的理解(面试语言组织):日志打印、安全性检查、异常处理、事务处理,这些东西太过分散了,提供一种封装,这就是AOP

第二,具体地,使用横切技术封装可重用模板,这个可重用模板就是切面Aspect

AOP技术则恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块之中,并将其命名为"Aspect",即切面。所谓"切面",简单说就是将那些被多个业务模块所共同调用的逻辑封装起来,以达到减少重复代码,降低模块之间的耦合度,并提高系统的可维护性的目的。

第三,接第二,横切技术将整个系统分为两部分:核心关注点和横切关注点

使用"横切"技术,AOP把整个软件系统分为两个部分:核心关注点和横切关注点。

业务处理的主要流程是核心关注点,与业务逻辑关系不大的部分是横切关注点。

横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务管理等。

AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

金手指:“AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来”解释
(1)概念划分:总算搞懂什么是核心关注点?什么是横向关注点?
定义:没有绝对划分,业务处理的主要流程是核心关注点,与业务逻辑关系不大的部分是横切关注点。
例如一个登录逻辑,核心关注点是账号和密码验证和登录成功的业务逻辑,横向关注点是服务端日志打印,
(2)解耦设计:总算搞懂AOP将核心关注点和横向关注点分离开来?
不使用AOP,要想给登录加上日志,就一定要加上修改登录的逻辑代码,但是使用AOP之后,可以不修改登录的逻辑代码,这就是将核心关注点和横向关注点分离开来,这样修改与业务逻辑关系不大的横向关注点就方便多了。
(3)具体实现:为什么AOP使用代理模式?【博客:Spring中的设计模式】
哪个是抽象接口,哪个是实体类,哪个是代理类

9.2 AOP失效构造+失效原理+失效解决

9.2.1 AOP失效构造/AOP失效情形

9.2.1.1 实际AOP注解失效

AOP失效构造:同一个类中的方法互相调用,被调用方aop失效

serviceA()和serviceB()在同一个类中,serviceA()中调用serviceB(),则加在serviceB()上的事务注解(这是一个基于AOP的注解)会失效

9.2.1.2 模拟AOP失效

test()和test1()在同一个类中,test()调用test1(),都是使用代理对象的引用调用,则被调用方test1()的模拟AOP(“qian” 和 “hou”)失效。

9.2.2 AOP失效原理 + 两种AOP都是AOP失效

9.2.2.1 AOP失效原理

之所以会出现上述AOP失效的现象,归根到底是由于AOP的实现机制导致的。Spring AOP底层采用代理的方式实现AOP(金手指:这个要看源码才知道,底层使用代理模式实现AOP,核心关注点是实际类,横向关注点向核心流程注入代码,就是代理类),我们编写的横切逻辑被添加到动态生成的代理对象中,只要我们调用的是代理对象,则可以保证调用的是被增强的代理方法。

但是,在代理对象中,不管你的横切逻辑是怎样的,也不管你增加了多少层的横切逻辑,有一点可以确定的是,你终归会调用目标对象的同一方法来调用原始的业务逻辑。

如果目标对象中的原始方法依赖于其他对象,那么Spring会注入所依赖对象的代理对象,从而保证依赖的对象的横切逻辑能够被正常织入。

但是,一旦目标对象调用的是自身的其他方法时,问题就来了,这种情况下,目标对象调用的并不是代理对象的方法,故被调用的方法无法织入横切逻辑。

如上图所示,method1和method2方法是同个类中的方法,当外部通过代理对象调用method1时,最终会调用目标对象的method1方法,而在目标对象的method1方法中调用method2方法时,最终调用的是目标对象的method2方法,而不是代理对象的method2方法,故而针对method2的AOP增强失效了。

金手指:就是同一个类中被调用方法method2的AOP失效了

9.2.2.2 两种AOP都是AOP失效

两种AOP都是AOP失效:无论是cglib,还是jdk代理,增强逻辑执行到最后,执行目标方法代码如下,此时,代码里面的target为目标对象,不是代理对象,嵌套方法中的内层方法是用目标对象直接调用的,没有经过aop代理对象。所以失效

9.2.3 AOP失效解决方案

要解决上述Spring AOP失效的问题,有两个方法,

一个是将isLegal方法跟login方法写在不同的类里,这样一来,当login方法调用isLegal方法时,Spring会注入相应的代理对象,从而可以调用到isLegal方法的代理逻辑。

另一个方法是在调用isLegal方法时先获取当前上下文的代理对象,再通过该代理对象调用被增强了的isLegal方法,这样一来也能解决AOP失效的问题。

第一个方法简单但是不优雅,将被调用的方法test1()放到另外一个类中;
第二个方法优雅但是不简单,需要加一个步骤:先获取当前上下文的代理对象,再通过该代理对象调用被增强了的isLegal方法。

十、小结

AOP和AOP失效,完成了。

天天打码,天天进步!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祖母绿宝石

打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值