Spring AOP简介

2020-04-11  本文已影响0人  LENN123
为什么需要AOP?

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

横切关注点
上图中上,Class AClass BClass C这三个不同的类,却都需要在某个方法执行前进行权限验证,在执行后进行日志记录。这样横跨了多个类的共同需求,我们称为横切关注点。在这里显然varify()log()在多个类中重复,当然重复代码还不是最主要的问题,当我们需要修改verify()log()方法时,我们要在A、B、C三个类中都进行修改,当类的数目越来越多,就会牵一发而动全身。那么有人会说,我们可以把verify()log()抽象成一个类,如果需要进行修改时,就在这个类中进行。这个方案似乎可行,但是仍然存在问题 业务逻辑横切逻辑纠缠
  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,表示吃之前要执行的横切逻辑。

public class People {
    public void eatFruit(){
        System.out.println("正在吃水果");
    }
}
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包

    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中的方法抛出异常时,将会执行的横切逻辑。

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个打包在一起的参数是可选的,如果你想获得更详细的信息,就加上它们。

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的所有功能,即可以同时在上述的所有位置执行横切逻辑。
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是一定会被执行的。
执行结果如下

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

猜你喜欢

热点阅读