轻松理解 Spring AOP

Spring AOP 简介

image

Spring AOP 的基本概念

  • AOP (Aspect-Oriented Programming),即 面向切面编程, 它与 OOP (Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.
  • 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
  • AOP是 Spring是最难理解的概念之一,同时也是非常重要的知识点,因为它真的很常用。

面向切面编程

在面向切面编程的思想里面,把功能分为两种

核心业务:登陆、注册、增、删、改、查、都叫核心业务
周边功能:日志、事务管理这些次要的为周边业务

在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的;

然后把切面功能和核心业务功能 "编织" 在一起,这就叫AOP

AOP 的目的

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

AOP 术语和流程

进一步了解AOP之前,我们先来看看AOP中使用到的一些术语,以及AOP运行的流程。

术语

  • 连接点(join point:对应的是具体被拦截的对象,因为Spring只能支持方法,所以被拦截的对象往往就是指特定的方法。具体是指一个方法

  • 切点(point cut:有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。具体是指具体共同特征的多个方法。

  • 通知(advice:它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件,有这几种:

  • 前置通知(before advice

  • 环绕通知(around advice

  • 后置通知(after advice

  • 异常通知(afterThrowing advice

  • 事后返回通知(afterReturning advice

  • 目标对象(arget:即被代理的对象,通俗理解各个切点的所在的类就是目标对象。

  • 引入(introduction:是指引入新的类和其方法,增强现有Bean的功能。

  • 织入(weaving:它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。

  • 切面(aspect:定义切点、各类通知和引入的内容,AOP将通过它的信息来增强Bean的功能或将对应的方法织入流程。

上述的描述还是比较抽象的,配合下面的流程讲解以及例子,应该充分掌握这些概念了。

流程

画了一张图,通过张图可以清晰的了解AOP的整个流程,以及上面各个术语的意义和关系。

图片的流程顺序基于Spring 5

image

五大通知执行顺序

不同版本的Spring是有一定差异的,使用时候要注意

Spring 4

正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing

Spring 5.28

正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终

图例

举一个实际中的例子来说明一下方便理解:


image

当我们有很多个房东的时候,中介的优势就体现出来了。代入到我们实际的业务中,AOP能够极大的减轻我们的开发工作,让关注点代码与业务代码分离!实现解藕!

实际的代码

用一个实际代码案例来感受一下

创建一个房东

@Component("landlord")
public class Landlord {

    public void service() {
        System.out.println("签合同");
        System.out.println("收钱");
    }
}

创建中介

@Component
@Aspect
class Broker {
    @Before("execution(* pojo.Landlord.service())")
    public void before(){
        System.out.println("带租客看房");
        System.out.println("谈钱");
    }
    @After("execution(* pojo.Landlord.service())")
    public void after(){
        System.out.println("给钥匙");
    }
}

3.在 applicationContext.xml 中配置自动注入

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="aspect" />
    <context:component-scan base-package="pojo" />
    <aop:aspectj-autoproxy/>
</beans>

4.测试

public class Test {
    public static void main(String[] args) {
        ApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class);
        landlord.service();
    }
}

5.执行看到效果:

带租客看房
谈钱
签合同
收钱
给钥匙

这个例子中我们用到了@Before和@After两个注解,其实就是设置的前置通知和后置通知。

最后的结果似乎与我们之前图例中的顺序不同,给钥匙在收钱之后了,这个问题留到后面再解决,目前只需要简单感受一下aop的使用即可。

预告:这种情况下应该使用环绕通知来完成这个需求

使用 Spring AOP

使用注解开发AOP

目前使用注解的方式进行Spring开发才是主流,包括SpringBoot中,已经是全注解开发,所以我们采用@AspectJ的注解方式,重新实现一下上面的用例,来学习AOP的使用。

image

第一步:选择连接点

Spring 是方法级别的 AOP 框架,我们主要也是以某个类额某个方法作为连接点,另一种说法就是:选择哪一个类的哪一方法用以增强功能。

@Component
public class Landlord {
    public void service() {
        System.out.println("签合同");
        System.out.println("收钱");
    }
}

我们在这里就选择上述 Landlord 类中的 service() 方法作为连接点。

第二步:创建切面#

选择好了连接点就可以创建切面了,我们可以把切面理解为一个拦截器,当程序运行到连接点的时候,被拦截下来,在开头加入了初始化的方法,在结尾也加入了销毁的方法而已,在 Spring 中只要使用 @Aspect 注解一个类,那么 Spring IoC 容器就会认为这是一个切面了:

@Component
@Aspect
class Broker {

    @Before("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void before(){
        System.out.println("带租客看房");
        System.out.println("谈钱");
    }

    @After("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void after(){
        System.out.println("给钥匙");
    }
}

切面的类仍然是一个 Bean ,需要 @Component 注解标注

在上面的注解中定义了 execution 的正则表达式,Spring会通过这个正则式去匹配、去确定对应的方法(连接点)是否启用切面编程

execution(* com.aduner.demo03.pojo.Landlord.service())
依次对这个表达式作出分析:

execution:执行方法的时候会触发

  • :任意返回类型的方法
    com.aduner.demo03.pojo.Landlord:类的全限定名
    service():拦截的方法的名称
    如果是service(*) 就是表示任意参数的service方法
    第三步:定义切点#
    每一个注解都重复写了同一个正则式,这显然比较冗余。为了克服这个问题,Spring定义了切点(Pointcut)的概念,切点的作用就是向Spring描述哪些类的哪些方法需要启用AOP编程,这样可以有效的降低代码的复杂度,而且有利于维护的方便。
@Component
@Aspect
class Broker {

    @Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void before() {
        System.out.println("带租客看房");
        System.out.println("谈钱");
    }

    @After("pointcut()")
    public void after() {
        System.out.println("给钥匙");
    }
}

第四步:配置好config#

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.aduner.demo03.*",
        excludeFilters = {@ComponentScan.Filter(classes = {Service.class})})
public class AppConfig {
//加入Java开发交流君样:756584822一起吹水聊天
}

第五步:测试 AOP#

@Test
void testAspect(){
    ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
    Landlord landlord=ctx.getBean(Landlord.class);
    landlord.service();
    ((ConfigurableApplicationContext)ctx).close();
}

结果

……
带租客看房
谈钱
签合同
收钱
给钥匙
……

环绕通知#
现在我们来解决一下前面遗留的那个问题,收钱和给钥匙的问题。

我们需要的应该是给了钥匙之后再收钱,但是现在是反过来的。

要实现这个需求,用到环绕通知,这是 Spring AOP 中最强大的通知,集成了前置通知和后置通知。 【获取资料】

环绕通知(Around)是所有通知中最为强大的通知,强大也意味着难以控制。一般而言,使用它的场景是在你需要大幅度修改原有目标对象的服务逻辑时,否则都尽量使用其他的通知。

环绕通知是一个取代原有目标对象方法的通知,当然它也提供了回调原有目标对象方法的能力。

我们先来修改一下Landlord

@Component
public class Landlord {
    public void service(int steps) {
        if (steps == 1) {
            System.out.println("签合同");
        } else if(steps==2){
            System.out.println("收钱");
        }
        else {
            System.out.println("签合同");
            System.out.println("收钱");
        }

    }
}

我们将service添加一个参数,第一步签合同,第二部收钱,如果没有制定第一步或者第二步,就一起执行。

然后重新编写一下我们的切面

@Component
@Aspect
class Broker {
    @Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service(*))")
    public void pointcut() {
    }
  //加入Java开发交流君样:756584822一起吹水聊天
    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) {
        System.out.println("带租客看房");
        System.out.println("谈价格");

        try {
            // joinPoint.proceed(); 这样就是执行原方法
            joinPoint.proceed(new Object[]{1}); //重新指定方法的参数
            System.out.println("交钥匙");
            joinPoint.proceed(new Object[]{2});
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}
修改一下刚刚的测试类,给到一个初始参数
@Test
void testAspect(){
  ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
  Landlord landlord=ctx.getBean(Landlord.class);
  landlord.service(0);
  ((ConfigurableApplicationContext)ctx).close();
}//加入Java开发交流君样:756584822一起吹水聊天

运行!成功!

……
带租客看房
谈价格
签合同
交钥匙
收钱
……

ProceedingJoinPoint对象
注意到切面编写中Around里面try中的joinPoint.proceed()方法

ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中,添加了以下两个方法。

Object proceed() throws Throwable //执行目标方法 
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 

前面的例子中我们显示把房东的工作分为了两步,然后再环绕通知中重新赋予参数并调用了两次,在两次中间插入了中介的工作。

实际开发中,上面这样的写法其实又会造成新的耦合,而且还会造成其他通知的混乱(调用了两次方法,其实会让有些通知返回两次)。

当然这只是一个例子,为了帮助更好的理解环绕通知。

多个切面#
Spring可以支持多个切面同时运行,如果刚好多个切面的切点相同,切面的运行顺序就是一个关键了。

默认情况下,切面的运行顺序是混乱的,如果需要指定切面的运行顺序,我们需要用到@Order注解

@Component
@Aspect
@Order(1)
public class FirstAspect {
  ……
}
--------------------
@Component
@Aspect
@Order(2)
public class SecondAspect {
  ……
}

@Order注解中的值就是切片的顺序,但是他们不是顺序执行的而是包含关系。

image

总结

  • AOP的出现是为了对程序解耦,减少系统的重复代码,提高可拓展性和可维护性。
  • 常见的应用场景有权限管理、缓存、记录跟踪、优化、校准、日志、事务等等等等……总之AOP的使用是非常常见的。
  • 需要注意不同Spring版本之间的AOP通知顺序是有差别的。补充:Spring5.28为分界线。
  • 环绕通知很灵活、强大,但是也就意味着很难控制,如非必要,优先使用其他通知来完成。
  • 多切面作用同一个切点时候注意切片顺序。


    image

最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

可以的话请给我一个三连支持一下我哟??????【获取资料】

[图片上传失败...(image-df5629-1624524265663)]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容