zoukankan      html  css  js  c++  java
  • 面向切面的Spring

    在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来说,这些横切关注点从概念上是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与他们所影响的对象之间解耦。

    AOP的术语

    切面(Aspect)

    横切关注点可以被模块化为特殊的类,这些类可以称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更加简洁,因为它们主要关注业务代码,而次要关注的代码被移入切面中。

    通知(Advice)

    在AOP术语中,切面的工作就被称为通知。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以应用5种类型的通知:

    • 前置通知(Before):在目标方法被调用之前调用通知功能
    • 后置通知(After):在目标方法完成之后调用通知,不关心方法的输出是什么
    • 返回通知(After-returning):在目标方法成功执行之后调用
    • 异常通知(After-throwing):在目标方法抛出异常之后调用
    • 环绕通知(Around):通知包裹被通知方法,在被通知的繁华调用之前和之后执行自定义的行为

    连接点(Join point)

    连接点是应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至修改一个字段时。切点代码可以利用这些点插入到应用的正常流程中,并添加新的行为。

    切点(Poincut)

    一个切点并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。切点的定义,我们需要使用明确的类和方法名称或者利用正则表达式来指定切点(切点表达式)

    织入(Weaving)

    织入是把切面应用到目标对象并创建新的代理的过程。切面在指定的连接点被织入到目标对象中。目标对象的生命周期你有多个阶段可以被织入:

    • 编译期:切面在目标类编译阶段被织入。这种方式需要特殊的编译器。
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器。
    • 运行期:切面在应用运行的某个阶段被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。

    AspectJ这三种方式方式都支持,Spring AOP只支持在运行期织入切面。

    引入(introduction)

    AOP的作用就在于增强目标对象现有的属性或方法,而引入允许我们向现有的类中添加新方法或属性。

    Spring 对AOP的支持

    并不是所有AOP框架都是相同的,它们在连接点模型上可能有强弱之分,它们织入切面的方式和时机也会有不同。但是无论如何,创建切点来定义切面所织入的连接点是所有AOP框架的基本功能。Spring AOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截,这是Spring作为AOP框架的局限性。

    如果AOP的需求超过了简单的方法调用(如构造器或属性拦截),那么就需要考虑使用AspectJ来实现切面。

    Spring只支持方法级别的连接点

    Spring基于动态代理,所以Spring只支持方法级别的连接点,缺少对字段连接点的支持,无法让我们创建细颗粒度的通知,例如拦截对象字段的修改;而且它不支持构造器连接点,我们无法在bean创建的时候应用通知。

    虽然方法拦截可以满足大部分的需求,但要拦截其他,就需要利用AspectJ来补充Spring AOP的功能。

    通过切点来选择连接点

    在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。AspectJ的切点指示器只有execution是实际用于执行匹配的,其他的只是限制匹配的。execution指示器是我们在编写切点表达式时最主要使用的。

    编写切点

    execution(返回类型 全限定的类.方法(参数))
    
    例如: execution(* cn.lynu.Performance.perform(..))

    表达式以"*"号开始,表明可以返回任意类型,然后我们使用全限定的类名和方法名,对于方法参数列表,我们使用两个点号(..)表明该方法可以使用任意入参

    我们还可以使用限制的指示器来匹配,例如使用execution()和within()限制切点。使用的是“&&”操作符进行连接:

     execution(* cn.lynu.Performance.perform(..) && within(cn.*))

    类似的还可以使用“||”运算符表示或的关系,“!”运算符表示非的关系。

    但是因为“&&”在XML中有特殊含义,Spring的XML配置里面使用切点可以使用and替代“&&”,or和not分别替代“||” 和“!”.

    使用Java代码定义切面

    使用AspectJ的@Aspect注解表明一个Jav类作为切面,这个类中的方法都可以使用注解来定义切面的具体行为。AspectJ使用5个注解来对应5中通知方式:

    • @After 后置通知
    • @Before 前置通知
    • @AfterReturning 返回通知
    • @AfterThrowing 异常通知
    • @Around 环绕通知

    所有的这些通知注解都可以使用一个切点表达式作为它的值。

    @Aspect
    public class Audience {
        
        @Before("execution(* test04.Performance.perform(..))")
        public void silenceCellPhone() {
            System.out.println("将手机调置静音");
        }
        
        @Before("execution(* test04.Performance.perform(..))")
        public void taskSeats() {
            System.out.println("观众就坐");
        }
        
        @AfterReturning("execution(* test04.Performance.perform(..))")
        public void applause() {
            System.out.println("鼓掌");
        }
        
        @AfterThrowing("execution(* test04.Performance.perform(..))")
        public void demandRefund() {
            System.out.println("表演失败,观众要求退款");
        }
    
    }

    如果所有的这些切点表达式都是相同的,我们可以使用@Pointcut注解定义个可重用的切点:

        @Pointcut("execution(* test04.Performance.perform(..))")
        public void performance() {}
        
        @Before("performance()")
        public void silenceCellPhone() {
            System.out.println("将手机调置静音");
        }

    performance方法是一个空方法,其本身只是作为一个标识,供@Pointcut注解依附。其实这个已经是切面的Audience,我们依然可以像其他Java类那样使用它的方法,它的方法也可以独立地进行测试,这与其他Java类并没有什么不同。只是使用了@Aspect注解,并不会被视为切面,这些注解也不会解析,也不会转换为切面的代理,还需要启动自动代理功能。

    如果使用的是JavaConfig的话,可以在配置类的类级别上使用@EnableAspectJAutoProxy注解启用:

    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackageClasses= {Performance.class})
    public class Config {
        @Bean
        public Audience audience() {
            return new Audience();
        }
    }    

    如果使用XML来装配bean,就需要使用aop命名空间的<aop:aspect-autoproxy>元素

    <!--启用AspectJ自动代理-->
    <aop:aspectj-autoproxy />
    
    <bean class="cn.Audience"/>

    不要忘了将切面声明为一个Spring bean,不论是用JavaConfig还是XML。

    虽然我们使用了AspectJ的注解来创建切面,但是这个切面依然是基于代理的,它依然是Spring基于代理的切面,仍然受限于代理方法的调用,并不能利用AspectJ所有的能力。

    接下来,我们可以测试这个切面的效果了:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes= {Config.class})
    public class Test {
    
        @Autowired
        private Performance performance;
        
        @org.junit.Test
        public void test01() {
            performance.perform();
        }
    }

    创建环绕通知

    环绕通知是最为强大的通知类型,它能够让所编写的逻辑将被通知的目标方法完全包装起来,事实上就像在一个通知方法中同时编写前置和后置通知。

    @Aspect
    public class Audience {
        
        @Pointcut("execution(* test04.Performance.perform(..))")
        public void performance() {}
        
        //环绕通知
        @Around("performance()")
        public void watchPerformance(ProceedingJoinPoint pj) {
            try {
                System.out.println("关闭手机");
                System.out.println("就坐");
                //调用被通知方法
                pj.proceed();
                System.out.println("鼓掌");
            } catch (Throwable e) {
                System.out.println("表演失败,观众要求退款");
            } 
        }
    }

    注意环绕通知方法的参数是ProceedingJoinPoint作为入参的,这个对象是必须的,因为需要在环绕通知方法中通过它来调用目标方法,使用的是它的proceed()方法,不要忘记调用这个方法,如果不调这个方法,则会阻塞被通知方法的调用。

    处理通知中的参数

    切面所通知的法拉伐确实有参数该怎么办?如何在切面中访问和使用传递给被通知方法的参数?

    我们换一个有参数的切点,并改造切点表达式:

    @Pointcut("execution(* cn.lynu.CompactDisc.playTrack(int)) && args(trackNumber)")
    public void trackPlayed(int trackNumber){}
    
    @Before("trackPlayed(trackNumber)")
    public void before(int trackNumber){
      system.out.print(trackNumber);  
    }

    被通知的方法入参是int类型,并使用args限制器,参数的名称是与切点方法签名中的参数名相匹配的。这样一来,就可以在通知方法中使用传递给切点方法的参数了

    通过注解引入新功能

    之前,我们一直是为目标对象以拥有的方法添加新功能,实际上,利用引入的概念,AOP可以为对象添加新的方法。在Spring中,切面只是实现了它们所包裹的bean所现有接口的代理。如果这些代理可以暴露新的接口,那么目标类看起来也实现了新的接口,即使底层实现类并没有实现这些接口。但调用这个新引入的方法时,代理会把调用传递给实现了新接口的某个对象。

    @Aspect
    public class EncoreableIntroducer {
        @DeclareParents(value="test04.Performance+",defaultImpl=DefaultEncorable.class)
        public static Encoreable encoreable;
    }

    通过@DeclareParents注解,将Encoredable接口引入到Performance bean中。这个注解有三个部分组成:value属性指定了哪种类型bean要引入该接口,标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。defaultImpl属性指定为引入功能提供实现的类。注解所标注的静态属性指明了要引入的接口。

    接下来,我们可以测试调用这个引入的新方法:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes= {Config.class})
    public class Test {
    
        @Autowired
        private Performance performance;
        
        @org.junit.Test
        public void test01() {
            //需要先强转为引入的接口类型,再调用新方法
            Encoreable encoreable=(Encoreable) performance;
            encoreable.performEncore();
        }
        
    }

    运行之后,可以正常使用这个新方法,如果不通过引入的方法,直接强转会出现ClassCaseException。

    使用注解的方式真的太方便了,但是这种方式由一个明显的缺点:必须可以看到和修改源码。如果没有源码,或不想将AspectJ的注解放在代码中,我们就需要使用XML的方式。

    在XML中声明切面

    在Spring aop命名空间中,提供了多个元素用在XML中声明切面:

    AOP配置元素 用途
    <aop:advisor> 定义AOP通知器
    <aop:after> 定义AOP后置通知
    <aop:after-returning> 定义AOP返回通知
    <aop:after-throwing> 定义AOP异常通知
    <aop:around> 定义环绕通知
    <aop:aspect> 定义切面
    <aop:before> 定义前置通知
    <aop:aspectj-autoproxy> 这个之前就见过,是为启用@Aspec注解
    <aop:config> 顶层到的AOP配置,大多数<aop:*>元素必须包裹在<aop:config>元素内
    <aop:declare-parents> 引入
    <aop:pointcut> 定义切点
    <bean id="audience" class="test04.Audience"></bean>    
    
    <aop:config>
         <aop:aspect ref="audience">
             <aop:pointcut expression="execution(* test04.Performance.perform(..))" id="pointcut"/>
             <aop:before pointcut-ref="pointcut" method="silenceCellPhone"/>
             <aop:before pointcut-ref="pointcut" method="taskSeats"/>
             <aop:after-returning pointcut-ref="pointcut" method="applause"/>
             <aop:after-throwing pointcut-ref="pointcut" method="demandRefund"/>
             <aop:around pointcut-ref="pointcut" method="watchPerformance"/>
         </aop:aspect>
        </aop:config>

    关于Spring AOP配置元素,注意的是大多数AOP配置元素必须在<aop:config>元素上下文内使用.这里使用<aop:pointcut>将相同的切点抽取出来,如果通知的切点不一致,在通知中使用pointcut属性而不是pointcut-ref。<aop:pointcut>元素还可以放在<aop:config>元素范围内,提供其他切面使用。

    为通知传递参数

    在AspectJ注解的方式中,我们可以获得目标方法的参数,使用XML的方式也可以:

    <aop:pointcut expression="execution(* test04.CompactDisc.playTrack(int)) and args(trackNumber)" id="pointcut"/>

    只不过在XML中用and or not表示与或非,而不是&& || !

    在XML中引入新功能

    AspectJ中使用的是@DeclareParents注解,在XML中对应的就是Spring aop命名空间中的<aop:declare-parents>元素:

        <bean id="audience" class="test04.Audience"></bean>
        <aop:config>
         <aop:aspect ref="audience">
             <aop:declare-parents types-matching="test04.Performance+" implement-interface="test04.Encoreable" delegate-ref="myPerformance"/>
         </aop:aspect>
        </aop:config>

    最后再说一点,相比较AspectJ,SpringAOP只局限与对方法的增强,AOP的功能较弱,如果需要对对于构造器,属性等类型的切点,就需要直接使用AspectJ。

  • 相关阅读:
    MySQL的Date()函数拼接
    org.osgi.framework.BundleException: Exception in org.eclipse.core.resources.ResourcesPlugin.start()
    js判断对象是否为空对象的几种方法
    json,js中typeof用法详细介绍及NaN、 null 及 undefined 的区别
    将[object Object]转换成json对象
    升级d7的代码到2010以上版本注意事项(SetLength的参数就是字符长度,而不是字节长度,但Move函数要改)
    我是如何用 10 天自学编程,改变一生的?(学习编程的时候,不要死记硬背,要培养感觉)
    Anbox —— 在 Linux 系统中运行 Android 应用
    一定要在commit之前做RAR备份,这样在出问题的时候,可以排除别人代码的干扰
    排序算法总结
  • 原文地址:https://www.cnblogs.com/lz2017/p/8975421.html
Copyright © 2011-2022 走看看