Spring AOP简介

为什么需要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是一定会被执行的。
执行结果如下

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

推荐阅读更多精彩内容