spring AOP 之一:spring AOP功能介绍

一、AOP简介

  AOP(Aspect Oriented Programming):是一种面向切面的编程范式,是一种编程思想,旨在通过分离横切关注点,提高模块化,可以跨越对象关注点。Aop的典型应用即spring的事务机制,日志记录。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等;主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

 

1.1、AOP术语:

AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理(在切面的某个特定的连接点上执行的动作)。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是类初始化、方法执行、方法调用、字段调用或异常的抛出。在 Spring AOP 中,只支持方法执行连接点。通俗讲:joinpoint就是一个允许使用通知的地方。
  • 切点(PointCut): 可以插入增强处理的连接点。通俗讲:其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点。
  • 切面(Aspect):切面是通知和切点的结合。是一个关注点的模块化,这个关注点可能会横切多个对象。通俗讲:切面是一个类,而通知就是类里的方法以及这个方法如何织入到目标方法的方式(用@AfterReturning@Around标注的方法)。我们的例子中只展示了两类通知,根据织入到目标方法方式的不同,AspectJ提供了五种定义通知的标注:
    1. Before:前置通知,在调用目标方法之前执行通知定义的任务
    2. @After:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
    3. @After-returning:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
    4. @After-throwing:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
    5. @Around:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务
     
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。【在不修改类代码的前提下,为类添加新的方法和属性。(也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象))】
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。
    1. 编译时织入:在代码编译时,把切面代码融合进来,生成完整功能的Java字节码,这就需要特殊的Java编译器了,AspectJ属于这一类
    2. 类加载时织入:在Java字节码加载时,把切面的字节码融合进来,这就需要特殊的类加载器,AspectJ和AspectWerkz实现了类加载时织入
    3. 运行时织入:在运行时,通过动态代理的方式,调用切面代码增强业务功能,动态代理会有性能上的开销。
  • 目标(target):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。

概念看起来总是有点懵,并且上述术语,不同的参考书籍上翻译还不一样,所以需要慢慢在应用中理解。

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

二、Spring的AOP实现

AOP实现方案:AspectJ和Spring AOP。
AspectJ:Aspectj是aop的java实现方案,AspectJ是一种编译期的用注解形式实现的AOP。
(1)AspectJ是一个代码生成工具(Code Generator),其中AspectJ语法就是用来定义代码生成规则的语法。基于自己的语法编译工具,编译的结果是JavaClass文件,运行的时候classpath需要包含AspectJ的一个jar文件(Runtime lib),支持编译时织入切面,即所谓的CTW机制,可以通过一个Ant或Maven任务来完成这个操作。
(2)AspectJ有自己的类装载器,支持在类装载时织入切面,即所谓的LTW机制。使用AspectJ LTW有两个主要步骤,第一,通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器;第二,LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类。
(3)AspectJ同样也支持运行时织入,运行时织入是基于动态代理的机制。(默认机制)

见《AspectJ入门

Spring AOP:Spring AOP是AOP实现方案的一种,它支持在运行期基于动态代理的方式将aspect织入目标代码中来实现AOP。但是spring aop的切入点支持有限,而且对于static方法和final方法都无法支持aop(因为此类方法无法生成代理类);另外spring aop只支持对于ioc容器管理的bean,其他的普通java类无法支持aop。现在的spring整合了aspectj,在spring体系中可以使用aspectj语法来实现aop。

 AOP 实现分类

AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,看到这其实应该明白了,AOP 其实就是前面一篇文章讲的代理模式的典型应用。
按照 AOP 框架修改源代码的时机,可以将其分为两类:

    • 静态 AOP 实现, AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
    • 动态 AOP 实现, AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。根据被代理对象是否为接口而采用不同的实现方式,如下图:
    •  

       

下面给出常用 AOP 实现比较

 

2.1、有接口无接口的Spring AOP 实现区别

  1. Spring AOP默认使用标准的javaSE动态代理作为AOP代理,这使得任何接口(或者接口集)都可以被代理
  2. Spring AOP中也可以使用CGLib代理(如果一个业务对象并没有实现一个接口)

2.2、Spring提供了4种实现AOP的方式:

1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面 《spring AOP 之二:@Aspect注解的3种配置
3.AOP标签的纯POJO切面
4.注入式AspectJ切面(编译期注入)见《AspectJ入门

前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

 

示例1

首先写一个接口叫Sleepable,这是一个牛X的接口,所有具有睡觉能力的东西都可以实现该接口(不光生物,包括关机选项里面的休眠)

package com.dxz.aop.demo1;

public interface Sleepable {

    void sleep();
}

然后写一个Human类,他实现了这个接口

package com.dxz.aop.demo1;

public class Human implements Sleepable {
    public void sleep() {
        System.out.println("睡觉了!梦中自有颜如玉!");
    }
}

好了,这是主角,不过睡觉前后要做些辅助工作的,最基本的是脱穿衣服,失眠的人还要吃安眠药什么的,但是这些动作与纯粹的睡觉这一“业务逻辑”是不相干的,如果把这些代码全部加入到sleep方法中,是不是有违单一职责呢?,这时候我们就需要AOP了。
编写一个SleepHelper类,它里面包含了睡觉的辅助工作,用AOP术语来说它就应该是通知了,我们需要实现上面的接口。

package com.dxz.aop.demo1;
import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;

public class SleepHelper implements MethodBeforeAdvice,AfterReturningAdvice{

    public void before(Method mtd, Object[] arg1, Object arg2)
            throws Throwable {
        System.out.println("通常情况下睡觉之前要脱衣服!");
    }

    public void afterReturning(Object arg0, Method arg1, Object[] arg2,
            Object arg3) throws Throwable {
        System.out.println("起床后要先穿衣服!");
    }
    
}

然后在spring配置文件applicationContext-aop1.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"
    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">

    <bean id="human" class="com.dxz.aop.demo1.Human">
    </bean>
    
    <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper">
    </bean>
    
    <bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*sleep" />
    </bean>

    <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="sleepHelper" />
        <property name="pointcut" ref="sleepPointcut" />
    </bean>

    <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="human" />
        <property name="interceptorNames" value="sleepHelperAdvisor" />
        <property name="proxyInterfaces" value="com.dxz.aop.demo1.Sleepable" />
    </bean>
</beans>

测试类:

package com.dxz.aop.demo1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {

    public static void main(String[] args){
        ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop1.xml");
        Sleepable sleeper = (Sleepable)appCtx.getBean("humanProxy");
        sleeper.sleep();
    }
}

程序运行产生结果:

十月 23, 2017 5:12:05 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5197848c: startup date [Mon Oct 23 17:12:05 CST 2017]; root of context hierarchy
十月 23, 2017 5:12:05 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext-aop1.xml]
通常情况下睡觉之前要脱衣服!
睡觉了!梦中自有颜如玉!
起床后要先穿衣服!

OK!这是我们想要的结果,但是上面这个过程貌似有点复杂,尤其是配置切点跟通知,Spring提供了一种自动代理的功能,能让切点跟通知自动进行匹配,修改配置文件如下:

<?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"
    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">

    <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper">
    </bean>
    
    <bean id="sleepAdvisor"
        class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="sleepHelper" />
        <property name="pattern" value=".*sleep" />
    </bean>
    
    <bean id="human" class="com.dxz.aop.demo1.Human">
    </bean>
    
    <bean
        class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
</beans>

执行程序:

    public static void main(String[] args){
        ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop11.xml");
        Sleepable sleeper = (Sleepable)appCtx.getBean("human");
        sleeper.sleep();
    }

成功输出结果跟前面一样!
只要我们声明了org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator就能为方法匹配的bean自动创建代理!

但是这样还是要有很多工作要做,有更简单的方式吗?有!

一种方式是使用AspectJ提供的注解:

package com.dxz.aop.demo2;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;


/**
 * @Aspect的注解来标识切面
 */
@Aspect
//@Component 
public class SleepHelper {

    public SleepHelper() {

    }

    @Pointcut("execution(* *.sleep())")
    public void sleeppoint() {
    }

    @Before("sleeppoint()")
    public void beforeSleep() {
        System.out.println("睡觉前要脱衣服!");
    }

    @AfterReturning("sleeppoint()")
    public void afterSleep() {
        System.out.println("睡醒了要穿衣服!");
    }

}

用@Aspect的注解来标识切面,注意不要把它漏了,否则Spring创建代理的时候会找不到它,@Pointcut注解指定了切点,@Before和@AfterReturning指定了运行时的通知,注意的是要在注解中传入切点的名称。
然后我们在Spring配置文件上下点功夫,首先是增加AOP的XML命名空间和声明相关schema,见配置文件applicationContext-aop2.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-2.0.xsd">

    <aop:aspectj-autoproxy/> 
    
    <bean id="human" class="com.dxz.aop.demo2.Human">
    </bean>
    
    <bean id="sleepHelper" class="com.dxz.aop.demo2.SleepHelper">
    </bean>
</beans>

记得加上这个标签:
<aop:aspectj-autoproxy/> 有了这个Spring就能够自动扫描被@Aspect标注的切面了。

最后是运行,很简单方便了:

    public static void main(String[] args){
        ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop2.xml");
        Sleepable human = (Sleepable)appCtx.getBean("human");
        human.sleep();
    }

下面我们来看最后一种常用的实现AOP的方式:使用Spring来定义纯粹的POJO切面

AOP标签
前面我们用到了<aop:aspectj-autoproxy/>标签,Spring在aop的命名空间里面还提供了其他的配置元素:
<aop:advisor> 定义一个AOP通知者
<aop:after> 后通知
<aop:after-returning> 返回后通知
<aop:after-throwing> 抛出后通知
<aop:around> 周围通知
<aop:aspect>定义一个切面
<aop:before>前通知
<aop:config>顶级配置元素,类似于<beans>这种东西
<aop:pointcut>定义一个切点

我们用AOP标签来实现:

package com.dxz.aop.demo3;

public class SleepHelper {

    public void beforeSleep()
            throws Throwable {
        System.out.println("通常情况下睡觉之前要脱衣服!");
    }

    public void afterSleep() throws Throwable {
        System.out.println("起床后要先穿衣服!");
    }
    
}

代码就不用继承啥了,只是修改配置文件,加入AOP配置即可:

<?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-2.0.xsd">

    <!-- 配置AOP  《方式一》 -->
    <aop:config>
        <!-- 配置切面及通知 -->
        <aop:aspect ref="sleepHelper">
            <aop:before method="beforeSleep" pointcut="execution(public * *..Sleepable.sleep(..))" />
            <aop:after method="afterSleep" pointcut="execution(public * *..Sleepable.sleep(..))" />
        </aop:aspect>
    </aop:config>
    
    
    <!-- 配置AOP  《方式二》-->
    <aop:config>
        <!-- 配置切点表达式 -->
        <aop:pointcut id="pointcut"
            expression="execution(public * *..Sleepable.sleep(..))" />
        <!-- 配置切面及通知 -->
        <aop:aspect ref="sleepHelper">
            <aop:before method="beforeSleep" pointcut-ref="pointcut" />
            <aop:after method="afterSleep" pointcut-ref="pointcut" />
        </aop:aspect>
    </aop:config>
    <bean id="human" class="com.dxz.aop.demo3.Human">
    </bean>

    <bean id="sleepHelper" class="com.dxz.aop.demo3.SleepHelper">
    </bean>
</beans>

 

4 注入AspectJ切面

虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能 比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。 例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语 言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知 应用于对象的创建过程。 对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应 用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。 但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切 面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们 可以借助Spring的依赖注入把bean装配进AspectJ切面中。

示例见《AspectJ入门》中的示例

三、Spring AOP 原理剖析

通过前面介绍可以知道:AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。

AOP 代理所包含的方法与目标对象的方法示意图如图 3 所示。


图 3.AOP 代理的方法与目标对象的方法
图 3.AOP 代理的方法与目标对象的方法 

Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。

纵观 AOP 编程,其中需要程序员参与的只有 3 个部分:

  • 定义普通业务组件。
  • 定义切入点,一个切入点可能横切多个业务组件。
  • 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。

上面 3 个部分的第一个部分是最平常不过的事情,无须额外说明。那么进行 AOP 编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP 框架将会自动生成 AOP 代理,而 AOP 代理的方法大致有如下公式:

代理对象的方法 = 增强处理 + 被代理对象的方法

在上面这个业务定义中,不难发现 Spring AOP 的实现原理其实很简单:AOP 框架负责动态地生成 AOP 代理类,这个代理类的方法则由 Advice 和回调目标对象的方法所组成。

对于前面提到的图 2 所示的软件调用结构:当方法 1、方法 2、方法 3 ……都需要去调用某个具有“横切”性质的方法时,传统的做法是程序员去手动修改方法 1、方法 2、方法 3 ……、通过代码来调用这个具有“横切”性质的方法,但这种做法的可扩展性不好,因为每次都要改代码。

于是 AOP 框架出现了,AOP 框架则可以“动态的”生成一个新的代理类,而这个代理类所包含的方法 1、方法 2、方法 3 ……也增加了调用这个具有“横切”性质的方法——但这种调用由 AOP 框架自动生成的代理类来负责,因此具有了极好的扩展性。程序员无需手动修改方法 1、方法 2、方法 3 的代码,程序员只要定义切入点即可—— AOP 框架所生成的 AOP 代理类中包含了新的方法 1、访法 2、方法 3,而 AOP 框架会根据切入点来决定是否要在方法 1、方法 2、方法 3 中回调具有“横切”性质的方法。

简而言之:AOP 原理的奥妙就在于动态地生成了代理类,这个代理类实现了图 2 的调用——这种调用无需程序员修改代码。接下来介绍的 CGLIB 就是一个代理生成库,下面介绍如何使用 CGLIB 来生成代理类。

 

posted on 2017-04-23 23:13  duanxz  阅读(3584)  评论(0编辑  收藏  举报