精华内容
下载资源
问答
  • Unity3D摄像远、近切面绘制

    千次阅读 2019-08-15 13:00:05
    最近在研究unity3D的Debug.DrawLine, 就利用该函数绘制了U3D上Mian Camera---Perspective模式下的近切面、远切面。思路来源:https://www.xuanyusong.com/archives/3036 接下来回顾一下unity3D camera绘制远、近...

    最近在研究unity3D的Debug.DrawLine, 就利用该函数绘制了U3D上Mian Camera---Perspective模式下的近切面、远切面。思路来源:https://www.xuanyusong.com/archives/3036

    接下来回顾一下unity3D camera绘制远、近切面的相关组件:

    (1)首先选择摄像机的投影方式,Camera包括透视投影、正交投影两种(如下图),选择透视投影

             1.orthographic正交摄像机:投影线垂直于投影面,也叫平行投影;

             2.perspective透视摄像机:犹如我们的眼睛一样,会根据距离的远近显示 大小;

                 

    (2)Field of View   视角(只有在透视投影时才有的特性)

             即: 视角越大,能看到的视野也越大,对应的焦距也越短

    (3)Clipping Planes 裁剪平面,Near和Far指定了裁剪的区域范围

           远近裁剪平面和Field Of view决定的平面一起构成一个椎体,被称为相机椎体或视椎体(如下图)

    (4)接下来新建脚本Draw.cs将其挂在maincamera上:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class Draw : MonoBehaviour
    {

        private Camera theCamera;

        public float NearDistance = 0.3f;

        public float FarDistance = 1000.0f;
        private Transform tx;

        // Start is called before the first frame update
        void Start()
        {
            if (!theCamera)
            {
                theCamera = Camera.main;
            }
            tx = theCamera.transform;
        }

        // Update is called once per frame
        void Update()
        {
            FindUpperCorners();
            FindLowerCorners();
        }
        void FindUpperCorners()
        {
            NearDistance = theCamera.GetComponent<Camera>().nearClipPlane;
            Vector3[] corners = GetCorners(NearDistance);

            // for debugging
            Debug.DrawLine(corners[0], corners[1], Color.yellow); // UpperLeft -> UpperRight
            Debug.DrawLine(corners[1], corners[3], Color.yellow); // UpperRight -> LowerRight
            Debug.DrawLine(corners[3], corners[2], Color.yellow); // LowerRight -> LowerLeft
            Debug.DrawLine(corners[2], corners[0], Color.yellow); // LowerLeft -> UpperLeft
        }

        void FindLowerCorners()
        {
            FarDistance = theCamera.GetComponent<Camera>().farClipPlane;
            Vector3[] corners = GetCorners(FarDistance);

            // for debugging
            Debug.DrawLine(corners[0], corners[1], Color.red);
            Debug.DrawLine(corners[1], corners[3], Color.red);
            Debug.DrawLine(corners[3], corners[2], Color.red);
            Debug.DrawLine(corners[2], corners[0], Color.red);
        }


        Vector3[] GetCorners(float distance)
        {
            Vector3[] corners = new Vector3[4];

            float halfFOV = (theCamera.fieldOfView * 0.5f) * Mathf.Deg2Rad;
            float aspect = theCamera.aspect;

            float height = distance * Mathf.Tan(halfFOV);
            float width = height * aspect;

            // UpperLeft
            corners[0] = tx.position - (tx.right * width);
            corners[0] += tx.up * height;
            corners[0] += tx.forward * distance;

            // UpperRight
            corners[1] = tx.position + (tx.right * width);
            corners[1] += tx.up * height;
            corners[1] += tx.forward * distance;

            // LowerLeft
            corners[2] = tx.position - (tx.right * width);
            corners[2] -= tx.up * height;
            corners[2] += tx.forward * distance;

            // LowerRight
            corners[3] = tx.position + (tx.right * width);
            corners[3] -= tx.up * height;
            corners[3] += tx.forward * distance;

            return corners;
        }

    }
    (5)运行效果

    展开全文
  • 使用注解来创建切面是 AspectJ 5 所引入的关键特性。我们前一篇已经定义了 Performance 接口,它是切面中切点的目标对象。Q:如何定义切面?A: 给出一个描述:如果一场演出没有观众的话,那就不能称之为演出。对...

    使用注解来创建切面AspectJ 5 所引入的关键特性。

    我们前一篇已经定义了 Performance 接口,它是切面中切点的目标对象

    Q:如何定义切面?

    A: 给出一个描述:如果一场演出没有观众的话,那就不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来将,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

    之前需要在 build.gradle 导入 aspectjrt

    // https://mvnrepository.com/artifact/org.aspectj/aspectjrt
    compile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.0.BETA-6'
    package concert;
    
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    /**
     * 该类使用 @AspectJ 注解进行了标注。该注解表明该类不仅仅是一个 POJO,还是一个切面。
     * 该类中的方法都使用注解来定义切面的具体行为。
     */
    @Aspect
    public class Audience {
    
        //表演之前:将手机调至静音状态
        @Before("execution(**concert.Performance.perform(..))")
        public void silenceCellPhones() {
            System.out.println("Silencing cell phones");
        }
    
        //表演之前:就座
        @Before("execution(**concert.Performance.perform(..))")
        public void takeSeats() {
            System.out.println("Taking seats");
        }
    
        //表演之后:精彩的话,观众应该会鼓掌喝彩
        @AfterReturning("execution(**concert.Performance.perform(..))")
        public void applause() {
            System.out.println("CLAP CLAP CLAP!!!");
        }
    
        //表演失败之后:没有达到观众预期的话,观众会要求退款
        @AfterThrowing("execution(**concert.Performance.perform(..))")
        public void demandRefund() {
            System.out.println("Demanding a refund");
        }
    
    }

    可以简化一下,使用 @Pointcut 注解。

    package concert;
    
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    
    /**
     * 简化方案:@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。
     */
    @Aspect
    public class Audience {
    
        //通过在该方法上添加 @Pointcut 注解,我们实际上扩展了切点表达式语言,
        //这样就可以在任何的切点表达式中使用 performance()了。
        //该方法内容并不重要,实际上应该是空的。只是一个标识,供 @Pointcut 注解依附。
        @Pointcut("execution(**concert.Performance.perform(..))")
        public void performance(){}
    
        //表演之前:将手机调至静音状态
        @Before("performance()")
        public void silenceCellPhones() {
            System.out.println("Silencing cell phones");
        }
    
        //表演之前:就座
        @Before("performance()")
        public void takeSeats() {
            System.out.println("Taking seats");
        }
    
        //表演之后:精彩的话,观众应该会鼓掌喝彩
        @AfterReturning("performance()")
        public void applause() {
            System.out.println("CLAP CLAP CLAP!!!");
        }
    
        //表演失败之后:没有达到观众预期的话,观众会要求退款
        @AfterThrowing("performance()")
        public void demandRefund() {
            System.out.println("Demanding a refund");
        }
    
    
    }
    

    AspectJ 提供了五个注解来定义通知。
    这里写图片描述

    package concert;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * 如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 @EnableAspectJAutoProxy 注解启用自动代理功能。
     */
    @Configuration
    @EnableAspectJAutoProxy //启用 AspectJ 自动代理
    @ComponentScan
    public class ConcertConfig {
    
        @Bean
        public Audience audience(){  // 声明 Audience bean
            return new Audience();
        }
    }
    

    或者 XML 中配置:使用 Spring aop 命名空间中的 aop:aspectJ-autoproxy 元素。

    <?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.xsd">
    
        <context:component-scan base-package="concert"/>
    
        <!-- 启用 AspectJ 自动代理 -->
        <aop:aspectj-autoproxy/>
    
        <!-- 声明 Audience bean -->
        <bean class="concert.Audience"/>
    
    </beans>

    不管使用 JavaConfig 还是 XML,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为 Concert bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。

    需要记住的是:Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。本质上,它依然是 Spring 基于代理的切面。这就意味着,尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖于 Spring 来创建基于代理的切面

    Q:如何创建环绕通知?

    A: 环绕通知是最为强大的通知类型。实际上就像在一个通知方法中同时编写前置通知和后置通知。

    package concert;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    /**
     * 使用环绕通知重新实现 Audience 切面
     * 这个通知所达到的效果跟之前的前置后置通知是一样的。但是,现在它们位于一个方法中。
     */
    @Aspect
    public class Audience {
    
        // 定义命名的切点
        @Pointcut("execution(**concert.Performance.perform(..))")
        public void performance(){}
    
        /**
         * 环绕通知方法
         * @param joinPoint 这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。
         */
        @Around("performance()")
        public void watchPerformance(ProceedingJoinPoint joinPoint){
            try {
                System.out.println("Silencing cell phones");
                System.out.println("Taking seats");
                // 通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用该方法。
                // 如果不调用这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。
                // 有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。
                joinPoint.proceed();
                System.out.println("CLAP CLAP CLAP!!!");
            } catch (Throwable throwable) {
                System.out.println("Demanding a refund");
            }
        }
    
    }
    

    对于 proceed() 方法,你也可以在通知中对它进行多次调用,这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试

    Q:如何处理通知的参数?(切面能访问和使用传递给被通知方法的参数吗?)

    A:如下列代码所示:BlankDisc.java & CompactDisc.java 参考 Spring 如何通过 XML 装配 bean?,并加以修改 play() 方法。

    package concert;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 假设你想记录每个磁道被播放的次数。由于记录磁道的播放次数与播放本身是不同的关注点,因此应该是切面要完成的任务。
     * 使用参数化的通知来记录磁道播放的次数
     */
    @Aspect
    public class TrackCounter {
    
        // 记录每个磁道播放次数
        private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
    
        // 通知 play() 方法,在切点表达式中声明参数,这个参数传入到通知方法 countTrack() 中
        // args(trackNumber) 表明传递给 play() 方法的 int 类型参数也会传递到通知中(countTrack()方法)去。
        // 参数名称与切点方法签名中的参数相匹配。
        @Pointcut("execution(* soundsystem.CompactDisc.play(int)) && args(trackNumber)")
        public void trackPlayed(int trackNumber) {
        }
    
        // 在播放之前,为该磁道计数
        // 这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。
        // 切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
        @Before("trackPlayed(trackNumber)")
        public void countTrack(int trackNumber) {
            int currentCount = getPlauCount(trackNumber);
            trackCounts.put(trackNumber, currentCount + 1);
        }
    
        public int getPlauCount(int trackNumber) {
            return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
        }
    
    }
    
    package soundsystem;
    
    import concert.TrackCounter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 在 Spring 配置中将 BlankDisc 和 TrackCounter 定义为 bean,并启用自动代理。
     * 配置 TrackCount 记录每个磁道播放的次数
     */
    @Configuration
    @EnableAspectJAutoProxy // 启用 AspectJ 自动代理
    public class TrackCounterConfig {
    
        @Bean
        public CompactDisc sgtPeppers(){
            BlankDisc cd = new BlankDisc();
            cd.setTitle("titleValue01");
            cd.setArtist("ArtistValue01");
            List<String> tracks = new ArrayList<String>();
            tracks.add("trackValue01");
            tracks.add("trackValue02");
            tracks.add("trackValue03");
            tracks.add("trackValue04");
            tracks.add("trackValue05");
            cd.setTracks(tracks);
            return cd;
        }
    
        @Bean
        public TrackCounter trackCounter(){
            return new TrackCounter();
        }
    
    }
    

    Q:如何通过注解引入新功能?

    A: Java 不是动态语言,一旦类编译完成了,我们就很难再为该类添加新功能了。但是我们可以使用切面,为对象拥有的方法添加了新功能,而没有为对象添加任何新的方法。实际上,利用被称为引入的 AOP 概念,切面可以为 Spring bean 添加新的方法

    这里写图片描述

    注意:当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个 bean 的实现被拆分到了多个类中

    代码如下:

    package concert;
    
    /**
     * 为所有的 Performance 实现引入下面的 Encoreable 接口
     *
     * 需要将这个接口应用到 Performance 实现中。
     * 两个问题:不能直接实现 Encoreable 接口,并不是所有的 Performance 都是具有 Encoreable 特性的;
     *           也有可能无法修改所有的 Performance 实现(使用第三方实现并且没有源码时)。
     * 解决方案:借助于 AOP 的引入功能,我可以避免以上两个问题。
     */
    public interface Encoreable {
        void performEncore();
    }
    
    package concert;
    
    /**
     * Encoreable 接口的实现类
     */
    public class DefaultEncoreable implements Encoreable {
        @Override
        public void performEncore() {
    
        }
    }
    
    package concert;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.DeclareParents;
    
    /**
     * 创建一个切面
     */
    @Aspect
    public class EncoreableIntroducer {
    
        // 通过 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。
        // @DeclareParents 注解由三部分组成:
        // ①、value 属性指定了哪种类型的 bean 要引入该接口。
        //     此处 value 的值,代表所有实现 Performance 的类型。(标记符后面的 "+" 表示是 Performance 的所有子类型,而不是它本身)
        // ②、defaultImpl 属性指定了为引入功能提供实现的类。这里,指定的是 DefaultEncoreable 提供实现。
        // ③、@DeclareParents 注解所标注的静态属性指明了要引入的接口。这里,所引入的是 Encoreable 接口。
        @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class)
        public static Encoreable encoreable;
    
    }
    
        <!-- 将 EncoreableIntroducer 声明为一个 bean -->
        <bean class="concert.EncoreableIntroducer"/>

    Spring 的自动代理机制将会获取到它的声明,当 Spring 发现了一个 bean 使用了 @Aspect 注解时,Spring 就会创建一个代理,然后将调用委托给被代理的 bean被引入的实现,这取决于调用的方法属于被代理的 bean 还是属于被引入的接口

    面向注解的切面声明有一个明显的劣势你必须能够为通知类添加注解。为了做到这一点,必须要有源码。

    如果没有源码的话,或者不想将 AspectJ 注解放到你的代码之中,我们可以在 Spring XML 配置文件中声明切面。

    上一篇: 面向切面的 Spring —— 如何通过切点来选择连接点?
    下一篇:面向切面的 Spring —— 如何在 XML 中声明切面?

    展开全文
  • Spring面向切面编程

    千次阅读 2017-09-23 17:38:26
    编写代码的时候,“切面”代码并不放在业务方法中,但是程序运行的时候,Spring会拦截到方法的执行,并运行这些“切面”代码。 OOP 引人了封装、继承及多态性等概念来建立对象层次结构,用于模拟公共行为的集合。在...

           JAVA就业套餐课:https://edu.csdn.net/combo/detail/1230

           第1章主要介绍了Spring管理实体对象的应用,通过ApplicationContext容器来了解Spring管理实体对象的原理以及设值注入、构造方法及自动注入等不同的注入方式。本章先介绍为什么需要AOP以及使用AOP的好处,然后采用手动代理的方式介绍什么是代理及代理的必要性,最后结合商场手机进货和收获的案例分别介绍前置通知、后置通知、环绕通知和异常通知。在介绍前置通知的时候,分别采用Spring1.x和Spring2.x的方式进行配置,为后续课程的学习打下铺垫。


    核心技能部分

     

    第1章 

    1.1 Spring AOP简介

    1.1.1 为什么需要AOP

    通过依赖注入,在编写程序的时候,我们不必关心依赖的组件如何实现,然而在实际开发过程中我们还需要将程序中涉及的公共问题集中解决,如图2.1.1所示。

     

    图2.1.1 Spring的两个重要模块

    看下面的一个应用:

     

    public void doSameBusiness (long lParam,String sParam){
    // 记录日志
    	log.info("调用 doSameBusiness方法,参数是:"+lParam);
    	// 输入合法性验证
    	if (lParam<=0){
    		throws new IllegalArgumentException("xx应该大于0");
    	}
    	try{ 
    		//真正的业务逻辑代码
    	    	//事务控制
    	}catch(){
    		tx.rollback();
    	}
    	// 事务控制
    	tx.commit();
    }

     

    这是一个典型的业务处理方法。日志、参数合法性验证、异常处理、事务控制等都是一个健壮的业务系统所必需的;否则系统出现问题或者有错误的业务操作时没有日志可查;传入的出库参数为负数,出库反而导致库存增加;转账时,打款方的钱已经被扣了,方法却异常退出,而收款方还没有收到,已经进行的交易没有回滚。这样的系统显然是没有人敢用的。

    为了保证系统健壮可用,就要在每个业务方法里都反复编写这些代码,如果需要修改,则每个业务方法都需要修改,这样的代码质量很难保障。

    我们怎样才能把心用在真正的业务逻辑上呢?这就是AOP要解决的问题。

    1.1.1 什么是AOP

    AOP 是Aspect-Oriented Programming的简称,意思是面向切面编程。AOP是对OOP的补充和完善。比如刚才的问题,程序中所有的业务方法都需要日志记录、参数验证、事务处理,这些公共的处理如果放在每个业务方法里,系统会变的臃肿,而且很难去维护。散布在系统各处的需要在实现业务系统时关注的事情就被成为“切面”,也称为关注点,AOP的思想就是把这些公共部分从业务方法中提取出来,集中处理。

    编写代码的时候,“切面”代码并不放在业务方法中,但是程序运行的时候,Spring会拦截到方法的执行,并运行这些“切面”代码。

    OOP 引人了封装、继承及多态性等概念来建立对象层次结构,用于模拟公共行为的集合。在OOP思想中,代码的重复可以被提取出来放在父类中被复用。但是方法中的重复代码,OOP却无能为力。例如,在日志信息记录中,日志代码常水平地散布在所有对象层次中,与对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理以及透明的持续性也同样如此。散布在各处且无关的代码称为横切 (Cross-cutting) 代码,这种代码在OOP 的设计中导致了大量重复且不利于各个模块重用。

    AOP技术则刚好相反,它代表横向关系,通过利用“横切”技术解剖封装的对象并获得其内部,将影响多个类的公共行为封装至可重用模块并命名为“Aspect”(切面)。简而言之,即 AOP技术将与业务无关却由业务模块共同调用的逻辑进行封装,减少系统的重复代码,降低模块间的耦合度,提高系统的可操作性及可维护性。若将“对象”视为空心的圆柱体,将属性与行为都封装于柱体中,则 AOP技术的作用类似于刀,将空心圆柱体剖开并获得内部的消息,剖开的面即“切面”。

    通过使用“横切”技术,AOP将软件系统分为两个部分:核心关注点与横切关注点。业务处理的主要流程即核心关注点,与之关系较小的部分就是横切关注点。横切关注点常发生在核心关注点的多处且各处基本相似,如权限认证、日志、事务处理等。AOP 的作用在于分离系统中的各种关注点,将核心关注点与横切关注点进行分离,其核心思想是将应用程序中的商业逻辑与向其提供支持的通用服务进行分离。

    AOP 中包含许多新的概念与术语,说明如下:

    (1)切面 (Aspect):切面是系统中抽象出来的的某一个功能模块。

    (2)连接点 (Joinpoint):程序执行过程中某个特定的点,如调用某方法或者处理异常时。在SpringAOP中,连接点总是代表某个方法的执行。

    (3)通知 (Advice):通知是切面的具体实现,是放置“切面代码”的类。

    (4)切入点 (Pointcut):多个连接点组成一个切入点,可以使用切入点表达式来表示。

    (5)目标对象 (Target Object):包含一个连接点的对象,即被拦截的对象。

    (6)AOP代理 (AOP Proxy):AOP框架产生的对象,是通知和目标对象的结合体。

    (7)织入 (Weaving):将切入点和通知结合的过程称为织入。

    1.1 AOP代理

    在生活中,“代理”一词出现的频率较高,并且现实事物能够形象、直观地反映代理模式的抽象过程及本质。下面我们以购买手机和出售房屋为例,初步分析“代理”的概念。

    众所周知,手机或电脑都可以直接从厂家购买,但是这种方式却很少使用。因为一般情况下,顾客都希望在购买时获得额外的充值卡、鼠标等,厂家却很少提供,但是顾客可以在代理商处获得此类额外物件。房屋买卖中同样存在类似情况:房屋待售时,屋主可以通过互联网发布出售信息寻找买家,同咨询者看房、洽谈、交易、过户以实现成交,需要占用卖家大量时间;此外还可以交给中介管理,由其处理琐碎的交易过程。实际上,中介即卖家的代理。

    接下来我们看一个需求,某手机商店需要购买手机和销售手机,现要求购买和销售手机时记录日志。

    根据需求我们知道,本系统有两个业务操作:购买手机和销售手机。两个业务方法都用着相同的日志操作,因此日志操作为该系统的“切面”代码,我们可以将这些代码从业务方法中分离出来。我们可以定义一个手机业务接口、一个手机业务接口实现和一个日志操作类。手机接口和手机接口的实现如示例2.1所示。

    示例2.1

     

    //手机业务接口
    public interface PhoneBiz {
    public void buyPhone(int num);//购买手机
    public void salePhone(int num);//卖手机
    }
    //手机业务接口实现
    public class PhoneBizImpl implements PhoneBiz {
    	public void buyPhone(int num) {
    		System.out.println("手机进货,进货数量 为" + num + "部");
    	}
    	public void salePhone(int num) {
    		System.out.println("销售手机,销售数量为" + num + "部");
    	}
    }

     

    日志操作类如示例2.2所示。

    示例2.2

    public class LogUtil {
    	public void log(String type,int num) {
    		System.out.println("日志:"+currentTime()+type+"手机"+num+"部...");
    	}
    	public String currentTime() {
    		SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    		return sdf.format(new Date());
    	}
    }

    接下来我们编写一个测试类,对系统的业务方法进行测试,测试代码如示例2.3所示。

    示例2.3

    PhoneBiz phoneBiz=new PhoneBizImpl();//创建目标对象
    phoneBiz.buyPhone(10);
    phoneBiz.salePhone(3);

     

    上面的测试结果可以实现手机的进货和销售,但是日志记录却无法实现,原因是目标对象的方法没有日志相关的代码(日志代码从业务方法中提取出去了),那么如何能在程序运行过程中加入“切面”代码(日志)呢?这个时候,代理的作用就体现出来了,我们可以设计一个针对目标对象的代理。手机业务代理类如示例2.4所示。

     

    示例2.4

    public class PhoneBizImplProxy implements PhoneBiz {
    	private PhoneBiz phoneBiz=new PhoneBizImpl();// 目标对象
    	private LogUtil logUtil=new LogUtil();// 日志操作对象
    	public void buyPhone(int num) {
    		phoneBiz.buyPhone(num);// 调用目标对象的手机进货方法
    		logUtil.log("购买", num);//日志操作
    	}
    	public void salePhone(int num) {
    		phoneBiz.salePhone(num);// 调用目标对象的手机销售方法
    		logUtil.log("销售", num);//日志操作
    	}
    	//setter & getter
    }

    通过上面的代码我们知道,该代理类中包含两个重要属性:目标对象和“切面对象”。

    要想把公共代码提取出来,又在业务方法运行的时候加上公共代码,我们可以调用代理对象的方法。测试代码如示例2.5所示。

    示例2.5

    PhoneBiz phoneBiz=new PhoneBizImplProxy();//创建代理对象
    phoneBiz.buyPhone(10);//调用代理对象的进货方法
    phoneBiz.salePhone(3);//调用代理对象的销售方法

     

    综上,代理通常可以作为目标对象的替代品,且提供更加强大的功能。代理包含目标Bean的所有方法,代理就是目标对象的加强,通过以目标对象为基础来增加属性与方法,提供更加强大的功能。AOP代理是由 AOP框架创建的对象。

     

    如果在Spring项目中使用AOP,则需要在Spring项目中添加Spring Libraries类库。

    1.1 Spring AOP的实现

    本章继续以2.2节中手机商店的进货和销售业务为例,以日志管理为需求,讲解Spring AOP技术的具体使用方法。

    1.1.1 引入AOP命名空间

    首先我们需要在Spring应用容器配置文件中引入Spring AOP命名空间,否则无法在Spring容器配置文件中使用AOP相关xml标签。如示例2.6所示,将其中加粗部分代码,添加在你的applicationContext.xml中即可:

    示例2.6 

    <beans xmlns=http://www.springframework.org/schema/beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    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-3.0.xsd 
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    
    </beans>

     

    1.1.1 目标对象(Target Object)

     

    包含业务关注点(本例是需要日志管理的点)的对象,称为目标对象。在本例中,我们的目标对象是手机业务对象,即s3spring.ch2.biz.impl.PhoneBizImpl类的对象,它实现s3spring.ch2.biz.PhoneBiz接口,我们需要在applicationContext.xml中将其配置成Spring bean,如示例2.7所示:

    示例2.7

    <bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>

    1.1.2 连接点(JoinPoint)

    程序执行过程中的某个业务关注点(本例是需要日志管理的点)被称作连接点(JoinPoint)。在Spring AOP中,连接点总是代表某个方法的执行。在本例中PhoneBizImpl类的buyPhone(int)方法和salePhone(int)的执行都是连接点。

    1.1.3 切入点(PointCut)

    多个连接点组成一个切入点。我们用切入点表达式来描述多个连接点的集合。为了选择方法,我们可以使用下面这些条件来构造切入点表达式:

    切入点指示符:如execution,最常用的切入点指示符,表示方法的执行。

    布尔运算符:AND(&&)、OR(||)和NOT(!),可以将多个表达式组合成一个新的表达式来缩小选择连接点的范围。

    通配符:星号(*),用于匹配任何方法、类名或者参数类型。双点号(..),用于表示0个或者多个参数类型。

    方法可见性修饰符:将选择连接点的范围缩小到某种可见性的方法。如public的。

    方法返回类型:如void、int、String等,也可以使用*,表示所有类型。

    类名:指定完整的类名(包括完整的包名称,如java.lang.String),将选择连接点的范围缩小到某个目标类。

    方法名称:可以是全名。也可以是*(任何名称)。也可以是部分名称结合通配符,如get*,即所有名称以get开头的方法。

    方法声明抛出的异常:如 throws java.lang.IOException。

    使用execution切入点指示符描述连接点范围时,除了方法返回类型、方法名称和方法参数是必须描述的以外, 其它所有的部分都是可选的(方法可见性修饰符、类名、方法声明抛出的异常)。

    示例2.8表达式描述了PhoneBizImpl类名字以Phone结尾,参数只有一个且类型为int,返回类型为void,可见性为public的方法的执行,当然也包括了buyPhone(int)方法和salePhone(int)方法。

    示例2.8  

    execution( public void s3spring.ch2.biz.impl.PhoneBizImpl.*Phone(int) )

    如果想描述所有业务类的所有业务方法,可以象示例2.9这样描述:

    示例2.9

    execution(* s3spring.ch2.biz.impl.*.* (..) )

    示例2.9中第一个*表示任意返回类型。第二个*表示任意类名。第三个*表示任意方法名称。..表示0或多个任意类型的方法参数。

    示例2.9也可以用within切入点指示符进行简化。within表示某一范围内的连接点,如某包内,某类内。示例2.10表示s3spring.ch2.biz.impl包中所有的连接点:

    示例2.10

    within( s3spring.ch2.biz.impl.* )

    示例2.11表示s3spring.ch2.biz.impl.PhoneBizImpl类中所有的连接点:

    示例2.11

    within( s3spring.ch2.biz.impl.PhoneBizImpl )

    示例2.12表示s3spring.ch2.biz.impl 包及其所有子孙包中所有的连接点:

    示例2.12

    within( s3spring.ch2.biz.impl.PhoneBizImpl..* )

    1.1.4 切面(Aspect)

    切面是系统中抽象出来的的某一个系统服务功能模块。在本例中是日志管理模块,它可以保含多个实现日志管理操作的通知。我们用一个POJO (Plain Old Java Ojbect,简单普通的Java对象)类来表示抽象的切面,用方法表示通知,并把切面类配置成一个Spring bean。示例2.13 是日志管理切面类,目前它还没有包含任何通知:

    示例2.13

    package s3spring.ch2.log;

    public class LogAspect {

    }

    此时,LogAspect仅仅是一个类,还不能代表一个切面,我们接着需要把它配置成一个Spring bean,请看示例:

    示例2.14

    <bean id="logAspectBean" class="s3spring.ch2.log.LogAspect"></bean>

    然后,我们可以利用示例2.6 引入的Spring AOP命名空间将“logAspectBean” bean 配置成一个切面。请看示例2.15:

    示例2.15

    <aop:config>

         <!-- 定义一个可以被多个切面共享的切入点 -->

    <aop:pointcut id="p1" expression="execution( void *Phone(int))"/>

         <!-- 定义一个切面 -->

    <aop:aspect id="logAspect" ref="logAspectBean"></aop:aspect>

    </aop:config>

    如示例2.15所示,切面、切入点等AOP相关内容必须定义在<aop:config>元素内部,且可以配置多个切面、多个切入点。

    我们首先使用<aop:pointcut>标签定义了一个id为p1的切入点,这个切入点的表达式描述了日志管理所关注的连接点——所有返回类型为void,名称以Phone结尾,参数只有一个且类型为int的方法。

    将切入点直接定义在<aop:config>元素内部,而不是<aop:aspect>标签内部的好处是它可以被多个切面共享。例如该切入点即被日志管理模块关注,又被事务管理模块关注。当然,你也可以将关注点定义在某个<aop:aspect>标签内部,这时该切入点就成为这个切面内部独享的私有切入点了。

    最后,我们用<aop:aspect>标签,通过ref属性指定id为 “logAspectBean”的bean为一个切面,并为该切面设置id为“logAspect”。

    1.1.5 通知(Advice)

    通知(Advice)是在切面的某个特定的连接点上执行的具体操作。按照执行的时机可以分为下面几种:

    前置通知(Before):在某连接点之前执行的通知,它可以阻止连接点的执行。例如检查权限,决定是否执行连接点。

    后置通知(AfterReturning):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。它可以访问方法返回值。

    异常通知(AfterThrowing):在方法抛出异常退出时执行的通知。它可以访问抛出的异常。

    最终通知(After ):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。 这是传统spring AOP中没有的新的通知类型。它不能访问返回值或抛出的异常。可以用来执行释放资源,日志记录等操作。

    环绕通知(Around):也叫方法拦截器。包围一个连接点的通知。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。 可用于实现性能测试,事物管理等。

    1.1.6 前置通知(Before)

    前置通知是在目标方法被调用之前织入的通知。即当目标方法被调用且执行之前,先执行的系统服务业务逻辑,如执行日志记录,参数检查,权限限制等。

    示例2.16将展示利用Spring的前置通知实现在手机进货和销售手机执行之前记录操作日志。示例2.1中的业务接口PhoneBiz和实现类PhoneBizImpl的代码不需要做任何修改,代理对象由Spring自动产生。我们只需要在示例2.13中创建的LogAspect切面类内部定义一个方法实现目标业务方法执行之前要进行的日志记录操作,如示例2.16所示。

    示例2.16 

    package s3spring.ch2.log;
    public class LogAspect {
    //前置通知
    public void before() throws Throwable {
    		System.out.println("业务方法开始执行……");
    }[先添加一个简单的方法,然后再引入JoinPoint]
    /** 
    * 前置通知 在目标方法执行之前执行日志记录 
    */
    	public void before(JoinPoint jp) throws Throwable {
    		Object[] args = jp.getArgs();// 目标方法所有参数
    		String methodName = jp.getSignature().getName();//获得目标方法名称
    
    		if ("buyPhone".equals(methodName)) {
    			System.out.println(currentTime() + 
    "即将执行进货操作,数量为" + args[0]);
    		}
    		if ("salePhone".equals(methodName)) {
    			System.out.println(currentTime() + 
    "即将执行销售操作,数量为" + args[0]);
    		}
    	}
    	/**
    	 * 输出当前时间的工具方法
    	 */
    	public String currentTime() {
    		SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    		return sdf.format(new Date());
    	}
    }

    如示例2.16所示,before方法拥有一个参数叫做jp, 类型为JoinPoint,即连接点。这个参数是可选的。JoinPoint对象提供了如下方法以获得连接点(在spring中一般是方法的执行)的一些有用信息:

    l Object[] getArgs():以对象数组的形式返回所有方法参数

    Signature getSignature():返回方法签名,通过Signature的getName()方法可以得到方法名称;getModifiers()方法可以得到方法修饰符。

    l String getKind():返回当前连接点的类型,如:“method-execution”,方法执行。

    l Object getTarget():返回连接点所在的目标对象。

    l Object getThis():返回AOP自动创建的代理对象。

    此时before方法虽然实现了一个日志管理前置通知的业务逻辑,但是它还是一个普通的方法,我们需要把这个方法定义成一个前置通知,并和示例2.15中定义的切入点p1关联起来,请看示例2.17中<aop:aspect>标签内的粗体部分代码:

    示例2.17

    <aop:config>
         <!-- 定义一个可以被多个切面共享的切入点 -->
    	<aop:pointcut id="p1" expression="execution( void *Phone(int))"/>
         <!-- 定义一个切面 -->
    	<aop:aspect id="logAspect" ref="logAspectBean">
             <!-- 定义一个前置通知 -->
             <aop:before method="before" pointcut-ref="p1" />
    </aop:aspect>
    </aop:config>

    <aop:before>标签用于定义一个前置通知。method属性指定实现通知的方法。pointcut-ref属性指定该通知关联的切入点。一个切面(<aop:aspect>)内可以包含多个不同类型的通知。

    到这里,我们的前置通知的示例就完成了。示例2.18是完整的配置:

    示例2.18

     

    <?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:p="http://www.springframework.org/schema/p"
    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-3.0.xsd 
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    	<!-- 目标业务对象  -->
    <bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>
    <!-- 日志管理切面类  -->
    <bean id="logAspectBean" class="s3spring.ch2.log.LogAspect"></bean>
    <!-- Aop配置  -->
    	<aop:config>
    		<aop:pointcut id="p1" expression="execution( void *Phone(int) )"/>
    <!-- 配置日志管理切面  -->
    		<aop:aspect id="logAspect" ref="logAspectBean">
             <!-- 配置日志记录前置通知  -->
    			<aop:before method="before" pointcut-ref="p1" />
    		</aop:aspect>
    	</aop:config>
    </beans>

     

    接下来我们来测试一下使用前置通知实现的日志管理操作的效果,示例2.19是测试代码:

    示例2.19

    ApplicationContext ac = 
    new ClassPathXmlApplicationContext("applicationContext.xml");
    //创建代理对象
    PhoneBiz pBiz = (PhoneBiz)ac.getBean("phoneBiz");
    //购买100部手机		
    pBiz.buyPhone(100);
    //销售88部手机
    pBiz.salePhone(88);

    示例2.19执行输出结果:

    2012年07月18日 18:02:45 即将执行进货操作,数量为100

    手机进货,进货数量 为100部

    2012年07月18日 18:02:45 即将执行销售操作,数量为88

    销售手机,销售数量为88部

    观察输出结果我们可以看出,在业务方法执行之前,前置通知成功织入连接点,先进行了日志输出。实际上,我们通过Spring应用容器获得的并不是真正的PhoneBizImpl类型的对象,而是Spring自动根据PhoneBizImpl 类实现的PhoneBiz接口创建的代理对象。示例2.20可以证明着一点:

    示例2.20

    ApplicationContext ac = 
    new ClassPathXmlApplicationContext("applicationContext.xml");
    //创建代理对象
    PhoneBiz pBiz = (PhoneBiz)ac.getBean("phoneBiz");
    //输出bean类型名称
    System.out.println(pBiz.getClass().getName());
    //购买100部手机		
    pBiz.buyPhone(100);
    //销售88部手机
    pBiz.salePhone(88);

    示例2.20粗体部分代码调用了pBiz的getClass()方法返回该对象的类型描述对象(一个Class类型的对象),并进一步调用Class对象的getName方法获取类名称。输出结果是:

    $Proxy4

    而不是bean对象的类型:

    s3spring.ch2.biz.impl.PhoneBizImpl

    这足以说明我们获得的并不是一个 PhoneBizImpl 类的对象,而是一个代理对象,而且该代理对象实现了PhoneBiz接口。所有此时你不能使用PhoneBizImpl类型的变量引用代理对象,请看示例2.21:

    示例2.21

    ApplicationContext ac =

    new ClassPathXmlApplicationContext("applicationContext.xml");

    //创建代理对象

    PhoneBizImpl pBiz = (PhoneBizImpl)ac.getBean("phoneBiz");

    //输出bean类型名称

    System.out.println(pBiz.getClass().getName());

    示例2.21将引用代理对象的变量类型从PhoneBiz改成PhoneBizImpl,执行时将抛出以下异常信息:

    java.lang.ClassCastException: $Proxy4 cannot be cast to s3spring.ch2.biz.impl.PhoneBizImpl

    示例2.21抛出的异常信息描述的很清楚:$Proxy4不能被转换成PhoneBizImpl。那么如果我们的目标对象没有实现接口怎么办呢?让我们修改PhoneBizImpl 类的定义,使其不实现任何接口,请看示例2.22:

    示例2.22

    public class PhoneBizImpl

    此时再次运行示例2.20,程序能够正常运行,输出代理对象类型信息如下:

    s3spring.ch2.biz.impl.PhoneBizImpl$$EnhancerByCGLIB$$ffeaaf7b

    实际上,Spring AOP 创建代理分为两种情况:

    l 如果被代理的目标对象实现了至少一个接口,Spring会使用针对接口产生代理的Java SE动态代理(Java直接提供的动态代理API)。目标对象类型实现的所有接口都将被代理,包括业务接口之外的接口。 注意,Java SE 动态代理只能针对接口产生代理,所以目标对象必须至少实现了一个接口。建议优先使用Java SE 动态代理。

    l 对于需要直接代理类而不是代理接口的时候,Spring也可以使用CGLIB(Code Generation Library)代理。如果一个业务对象并没有实现任何接口,Spring就会使用CGLIB在运行时动态生成目标对象的子类对象来作为代理对象。就算目标对象实现了接口,你也可以强制使用CGLIB代理,例如:希望代理目标对象的所有方法,而不只是实现自接口的方法;或当目标对象没有实现有用的业务接口,而只是实现了一些辅助工具接口(如果此时针对接口产生代理,则代理对象仅拥有这些辅助工具接口定义的方法,没有业务方法)。我们只需要将<aop:config>标签的proxy-target-class属性置为"true"即可强制使用CGLIB针对类产生代理,请看示例:

    示例2.23

    <aop:config proxy-target-class="true">

             ……

    </aop:config>

    1.1.1 后置通知(AfterReturning)

    后置通知是在目标方法调用之后织入通知,即在方法正常退出返回值之后且返回调用地点之前进行织入。

    示例2.24是一个后置通知实现,也位于示例2.13的LogAspect切面类中,作用是在业务方法调用结束之后进行日志记录。

    示例2.24

     

    public void afterReturning(JoinPoint jp) throws Throwable {
    		String methodName = jp.getSignature().getName();
    		if ("buyPhone".equals(methodName)) {
    			System.out.println(currentTime() + "进货操作执行完毕...");
    		}
    		if ("salePhone".equals(methodName)) {
    			System.out.println(currentTime() + "销售操作执行完毕...");
    		}

     

    在配置文件中<aop:aspect>标签内增加如示例2.25粗体部分配置内容,将后置通知织入切入点p1。

     

    示例2.25

    <!-- Aop配置  -->
    <aop:config >
    	<!-- 配置切入点  -->
    	<aop:pointcut id="p1" expression="execution( void *Phone(int) )" />
    	<!-- 配置切面  -->
    	<aop:aspect id="logAspect" ref="logAspectBean">
    		<!-- 配置前置通知  -->
    		<aop:before method="before" pointcut-ref="p1" />
    		<!-- 配置后置通知  -->
    		<aop:after-returning method="afterReturning" pointcut-ref="p1"/>
    	</aop:aspect>
    </aop:config>

     

    不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

     

    2012年07月20日 15:04:56即将执行进货操作,数量为100

    手机进货,进货数量 为100部

    2012年07月20日 15:04:56进货操作执行完毕...

     

    2012年07月20日 15:04:56即将执行销售操作,数量为88

    销售手机,销售数量为88部

    2012年07月20日 15:04:56销售操作执行完毕...

    1.1.1 异常通知(AfterThrowing)

    如果连接点抛出异常,异常通知(throws advice)将在连接点异常退出后被调用。示例2.26是一个自定义业务异常,表示缺货。

    示例2.26

    package s3spring.ch2.exception;

    /** 缺货异常*/

    public class OutOfStockException extends Exception{

     

    public OutOfStockException(String msg) {

    super(msg);

    }

    }

    修改示例2.1,为 PhoneBizImpl 类添加一个num属性代表库存。在salePhone()方法中判断如果销售量高于库存量,则抛出OutOfStockException,请看示例2.27粗体部分代码:

    示例2.27

    public class PhoneBizImpl  {
    	int num;// 库存
    public void buyPhone(int num) {
    		System.out.println("手机进货,进货数量 为" + num + "部");
    	     this.num += num;
    	}
    	public void salePhone(int num) throws OutOfStockException {
    		if(this.num<num) 
    throw new OutOfStockException("存货不足,客户需要"+num+
    "部手机,库存只有"+this.num+"部");
             System.out.println("销售手机,销售数量为" + num + "部");
    	}
         ……
    }

     

    在LogAspect切面类添加一个实现异常通知的方法,作用是在业务方法异常退出之后进行日志记录。OutOfStockException类型的参数e用于接收目标方法抛出的OutOfStockException类型异常,以便在通知内处理该异常。如果目标方法抛出的是其它异常,比如空指针异常,则示例2.28中异常通知方法不会被执行:

     

    示例2.28

    public void afterThrowing(JoinPoint jp,OutOfStockException e) {
    String methodName = jp.getSignature().getName();
    	System.out.println(currentTime()+methodName+"方法执行,发生缺货异常"+
    e.getMessage());
    }

    在配置文件中<aop:aspect>标签内增加如示例2.29粗体部分配置内容,将afterThrowing方法作为异常通知织入切入点p1,将throwing属性的值指定为afterThrowing方法OutOfStockException类型参数的名称“e”:

    示例2.29

    <!-- Aop配置  -->
    <aop:config >
    	<!-- 配置切入点  -->
    	<aop:pointcut id="p1" expression="execution( void *Phone(int) )" />
    	<!-- 配置切面  -->
    	<aop:aspect id="logAspect" ref="logAspectBean">
    		<!-- 配置前置通知  -->
    		<aop:before method="before" pointcut-ref="p1" />
    		<!-- 配置后置通知  -->
    		<aop:after-returning method="afterReturning" pointcut-ref="p1"/>
    <!-- 配置异常通知  -->
    		<aop:after-throwing method="afterThrowing" pointcut-ref="p1" 
    throwing="e" />
    	</aop:aspect>
    </aop:config>

     

    不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

     

    2012年07月20日 21:19:10即将执行进货操作,数量为100

    手机进货,进货数量 为100部

    2012年07月20日 21:19:10进货操作执行完毕...

     

    2012年07月20日 21:19:10即将执行销售操作,数量为120

    2012年07月20日 21:19:10salePhone方法执行,发生缺货异常:货存不足,客户需要120部手机,库存只有100部

    观察执行结果你会发现,当目标方法发生缺货异常时异常通知正确执行,但是后置通知没有执行。因为后置通知只在目标方法正常执行结束时执行。如果我们希望能够在目标方法抛出异常之后,像try、catch、finally结构一样,除了是用异常通知处理异常外,还可以通过某种通知去完成类似finally的任务,即无论目标方法是否抛出异常,该通知都一定会执行。这就是我们继续要学习的最终通知。

    1.1.1 最终通知(After)

    最终通知是无论目标方法异常退出,还是正常退出都一定会执行的通知。在LogAspect切面类中添加如示例2.30所示方法。

    示例2.30 最终通知

    public void after(JoinPoint jp) throws Throwable {
    	String methodName = jp.getSignature().getName();
    	if ("buyPhone".equals(methodName)) {
    		System.out.println(currentTime() + "进货操作执行完毕,发生异常也要执行的最终通知...");
    	}
    	if ("salePhone".equals(methodName)) {
    		System.out.println(currentTime() + "销售操作执行完毕,发生异常也要执行的最终通知...");
    	}
    }

    在配置文件中<aop:aspect>标签内增加如示例2.31粗体部分配置内容,将after方法作为最终通知织入切入点p1。

    示例2.31最终通知配置

    <!-- Aop配置  -->
    <aop:config >
    	<!-- 配置切入点  -->
    	<aop:pointcut id="p1" expression="execution( void *Phone(int) )" />
    	<!-- 配置切面  -->
    	<aop:aspect id="logAspect" ref="logAspectBean">
    		<!-- 配置前置通知  -->
    		<aop:before method="before" pointcut-ref="p1" />
    		<!-- 配置后置通知  -->
    		<aop:after-returning method="afterReturning" pointcut-ref="p1"/>
    <!-- 配置异常通知  -->
    		<aop:after-throwing method="afterThrowing" pointcut-ref="p1" 
    throwing="e" />
    <!--配置最终通知  -->
    		 <aop:after method="after" pointcut-ref="p1"/>
    	</aop:aspect>
    </aop:config>

    不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

    2012年07月25日 15:55:32即将执行进货操作,数量为100

    手机进货,进货数量 为100部

    2012年07月25日 15:55:32进货操作执行完毕...

    2012年07月25日 15:55:32进货操作执行完毕,发生异常也要执行的最终通知...

    2012年07月25日 15:55:32即将执行销售操作,数量为120

    销售手机,销售数量为120部

    2012年07月25日 15:55:32销售操作执行完毕...

    2012年07月25日 15:55:32销售操作执行完毕,发生异常也要执行的最终通知...

    1.1.1 环绕通知(Around)

    环绕 (Around)通知是最强大的通知类型,它能够代替之前所有通知类型,在连接点的前后执行,获取方法入参、返回值,捕捉并处理异常。下面我们就以性能测试为需求,针对业务层编写环绕通知,计算业务方法执行耗费的时间长短。在LogAspect切面类中添加如示例2.32所示性能测试环绕通知方法实现。

    示例2.32 

     

    public Object aroundTest(ProceedingJoinPoint pjp) throws Throwable {
          String method = pjp.getSignature().getName();
      	 long begin = System.currentTimeMillis();
      	 System.out.println(currentTime()+":"+method+"方法开始执行,计时开始!");
      	 try {
      		  return pjp.proceed();
      	 } finally{
      		  long end = System.currentTimeMillis();
      	  	  System.out.println(currentTime()+":"+method+"方法执行完毕,耗时"+
    (end-begin)+ "毫秒");
      	 }
    }

    环绕通知的实现方法必须包含一个连接点入参,但是其类型与其它类型的通知不同,为ProceedingJoinPoint。ProceedingJoinPoint对象代表了通知织入的当前连接点,调用其proceed()方法就会执行目标方法,proceed()方法的返回值就是目标方法的返回值,proceed()方法抛出的异常就是目标方法抛出的异常;如果你不想执行目标方法,只要不执行proceed()方法即可。

    在配置文件中<aop:aspect>标签内增加如示例2.33粗体部分配置内容,将aroundTest方法作为环绕通知织入切入点p1。

    示例2.33 

     

    <!-- Aop配置  -->
    <aop:config >
    	<!-- 配置切入点  -->
    	<aop:pointcut id="p1" expression="execution( void *Phone(int) )" />
    	<!-- 配置切面  -->
    	<aop:aspect id="logAspect" ref="logAspectBean">
    		<!-- 配置前置通知  -->
    		<aop:before method="before" pointcut-ref="p1" />
    		<!-- 配置后置通知  -->
    		<aop:after-returning method="afterReturning" pointcut-ref="p1"/>
    <!-- 配置异常通知  -->
    		<aop:after-throwing method="afterThrowing" pointcut-ref="p1" 
    throwing="e" />
    <!--配置最终通知  -->
    		 <aop:after method="after" pointcut-ref="p1"/>
    <!--配置环绕通知  -->
    <aop:around method="aroundTest" pointcut-ref="p1"/>
    	</aop:aspect>
    </aop:config>

     

     

     

    不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

    ……省略部分日志

    2012年07月25日 10:58:41:buyPhone方法开始执行,计时开始!

    手机进货,进货数量 为100部

    ……省略部分日志

    2012年07月25日 10:58:41:buyPhone方法执行完毕执行完毕,耗时31毫秒

     

    ……省略部分日志

    2012年07月25日 10:58:41:salePhone方法开始执行,计时开始!

    销售手机,销售数量为120部

    ……省略部分日志

    2012年07月25日 10:58:41:salePhone方法执行完毕执行完毕,耗时0毫秒

    1.1 Spring AOP 注解方式的实现

    我们也可以使用注解来进行AOP编程的配置。两种方式原理和概念不变,仅仅是配置形式不同罢了。下面我们就在2.3小节示例项目的基础之上将xml配置方式改为注解配置方式。

    1.1.1 用注解方式重构日志管理切面

    首先创建一个新包s3spring.ch2.log.annotation,然后将默认包下的applicationContext.xml和s3spring.ch2.log.LogAspect类复制到该包下。

    修改s3spring.ch2.log.annotation.applicationContext.xml中代码为示例2.34所示:

    示例2.34

    <beans xmlns=http://www.springframework.org/schema/beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    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-3.0.xsd 
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    <!-- 启用注解配置  -->
    <aop:aspectj-autoproxy />	
    <!-- 目标业务对象  -->
    <bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>
    <!-- 日志管理切面  -->
    <bean class="s3spring.ch2.log.annotation.LogAspect"></bean>
    </beans>

    注解配置方式和XML配置方式相同的是也需要在Spring应用容器的配置文件applicationContext.xml中引入AOP命名空间,不同的是我们采用注解来代替<aop:config >、<aop:pointcut>、<aop:aspect>、<aop:before>等等这些标签。

    另外,我们需要用<aop:aspectj-autoproxy />标签来告诉spring应用容器启用AOP注解配置,Spring应用容器就会在初始化时扫描所有的bean,以找出应用AOP注解配置的切面bean。

    id为“phoneBiz”的bean是目标业务对象,其实现类是示例2.1中的s3spring.ch2.biz.impl.PhoneBizImpl,代码和配置无需任何修改。

    s3spring.ch2.log.annotation.LogAspect类作为日志管理切面的实现类被配置成bean,在类声明上方添加@Aspect注解即可将它声明为切面。请看示例:

    示例2.35 

     

    package s3spring.ch2.log.annotation;

     

    @Aspect

    public class LogAspect {……}

    spring为我们提供了如下几个注解来帮助我们配置不同类型的通知:

    l @Before,前置通知

    @AfterReturning,后置通知

    l @AfterThrowing,异常通知

    l @After,最终通知

    l @Around,环绕通知

    在方法声明上方使用以上注解即可将方法声明为相应类型的通知,再为注解指定切入点表达式即可将通知织入该切入点。示例2.36演示了@Before、@AfterReturning、 @After和@Around注解的使用,请看示例2.36中粗体部分代码:

    示例2.36

    package s3spring.ch2.log.annotation;

    @Aspect

    public class LogAspect {

     

    /** 前置通知 在目标方法执行之前执行日志记录 */

    @Before("execution( void *Phone(int))")

    public void before(JoinPoint jp) throws Throwable {  ……  }

     

    /** 后置通知 在目标方法正常退出时执行日志记录 */

    @AfterReturning("execution( void *Phone(int))")

    public void afterReturning(JoinPoint jp) throws Throwable { …… }

     

    /** 最终通知 无论目标方法正常退出还是异常退出都执行日志记录 */

    @After("execution( void *Phone(int))")

        public void after(JoinPoint jp) throws Throwable { …… }

     

    /** 环绕通知 */

    @Around("execution( void *Phone(int))")

        public void after(JoinPoint jp) throws Throwable { …… }

     

         ……省略异常通知

    }

    用@AfterThrowing注解配置异常通知除了需要指定切入点外还需要根据方法参数名称绑定异常对象,请看示例2.37中粗体部分代码:

    示例2.37

    package s3spring.ch2.log.annotation;

    @Aspect

    public class LogAspect {

         ……

    /** 异常通知 在目标方法抛出参数指定类型异常时执行 */

    @AfterThrowing(pointcut="execution( void *Phone(int))",throwing="e")

    public void afterThrowing(JoinPoint jp,OutOfStockException e) {

    ……

    }

    }

    @AfterThrowing注解的pointcut配置项用于指定切入点,Throwing配置项用于指定方法中表示异常对象的参数名称,这样可以将目标方法抛出的异常对象绑定到同类型叫e的方法参数。

    1.1.1 切入点重用

    在示例2.36小节中所有通知织入的其实都是同一个切入点,但是却多次重复编写相同的切入点表达式。下面让我们用@Pointcut注解结合切入点表达式在LogAspect类中定义一个切入点,并将before通知织入该切入点,请看示例2.38:

    示例2.38

    package s3spring.ch2.log.annotation;

    @Aspect

    public class LogAspect {

    /** 切入点 */

    @Pointcut("execution( void *Phone(int))")

    public void p1(){}

     

    /** 前置通知 在目标方法执行之前执行日志记录 */

    @Before("s3spring.ch2.log.annotation.LogAspect.p1()")

    public void before(JoinPoint jp) throws Throwable {  ……  }

     

         ……

    }

    上例中用一个叫做p1的空方法来表示一个切入点,当我们希望将通知织入该切入点时,在注解中用方法签名来代替切入点表达式即可。由于切入点的声明和通知的声明在同一个类中,可以省略包路径和类名,示例可以简写为示例2.39粗体部分代码:

    示例2.39

    package s3spring.ch2.log.annotation;

    @Aspect

    public class LogAspect {

    /** 切入点 */

    @Pointcut("execution( void *Phone(int))")

    public void p1(){}

     

    /** 前置通知 在目标方法执行之前执行日志记录 */

    @Before("p1()")

    public void before(JoinPoint jp) throws Throwable {  ……  }

    }

     

    1:使用Spring AOP实现商场购物

     

    训练技能点

    Ø Spring AOP前置通知

    Ø AOP的配置

    需求说明

    某商场进行电冰箱促销活动,规定每位顾客只能购买一台特价电冰箱。顾客在购买电冰箱之前输出欢迎信息,顾客如果购买多台特价电冰箱,请给出错误提示,顾客成功购买电冰箱之后输出欢送信息。请使用Spring面向切面编程实现该需求的顾客欢迎信息显示。

    实现思路

    (1) 定义出售电冰箱的接口和接口实现。

    (2) 定义前置通知。

    (3) 编写配置文件。

    关键代码

    (1) 定义缺货异常

    public class NoThisFrigException extends Exception {

    public NoThisFrigException(String msg) {

    super(msg);

    }

    }

    (2) 定义顾客只能购买一台特价电冰箱的异常

    public class BuyFrigException extends Exception {

    public BuyFrigException(String msg)

    {

    super(msg);

    }

    }

    (3)  定义电冰箱业务接口。

    public interface FrigBiz {//出售电冰箱接口

    public void buyFrig(String customer,String frig) throws NoThisFrigException;

    }

    (4) 定义电冰箱接口的实现类。

    public class FrigBizImpl implements FrigBiz {

    public void buyFrig(String customer, String frig) throws NoThisFrigException {

      if ("美的".equals(frig)) {

    throw new NoThisFrigException("对不起,没有" + frig + "的货了");

            }

    System.out.println("您好,您已经购买了一台" + frig);

    }

    }

    (5) 定义前置通知,实现欢迎顾客的信息

    public class FrigBefore {

    public void before(Joinpoint jp) throws Throwable {

    // 通过Joinpoint获得目标方法传入的参数customer值

       String customer = (String) jp.getArgs()[0];// 取得第一个参数,客户名称

       // 显示欢迎信息,在buyFrig方法前调用

        System.out.println("欢迎光临!" + customer + "!");

    }

    }

    (6) 编写配置文件,使用Spring2.x方式配置

     <!-- 配置出售电冰箱的实现类 -->

    <bean id="frigBiz" class="bean.FrigBizImpl" />

    <!-- 配置前置通知 -->

    <bean id="frigBefore" class="utils.FrigBefore" />

    <!-- 配置代理对象 -->

    <bean id="frigBizProxy" <!-- AOP配置 -->

    <aop:config>

         <!-- 定义一个可以被多个切面共享的切入点 -->

    <aop:pointcut id="p1"

    expression="execution( void buyFrig(String,String))"/>

         <!-- 定义一个切面 -->

    <aop:aspect id="logAspect" ref="frigBefore">

             <!-- 定义一个前置通知 -->

             <aop:before method="before" pointcut-ref="p1" />

    </aop:aspect>

    </aop:config>

     

    (7) 编写测试代码测试。

    public static void main(String[] args) throws NoThisFrigException {

    ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");

    FrigBiz frigBiz=(FrigBiz)context.getBean("frigBizProxy");

    frigBiz.buyFrig("张无忌", "Lg");

    }

    }

    2:完善商场购物实现顾客欢送信息

    训练技能点

    Ø Spring AOP后置通知

    需求说明

    实训任务1的基础上完善系统,当电冰箱卖出之后,显示“欢迎下次再来”。

    实现思路

    (1) 编写后置通知FrigAfter。

    (2) 在配置文件中增加对后置通知的配置。

    (3) 编写测试代码。

    3:使用环绕通知重构商场购物

    训练技能点

    Ø Spring AOP环绕通知

    Ø Spring2.x的AOP配置

    需求说明

    每位顾客只能购买一台特价电冰箱,已经购买电冰箱的顾客如果重复购买,则给出顾客限购的提示。

    实现思路

    (1) 定义环绕通知。

    (2) 使用Spring2.x方式配置。

    (3) 编写测试代码。

    关键代码

    (1) 定义环绕通知FrigAround。

    public class FrigAround {

    private Set<String> customers = new HashSet<String>();

    public Object around(ProceedingJoinPoint pjp) throws Throwable {

    Object[] args = pjp.getArgs();// 目标方法所有参数

    String customer = (String)args[0];

    String frig = (String) args[1];

     

    if (customers.contains(customer)) {

    throw new BuyFrigException("对不起,一名顾客只能购买一台特价电冰箱!您已购买一台特价" + frig);

    }

     

    try {

              return pjp.proceed();

           } finally{

    customers.add(customer);

           }

    }

    }

    (2) 新建配置文件aop.xml,配置环绕通知。

    <!-- 配置出售电冰箱的实现类 -->

    <bean id="frigBiz" class="bean.FrigBizImpl" />

    <!-- 配置环绕通知 -->

    <bean id="frigAround" class="utils.FrigAround" />

    <aop:config>

         <!-- 定义一个可以被多个切面共享的切入点 -->

    <aop:pointcut id="p1"

    expression="execution( void buyFrig(String,String))"/>

         <!-- 定义一个切面 -->

    <aop:aspect id="frigAspect" ref="frigAround">

             <!-- 定义一个后置通知 -->

             <aop:around method="around" pointcut-ref="p1" />

    </aop:aspect>

    </aop:config>

    4:为商场购物添加库存验证

    训练技能点

    Ø Spring AOP异常通知

    需求说明

    升级商场购物,如果电冰箱库存不足,给出订货提示。

    实现思路

    (1) 编写异常通知FrigThrows。

    (2) 在aop.xml中增加异常通知的配置。

    (3) 编写测试代码。

    关键代码

    (1) 编写异常通知。

    public class FrigThrows{

    public void afterThrowing(NoThisFrigException e) throws Throwable {

    System.out.println("通知仓库,赶紧订货!");

    }

    }

    (2) 在aop.xml中增加FrigThrows的配置。

    <bean id="frigBiz" class="bean.FrigBizImpl" />

    <bean id="frigThrows" class="utils.FrigThrows" />

    <aop:config>

         <!-- 定义一个可以被多个切面共享的切入点 -->

    <aop:pointcut id="p1"

    expression="execution( void buyFrig(String,String))"/>

         <!-- 定义一个切面 -->

    <aop:aspect id="frigAspect" ref="frigThrows">

             <!-- 定义一个异常通知 -->

             <aop:after-throwing method="afterThrowing" pointcut-ref="p1" />

    </aop:aspect>

    </aop:config>

     

    5:应用注解配置方式将1~4题重新配置

    训练技能点

    Ø Spring AOP注解配置


    巩固练习

     

    一.选择题

    1. Spring通知不包括 ()。

        A. 前置通知

        B. 后置通知

        C. 环绕通知

        D. 设置通知

    2. 以下对AOP 的说法中,错误的是 ()。

        A. AOP将散落在系统中的“方面”代码集中实现

        B. AOP有助于提高系统的可维护性

        C. AOP可以取代OOP

        D. AOP能大大简化程序的代码

    3. 下列关于Spring AOP的说法错误的是 ()。

        A. 可支持前置通知、后置通知、环绕通知

        B. Spring AOP采用拦截方法调用的方式实现,可以在调用方法前、调用后、抛出异常时拦截

        C. Spring AOP采用代理的方式实现

        D. 采用Spring2.x方式配置的时候,不会产生AOP代理

    4. 在 Spring框架中,面向方面编程 (AOP)的目标在于 ()。

        A. 编写程序时无须关注其依赖组件的实现

        B. 封装JDBC访问数据库的代码,简化数据访问层的重复性代码

        C. 将程序中涉及的公共问题集中解决

        D. 可以通过Web服务调用

    5.下面关于 Spring AOP 错误的是 ()。

    A. 任何一个通知的实现发那个发都可以以JoinPoint或ProceedingJoinPoint为

    参数

    B. 只有环绕通知可以使用ProceedingJoinPoint为入参,其它类型通知只能使用

    JoinPoint为入参

    C. JoinPoint和ProceedingJoinPoint都有proceed()方法,用于执行目标发方

    D. 仅ProceedingJoinPoint有proceed()方法

    6. 以下那一个注脚不是用于定义通知的? ()。

        A. @After

        B. @Before

        C. @Aspect

        D. @AfterThorwing

     

    二.操作题

    1.在第一章操作题第1题的基础上实现以下功能。

    现举行活动,升级指环的话,可以免费将任意指环升级为“紫色梦幻”指环,新的装备名称为“紫色梦幻+原指环名”,而且将在原有的基础上再加6点攻击,6点防御。

    提示:使用前置通知,判断要升级的装备类型是否为指环,如果是则按照要求修改传入参数的名称以及攻击增效和防御增效的属性。

    2.在电子商务网站购物时,需要对生成订单的过程进行日志记录,以便于查询交易过程。该系统有一个生成订单的业务,请使用AOP方式实现在交易方法调用时记录客户信息及生成订单的时间。

    3.在银行管理系统中,转账是很重要的操作,如张三向李四的账户转账1000元,需要经过的步骤是:

    (1)修改张三账户信息,从账户中扣除1000元。

    (2)修改李四账户信息,在李四账户中增加1000元钱。

    (3)向交易记录表中增加一条记录。

    以上三步操作必须运行在同一事务,而且任何一步出现异常,事务必须回滚。请使用SpringAOP实现事务控制。

    提示:

    在BankBiz中添加一个转账方法,该方法实现以上三步操作,然后编写一个前置通知、后置通知和异常通知。在前置通知中开启事务,在后置通知中提交事务,在异常通知中进行事务回滚。

    开启事务的方法:

    HibernateSessionFactory.getSession().beginTransaction();

    提交事务的方法:

    HibernateSessionFactory.getSession().beginTransaction().commit();

    事务回滚的方法:

    HibernateSessionFactory.getSession().beginTransaction().rollback();

    4.升级“会员账户管理系统”,使用Spring的面向切面编程为会员状态的更改、会员充值、会员信息的删除增加日志,日志要求:

    (1)会员状态的更改,需要记录被更改状态的会员的编号和更改时间。

    (2)会员充值的时候,如果充值失败,记录失败原因及充值操作时间。如果充值成功,记录被充值会员的编号,充值时间和充值金额。

    (3)会员信息的删除,需要记录被删除的会员编号、删除时间。

    (4)日志信息在控制台输出。

     

     

     

    展开全文
  • 关于切面相关概念 这篇不提,可以大致理解成,只要调用某个特定的方法, 这个调用信息会被切面拦截,然后执行切面定义的逻辑,之后才能顺利的调用该方法。 我这个是一个maven项目所有代码写在同一个包下面,测试类...

    个人博客

    https://kongdada.github.io/
    最近在看《Spring实战》,在这儿使用注解完整的实现一个切面的例子,也实现通过注解引入新功能;

    实现切面

    关于切面相关概念这篇不提,可以大致理解成,只要调用某个特定的方法,这个调用信息会被切面拦截,然后执行切面定义的逻辑,之后才能顺利的调用该方法。
    我这个是一个maven项目所有代码写在同一个包下面,测试类除外。关于切面这个部分可能需要导入一些包。
    pom.xml

            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.8.9</version>
            </dependency>
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>3.2.4</version>
            </dependency>
    
    定义特定的方法
    • 首先定一个接口
    package aopJavaConfig;
    
    public interface Performance {
        public void perform();
    }
    
    • 实现这个接口,定义一个叫演出的方法
    package aopJavaConfig;
    import org.springframework.stereotype.Component;
    
    @Component
    public class PerformanceImpl implements Performance {
        @Override
        public void perform(){
            System.out.println("演出~~~");
        }
    }
    
    定义一个切面,同时定义了切点和通知
    package aopJavaConfig;
    import org.aspectj.lang.annotation.*;
    
    @Aspect
    public class Audience {
    
        @Pointcut("execution(* aopJavaConfig.Performance.perform(..))")
        public void performance(){}
    
        @Before("performance()")
         public void silencePhone(){
            System.out.println("手机静音!");
        }
    
        @Before("performance()")
        public void takeSeats(){
            System.out.println("请坐!");
        }
    
        @AfterReturning("performance()")
        public void applause(){
            System.out.println("表演结束,鼓掌!!!");
        }
    
        @AfterThrowing("performance()")
        public void demanRefund(){
            System.out.println("表演失败,退票!!!");
        }
    }
    
    
    • @Aspect修饰的这个类是切面,我们可以在切面中定义切点和通知;
    • @Pointcut修饰的是切点,可以看到切点就是我们之前定义的那个特定的方法;
    • @Before修饰的方法会在定义的特定方法之前被调用;
    • @AfterReturning修饰的方法会在特定方法被成功调用后执行;
    • @AfterThrowing修饰的方法会在特定方法执行异常时被执行;
    定义一个配置类

    这个配置类负责开启自动代理,并且将我们定义好的切面声明成Bean;

    package aopJavaConfig;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan
    public class ConcertConfig {
    
        @Bean
        public Audience audience(){
            return new Audience();
        }
    }
    

    至此,我们有两个bean,一个是那个特定的方法,一个是声明好的切面,那么我们就有一个很朴素的冲动,这样定义是否成功了呢,所以写一个测试类;

    测试切面定义是否成功

    预期的结果:调用演出方法,在控制台打印出来演出之前要干嘛,演出成功后要干嘛;
    具体的输出请查看代码本身;

    package aopJavaConfig;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = ConcertConfig.class)
    public class PerformanceTest {
        @Autowired
        private Performance performance;
    
        @Test
        public void test() {
            performance.perform();
        }
    }
    

    至此,使用java注解方式定义一个切面结束;

    引入新功能(DeclareParents)

    接上文代码,当我们接受老代码,期望在原来的特定方法的基础上加一个功能,很朴素的想法就是修改原来的方法,加一段功能代码,但是假设现在老代码很复杂不得修改;那么为了增加这个功能,Spring提供了一个另一个实现方式。

    新功能代码
    • 定义新接口
    package aopJavaConfig;
    
    public interface Encoreable {
        void performEncore();
    }
    
    • 实现这个接口
    package aopJavaConfig;
    import org.springframework.stereotype.Component;
    
    @Component
    public class EncoreableImpl implements Encoreable{
        @Override
        public void performEncore(){
            System.out.println("返场表演~~~");
        }
    }
    
    将新功能引入原来特定方法形成的bean中

    可以将bean看作一个对象,这个特定的这个对象中添加一个新的方法,也就是让这个对象可以直接调用新加的方法;

    package aopJavaConfig;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.DeclareParents;
    
    @Aspect
    public class EncoreableIntroducer {
        /**
         * 此注解可以将新加的接口引入到原来调用目标函数的bean中
         */
        @DeclareParents(value = "aopJavaConfig.Performance+", defaultImpl = EncoreableImpl.class)
        public static Encoreable encoreable;
    }
    
    • @DeclareParents 这个注解的value制定了这个新方法加给谁,defaultImpl这个参数指定了新加方法的具体实现。
      实际上新加方法是加给了value制定方法的父类。
    新加方法要告知Spring容器

    修改配置类如下:

    package aopJavaConfig;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan
    public class ConcertConfig {
    
        @Bean
        public Audience audience(){
            return new Audience();
        }
    
        @Bean
        public EncoreableIntroducer encoreableIntroducer(){
            return new EncoreableIntroducer();
        }
    }
    
    
    测试类

    预期结果:能够使用特定类的对象成功调用新加的方法;

    package aopJavaConfig;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = ConcertConfig.class)
    public class PerformanceTest {
        @Autowired
        private Performance performance;
    
        @Test
        public void test() {
            performance.perform();
            System.out.println("+++++++++++++++++++++++++++++++++++");
            Encoreable encoreable = (Encoreable)performance;
            encoreable.performEncore();
        }
    }
    

    测试结果如下:

    手机静音!
    请坐!
    演出~~~
    表演结束,鼓掌!!!
    +++++++++++++++++++++++++++++++++++
    返场表演~~~
    

    至此使用注解方式开发切面,切点,通知,新加方法都已经完成。代码经过了验证;
    如有错误,还请指正。欢迎交流。

    后记

    展开全文
  • aop切面编程真是个好东西,他类似于过滤器又不同于过滤器,因为这次项目中需要对用户的token进行验证。同事写了一个过滤器,但是他要加黑名单(不是所有请求都需要验证token),这样一来这个黑名单就很难维护。所以...
  • Spring AOP(二)——在XML中配置切面

    万次阅读 2016-03-16 22:32:40
    在Spring AOP(一)中介绍了AOP的基本概念和几个术语,现在学习一下在XML中如何配置AOP。 在XML中AOP的配置元素有以下几种: ... 启用@AspectJ注解驱动的切面 定义切点 定义AOP通知器 定义
  • Spring boot AOP(切面)使用方法和解析

    万次阅读 多人点赞 2018-07-27 15:15:16
    最近开始实习,终于有时间写博客了。因为需要使用springboot,大三学Oracle数据库的时候使用过springboot,之前没怎么用java写网页后台,发现网上很多的博客要不就是复制粘贴,要不就是存在默认知识现象(就是跳过了...
  • java ssh spring aop 切面编程 操作日志

    千次阅读 2014-09-01 22:50:04
    切面编程原理:
  • Spring @Aspect 切面,动态代理教程

    千次阅读 2018-11-09 11:21:52
    一般@Aspect 注解的切面, 通常可以用切面表达式, 和注解切面来完成我们的切面编程. 首先必须引入依赖 &lt;dependency&gt; &lt;groupId&gt;org.aspectj&lt;/groupId&gt; ...
  • AOP,面向切面编程

    千次阅读 2009-10-15 12:57:00
    AOP,面向切面编程 举一个最简单的例子说明什么是面向切面编程,银行柜员程序,它实现的商业逻辑很简单,如果客户取钱就在客户的帐户上做减法,如果存钱就做加法。这样的开发任务交给程序员他最开心,因为这对...
  • 通过springboot的Aop面向切面实现彩色日志使用的场景 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员...
  • AOP的概念很久前就接触过了,不过没有真正写过项目,甚至Demo都没有,今天把这点缺陷补上。 推荐两篇文章(本文部分图片引自这两篇... AOP是Aspect Oriented Program的首字母缩写,译为:面向切面编程。类似的OOP,译
  • 通过切面为Spring bean添加新的方法

    千次阅读 2017-12-01 19:23:37
    通过切面为Spring bean添加新的方法
  • 印刷常见故障解决办法

    千次阅读 2014-07-28 14:53:09
    印刷常见故障解决办法,第一种故障现象:半自动时未踩脚踏开关而立即动作。  此类故障原因多半是脚踏开关损坏或脚踏开关插座进水导致短路造成,另一个是手动起动按钮开关有问题。修复方法是更换新的开关、...
  • 实际项目中我们往往需要将一些重要的操作,以日志的形式进行保存,当机器...2:编写将要进行切面工作的类 /** * */ package com.zhiyou100.aspect; import java.util.Arrays; import org.aspectj.lang.JoinP...
  • SpringAOP之注入AspectJ切面

    千次阅读 2017-06-03 12:54:21
    那么,当我们需要创建更细粒度的通知或想监测bean的创建时,Spring所支持的AOP就比较弱了,这时,可以选择使用AspectJ提供的构造器切点,并且可以借助Spring的依赖注入把bean装配进AspectJ切面中。下面就来举个栗子...
  • 本文阐述了Spring架构的控制反转和面向切面编程技术的原理,展示了Spring framework的简单、高效、可维护等优点。 注:本文大部分内容摘自《Spring.3.x企业应用开发实战》,涉及的例子也来自此书源代码。 1 ...
  • 利用SPring AOP配置切面的一个例子

    万次阅读 2011-09-07 10:18:09
    这个例子,就是对于DukePerformer类,在它的演奏方法perform执行前,输出观众找座位takeSeat和关手机turnOffPhone,在执行后...将代码提取出来,独立于一个模块中,就是切面编程。主要还是为了松散耦合。 首先定义Duke
  • Android进阶系列之AOP面向切面编程

    万次阅读 多人点赞 2016-12-26 17:30:34
    首先创建一个类,用来处理触发切面的回调。 package com.example.administrator.aspectjdemo; import android.app.Activity; import android.content.Context; import android.util.Log; import org.aspectj...
  • Spring4 实战笔记(3):面向切面编程

    千次阅读 2016-12-13 01:31:25
    何为面向切面编程? 切面可以用于将交叉关注点模块化。简单来说,交叉关注点值得是那些影响一个应用中多个模块的通用功能。例如,安全处理是一个交叉关注点,在应用中的很多模块中都需要应用一定的安全检查,下图...
  • 面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。 从《Spring实战(第4版)》图书中扒了一张图: 从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独...
  • 本系列博客为spring In Action 这本书的学习笔记在上一篇博客中, 我们了解了AOP的基本概念和Spring中的AOP, 那么本篇博客就来实际操练一下, 看看在Spring中如何创建一个切面. 一. 编写切点通知和切点是切面的最基本...
  • 家里的电表总结起来有两大特性: 电视需要(电量管理),空调需要(电量管理),热水器也需要电量管理,即一组对象都需要某一功能特性...软件开发中,散布于应用中多处的功能被称为横关切点(cross-cutting concern
  • 1、通行理论 在软件中,有些行为是通用的。比如日志、安全和事务管理,他们有一个共同的特点,分布于应用中的多处,这种功能被称为横切关注点(cross-cutting ...下面涉及的内容包括Spring对切面的支持,包括如何把...
  • 切面的功能被称为Advice(通知),它定义了切面是什么及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应用在某个方法被调用之前?之后?之前和之后?或是只在方法抛出一个异常时? ...
  • 技术位面 Redis Redisson fastjson 切面(Aspect) 序列化 (Serializable) SpringBoot (示例工程) 功能示例图: 核心实现 实现原理简介 自定义注解 @CacheData ; 通过切面 CacheAspect 拦截添加自定义注解的方法 ; ...
  • 目录 1、主旨阐述 2、准备工作:搭建Spring环境,必要的基础理论,下载Aspectj的4个jar包 3、本文要做的事 4、代码结构说明 ...5.1、Spring AOP注解-简单的...5.2、Spring AOP注解-环绕型切面定义,包含切点和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 48,567
精华内容 19,426
关键字:

切面机