• spring AOP 之一:spring AOP功能介绍


    一、AOP简介

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

    1.1、AOP几个相关的概念:

    名称 说明
    切面(Aspect) 一个关注点的模块化,这个关注点可能会横切多个对象
    连接点(Joinpoint) 程序执行过程中的某个特定的点。例如类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点。
    通知(Advice) 在切面的某个特定的连接点上执行的动作。(通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。)
    切入点(Pointcut) 匹配连接点的断言,在AOP中通知和一个切入点的表达式。(例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定)
    引入(Introduction) 再不修改类代码的前提下,为类添加新的方法和属性。(也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象))
    目标对象(Target Object) 被一个或多个切面所通知的对象。(需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为“被通知对象”;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象)
    AOP代理(AOP Proxy) AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)
    织入(Weaving) 把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象,     分为:编译时织入、类加载时织入、执行时织入。(将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行)

    把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
    (1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
    (2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码
    (3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术

    1.2、Advice的类型

    名称 说明
    前置通知(Before advice) 在某个连接点(join point)之前执行的通知,但不能阻止连接点前的执行(除非它抛出异常)
    返回后通知(After returning advice) 在某个连接点(join point)正常完成后执行的通知
    抛出异常后通知(After throwing advice) 在方法抛出异常退出时执行的通知
    后通知(After(finally) advice) 当某个连接点退出的时候执行的通知(无论是正常返回还是异常退出)
    环绕通知(Around advice) 包围一个连接点(join point)的通知

    二、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。

    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 来生成代理类。

  • 相关阅读:
    【转】GitHub 中国区前 100 名到底是什么样的人?
    不同服务器数据库之间的数据操作
    行列互换
    千万级数据查询
    用命令对sql进行备份
    通过SQL Server 2008数据库复制实现数据库同步备份
    各种字符串合并处理示例.
    字符串分解
    四大排序函数
    cross apply 和 outer apply
  • 原文地址:https://www.cnblogs.com/duanxz/p/6754606.html
Copyright © 2020-2023  润新知