Spring基础篇——AOP切面编程

一  基本理解

  AOP,面向切面编程,做为Spring的核心思想之一,度娘上有太多的教程啊、解释啊,但博主仍是要本身按照本身的思路和理解再来阐释一下。缘由很简单,别人的思想终究是别人的,本身的理解才是本身的,尤为当用文字、代码来阐述一遍事后,理解层面上又彷佛变得不同了。java

  博主就不概念化解释AOP了,这里只简单说下为啥要使用这样一种编程思想和相关的AOP技术。其实很简单,就是为了业务模块间的解耦,尤为在现代的软件设计中强调高内聚、低耦合,要求咱们的业务模块化,各个功能模块只关注本身的逻辑实现,而不用关注与主业务逻辑不相关的功能。然而,在面向对象的系统设计中,系统中不可或缺的一些功能如日志、事务是散布在应用各处与主逻辑代码高度耦合的,这让主业务代码变得至关冗余、难以复用。而在面向切面的编程思想中,咱们是考虑将那些散布在应用多处的重复性代码抽离出来封装成模块化的功能类,一来让主业务逻辑更加专一、简单,二来模块化的日志、事务也便于复用和移植,这就是解耦的思想。可是,解耦并不等于断耦,抽离的功能最终仍是要以某种方式"还"(qie)回去,不然应用的功能就不完善了。这里,"还"(qie)回去的技术就是AOP技术,而这种解耦的编程思想就是AOP的编程思想。在Java的生态中,提供AOP技术的框架也有很多,主要的运用就是Spring的AOP和Spring"借鉴"并包含进了本身的生态体系的 AspectJ的AOP。spring

二  核心概念

  为便于理解阐述,博主先唠叨几句。上面的基本阐述中,咱们知道,AOP要干的事情其实也很简单,就是要将对象编程中,抽离出来的模块代码(权限、日志、事务)还(qie)回去,但确定不能是对象思惟中的代码冗杂的组合,而是应该更加高明一些,最好能在原来的业务代码执行的过程当中不知不觉的还(qie)回去——也就是说要在主业务逻辑执行的流程里,动态的添加(权限、日志、事务)代码抽离前干的那些事情。怎么能作到呢?用代理啊,亲!想一想,咱们对一个目标对象采用代理不就是为了在目标对象逻辑执行时候经过在代理对象中干点额外的事情吗?这样,虽然,原目标对象并无增长任何额外的功能,经过代理的一番暗中骚操做,展现给调用者的就好像目标对象有了代理对象中的那些额外的功能同样。因而你也很好理解,为何Spring的AOP中要用到动态代理了。好了,通过一番唠叨,咱们再来看AOP的相关术语就要好理解得多——express

 

  一、横切关注点

  如上描述,咱们把日志、事务、权限等代码重复性极高却散布在应用程序各个地方的功能称为横切关注点。编程

 

  二、链接点(Join Point)

  被代理的目标对象在业务逻辑执行的过程当中,能够被代理对象动态切入代理功能的一些时机节点,好比方法执行前、后,异常时,成功返回时等等。固然,这只是针对Spring来讲的,由于Spring基于动态代理,只支持方法级别的AOP切入,实际上,AspectJ、JBoss等框架的AOP还能提供构造器以及更细粒度字段等的链接点支持。安全

 

  三、通知(Advice)

  如上描述,就是代理对象在什么时机要为目标对象额外增长的功能代码,于是不少教程资料上称之为 加强。请注意博主对通知的描述里有提到什么时机,这很好理解,你的代理对象要给目标对象增长额外功能,总得清楚要增长在哪些时机吧,因此,咱们的通知按照功能切入的时机分为如下5个类型:多线程

    前置通知(Before):被代理对象目标方法被调用以前执行通知代码;并发

    后置通知(After):被代理对象目标方法执行完成以后执行通知代码,无论方法是否成功执行(这至关于异常捕获中的finally块,老是会执行的意思,因此博主以为若是将其命名为最终通知要更好理解些);app

    异常通知(After-throwing):被代理对象目标方法抛出异常后执行通知代码;框架

    返回通知(After-returning):被代理对象目标方法成功执行后执行通知代码;模块化

    环绕通知(Around) :包裹被代理对象的目标方法,至关于结合了以上的全部通知类型。

 

  四、切点(Pointcut)

  被代理对象目标方法执行过程当中真正的要执行通知代码的一个或多个链接点,这会经过切点表达式语言进行匹配。

 

  六、切面(Aspect)

  通知和切点的结合,切面完整的包含了代理对象对目标对象进行通知的三个基本要素:什么时候(前、后、异常、环绕、返回等),何地(切点),干什么(通知切入的功能)。

 

  七、织入(Weaving)

  将切面应用到被代理对象并建立代理对象的的过程。切面会在指定的链接点(切点)被织入到被代理对象的执行方法中。其实,被代理对象的生命周期中有多个时机(编译、类加载、运行)均可以进行织入,就 Spring 而言,是在被代理对象运行期进行代理对象的建立,织入切面逻辑的。

 

注:以上描述都是基于Spring 方法级别的AOP 来进行阐述

 

三  基础代码示例

   说了那么多,仍是上代码最简单直接。准备工做:

  ① 测试依赖的包及其版本(注:不少教程中都提到须要 aopalliance包,可是博主测试过程当中并无确认此包存在的必要性)

    aspectjweaver-1.9.2.jar
    commons-logging-1.2.jar
    spring-aop-4.3.18.RELEASE.jar
    spring-beans-4.3.18.RELEASE.jar
    spring-context-4.3.18.RELEASE.jar
    spring-core-4.3.18.RELEASE.jar
    spring-expression-4.3.18.RELEASE.jar
    spring-test-4.3.18.RELEASE.jar

  ② 定义两个基础模型类(以下),业务是:给只有打电话功能的手机动态的添加 拍照、玩游戏这样的非主业务功能。

//主业务功能
public class HuaWeiPhone  {
    public void ring() {
        System.out.println("华为手机,产销第一");
    }
}


//额外添加的功能
public class Photograph {

    public void  takePictures(){
        System.out.println("华为手机,拍照牛批");
    }

    public void  playGames(){
        System.out.println("华为手机,游戏玩得也这么畅快");
    }
}

 

  一、XML配置的方式

   根据以上Java代码,进行很是简单的配置,就能看到动态的为手机增长了拍照功能的效果了——

<bean  class="main.java.model.HuaWeiPhone"/>
    <bean id="photograph" class="main.model.Photograph"/>
    <aop:config>
        <aop:pointcut id="ring" expression="execution(* main.model.HuaWeiPhone.ring(..))"/>
        <aop:aspect  ref="photograph">
            <aop:before method="takePictures" pointcut-ref="ring"/>
            <aop:after method="playGames" pointcut-ref="ring"/>
        </aop:aspect>
    </aop:config>

 

  在Spring环境下测试类XML配置——

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testXml(){
        huaWeiPhone.ring();
    }
}

  输出结果

         

  二、Java注解的方式

  须要先说明的是,Spring的基于注解的 AOP 其实是借鉴吸取了AspectJ的功能,因此你会看到不少相似 AspectJ 框架的注解。在以前的模型类上经过添加相应的注解改形成一个切面——

@Aspect  //将该类标注为一个AOP切面
@Component
public class Photograph {

    @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))")
    public void chenbenbuyi (){}


    @Before("chenbenbuyi()")
    public void  takePictures(){
        System.out.println("华为手机,拍照牛批");
    }

    @After("chenbenbuyi()")
    public void  playGames(){
        System.out.println("华为手机,游戏玩得也这么畅快");
    }
}

  一样的,目标类(HuaWeiPhone)上也要添加@Componet注解将其交给Spring 容器管理。而后,若是是纯注解的话,还要一个配置类——

//配置注解扫描
@ComponentScan(basePackages = "main")
//启用AspectJ的自动代理功能
@EnableAspectJAutoProxy
public class JavaConfig {
}

  最后,在Spring的环境下测试——

@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testAnno(){
        huaWeiPhone.ring();
    }
}

  结果同上,这里就不展现了。不过须要注意的是,无论什么配置方式,基于Spring 的AOP编程实现的前提都是要将通知对象和被通知方法交给Spring IOC容器管理,也就是要声明为Spring 容器中的Bean。

四 需求升级

  在第三部分中,博主只是展现了最最简单的AOP功能实现,还有稍微复杂的技能点没有列出。好比,5种通知类型中的环绕通知呢?再好比,个人切面代码若是要传参数怎么办呢?接下来博主依次讲解。

   ① 关于环绕通知的运用

  基于 二 中的阐述,5 种通知类型中 环绕通知 是功能最为强大,实际上,咱们能够在环绕通知中个性化的定制出前置 、后置、异常和返回的通知类型,而若是单独的采用前置、后置等通知类型,若是业务涉及多线程对成员变量的修改,可能出现并发问题,因此环绕要比单独的使用另外的几种通知类型更加的安全。咱们对上面的切面基于环绕通知进行修改,使之包含全部的通知类型的功能——

@Aspect
@Component
public class Photograph {

    @Pointcut("execution(* main.model.HuaWeiPhone.ring(..))")
    public void chenbenbuyi (){}

    @Around("chenbenbuyi()")
    public void  surround(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("目标方法执行前执行,我就是前置通知"); 
            joinPoint.proceed();////            int i =1/0;       // ②  制造异常
            System.out.println("正常返回,我就是返回通知");
        } catch (Throwable e) {
            System.out.println("出异常了,我就是异常通知");
        }finally {
            System.out.println("后置通知,我就是最终要执行的通知");
        }
    }
}

  XML的配置和上面的其它通知类型同样,只不过元素标签为 <aop:around />而已。上面的打印语句的位置就对应了其它几种通知类型执行切面逻辑的时机。这里注意,环绕通知方法体中须要有 ProceedingJoinPoint 接口做为参数,在环绕通知中,经过执行该参数的 proceed() 方法来调用通知须要切入的目标方法。若是不执行 ① 处的调用,被通知方法实际上会被阻塞掉,因此你会看到,明明测试中执行了被通知的方法,实际却没有执行。该参数对象还能够获取方法签名、代理对象、目标对象等信息,能够本身测试着玩。

  ② 关于通知的传参问题

  切面虽然是通用逻辑,但实际在切入不一样的目标方的时候,可能仍是但愿通知方法根据被通知方法的不一样(好比参数不一样)而执行不同的逻辑,这就要求咱们的通知也能获取到被通知方法传入的参数。经过切点表达式,这也很容易办到。首先咱们修改被通知的方法能够传参:

 public void ring(String str) {
        System.out.println("华为手机,产销第一");
        int i =1/0;
    }

  而后切面中切点表达式和切面方法也作对应的修改——

@Aspect
@Component
public class Photograph {
    /**
     * Spring 借助于 AspectJ的切点表达式语言中的arg()表达式执行参数的传递工做
     */
    @Pointcut("execution(* main.model.HuaWeiPhone.ring(String))&&args(name)")
    public void chenbenbuyi (String name){}

    /**
     *  ① 在引用空标方法的切点表达式时同时也就要传入相应的参数
     *  ② 传入的参数形参名字必须和切点表达式中的相同
     */
    @Before("chenbenbuyi(name)")
    public void  takePictures(String name){
        System.out.println("喂喂,你好我是 "+ name);
    }

    /**
     *  对于异常通知,有专门的异常参数能够直接获取到被通知方法出现异常后信息的
     */
    @AfterThrowing(pointcut = "chenbenbuyi(name)",throwing = "e")
    public void  excep(String name,Throwable e){
        System.out.println("出异常了,异常信息是:"+e.getMessage());
    }
}

  XML中配置参数传递

    <bean  class="main.java.model.HuaWeiPhone"/>
    <bean id="photograph" class="main.java.model.Photograph"/>
    <aop:config>
        <aop:pointcut id="ring" expression="execution(* main.java.model.HuaWeiPhone.ring(..)) and args(name)"/>
        <aop:aspect  ref="photograph">
            <aop:before method="takePictures" pointcut-ref="ring" arg-names="name" />
            <aop:after-throwing method="excep"  throwing="e" arg-names="name,e" pointcut-ref="ring"/>
        </aop:aspect>
    </aop:config>

  测试代码——

@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "classpath:main/resource/applicationContext.xml")
@ContextConfiguration(classes = JavaConfig.class)
public class SpringTest {

    @Autowired
    HuaWeiPhone huaWeiPhone;

    @Test
    public void testAnno(){
        huaWeiPhone.ring("博客园 陈本布衣");
    }
}

  最终测试的执行结果——  

 

  注意点:

    ① XML配置中因为 &符号有特殊含义,因此 切点表达式中 链接形参名的时候就不能再使用注解中的 && ,而应该使用 and 代替,一样的若是有 或(|| )非 (!)操做,分别使用 or 和 not 代替。

    ② 注解和XML配置中切点表达式描述形参类型的地方博主采用了不一样的方式,由于 .. 就表示任意类型,能够不用指明。 

 

五 切点表达式经常使用图解

  

相关文章
相关标签/搜索