深入分析JavaWeb 53 -- Spring中的AOP面向切面编程1

一、AOP基本概念和Spring对AOP的支持

1、AOP的基本概念

AOP从运行的角度考虑程序的流程,提取业务处理过程的切面。AOP面向的是程序运行中的各个步骤,希望以更好的方式来组合业务逻辑的各个步骤。AOP框架并不与特定的代码耦合,AOP框架能处理程序执行中特定切入点,而不与具体某个类耦合(即在不污染某个类的情况下,处理这个类相关的切点)。下面是一些AOP的一些术语:

切面(Aspect):业务流程运行的某个特定步骤,也就是应用运行过程的关注点,关注点通常会横切多个对象,因此常被称为横切关注点

连接点(JoinPoint):程序执行过程中明确的点,如方法调用,或者异常抛出。Spring AOP中,连接点总是方法调用。

通知(Advice):AOP框架在特定的切入点执行的通知。处理有around,before,after等类型。(说明:AOP是比较前言的只是,而大部分国内翻译人士在翻译计算机文献时,总是一边看各种翻译软件和词典,一边逐字逐句的翻译,而不是先去从总体上把握知识的架构。因此难免导致一些术语翻译的词不达意,在AOP的术语上也存在较大差异,对于Advice一词,有人翻译为“通知”,有人翻译为“建议”……实际上,Advice是指AOP框架在特定切面所加入的某种处理,笔者这里也翻译为“通知”,希望可以表达出Advice的真正含义)

切入点(PointCut):可以插入通知的连接点。简而言之,当某个连接点满足执行要求时,该连接点将被添加通知,该连接点也就变成了切入点。例如:

pointcut xxxPointcut():execution(void H*.say*())

每个方法被调用都只是连接点,但如果该方法属于H开头的类,且方法名以say开头,按该方法的执行将变成切入点。如何使用表达式来定义切入点是AOP的核心,Spring默认使用AspectJ切入点语法。

引入:将方法或字段添加到被处理的类中。Spring允许引入新的接口到任何被处理的对象

目标对象:被AOP框架进行通知的对象,也被称为增强的对象。如果AOP框架是通过运行时代理来实现的,那么这个对象将是一个被代理的对象。

AOP代理:AOP框架创建的对象,简单地说,代理就是对目标的对象的加强。Spring中AOP代理可以是JDK动态代理,也可以是CGLIB代理。前者为实现接口的目标对象的代理,后者为不实现接口的目标对象的代理。

织入(Weaving):将通知添加到目标对象中,并创建一个被增强的对象(AOP代理)的过程就是织入。织入有两种实现方式:编译时增强(如AspectJ)和运行时增强(如CGLIB)。Spring和其他纯Java AOP框架一样,在运行时织入。

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

2、Spring的AOP支持

Spring中AOP代理由Spring的IoC容器负责生成、管理。其依赖关系也由IoC容器负责管理。因此AOP代理可以直接使用容器中的其他Bean作为目标,这种关系可由IoC容器的依赖注入提供。Spring默认支持使用JDK动态代理来创建AOP代理,这样就可以为任何接口实现创建代理了。

Spring也支持CGLIB代理,在需要代理类而不是代理接口的时候,Spring自动会切换为使用CGLIB代理。但Spring推荐使用面向对象编程,因此业务对象通常都会实现一个或多个接口,此时默认将使用JDK动态代理,但也可以强制使用CGLIB。

Spring AOP使用纯Java实现。它不需要专门的编译过程。Spring AOP不需要控制类装载器的层次,因此它可以在所有JavaWeb容器或应用服务器中运行良好。

Spring目前仅支持将方法调用作为连接点(JoinPoint),如果需要把对Field的访问和更新也作为通知(Advice)的连接点,则可以考虑使用AspectJ。

Spring实现AOP框架跟其他的框架不同。Spring并不是要提供最完整的AOP实现(尽管Spring AOP有这个能力),而是侧重于AOP实现和Spring IoC之间的整合,用于帮助解决在企业级开发中的常见问题。因此,Spring通常和IoC容器一起使用,Spring从来就没有大官通过提供一种全面的AOP解决方案来与AspectJ竞争。Spring AOP采用基于代理的AOP实现方案,而AspectJ则采用编译时增强的解决方案。

Spring2.0可以无缝的整合Spring AOP、IoC和AspectJ,是的所有的AOP应用完全融入基于Spring的框架,这样的集成不会影响Spring AOP API或者AOP Alliance API,Spring AOP保持了向下兼容性,依然允许直接使用Spring AOP API来完成AOP编程。

一旦我们掌握了AOP的概念,不难发现进行AOP编程是一件很简单的事情。纵观AOP编程,其实需要程序员参与的只有三个部分:

  • 定义普通的业务组件

  • 定义切入点(PointCut),一个切入点可能横切多个业务组件

  • 定义通知(Advice),通知及时在AOP框架为普通业务组件织入时的处理动作

第一部分是最为平常的事情了,无需说明。第二、三部分就是AOP的关键:一旦定义了合适的切入点和通知,AOP框架将会自动生成代理,而AOP代理的方法大致有如下公式:

代理对象的方法 = 通知 + 被代理对象的方法

Spring 1.x采用自身提供的AOP API来定义切入点和通知,程序可以直接使用Spring AOP API来定义切入点和通知,但这种方式似乎有些过时了,现在通常建议使用AspectJ方式来定义切入点和通知,在这种方式下,Spring依然有如下两种选择来定义切入点和通知:

  • 基于注解的配置方式:使用@Aspect ,@Pointcut等注解来标注切入点和通知

  • 基于XML配置文件的方式

二、基于Annotation的配置方式

AspectJ允许使用注解用于定义切面、切入点和通知,而Spring框架则可以识别并根据这些注解来生成AOP代理。Spring只是使用了和AspectJ 5一样的注解,但并没有使用AspectJ的编译器或者织入器,底层依然使用SpringAOP来实现,依然是在运行时动态生成AOP代理,因此不需要增加额外的编译,也不需要AspectJ的织入器支持。而AspectJ采用编译时增强,所以AspectJ需要使用自己的编译器来编译Java文件,还需要织入器。

为了启用Spring对@AspectJ切面配置的支持,并保证Spring容器中的目标Bean被一个或多个切面自动增强,必须在Spring配置文件中配置如下内容(第4、9、10、15行):

<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
    http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!-- 启动@AspectJ支持 -->
<aop:aspectj-autoproxy/>
</beans>

所谓自动增强,指的是Spring会判断一个或多个切面是否需要对指定的Bean进行增强,并据此自动生成相应的代理,从而使得通知在合适的时候被调用。如果不打算使用XML Schema的配置方式,则应该在Spring配置文件中增加如下片段来启用@AspectJ支持(即上面的<aop:aspectj-autoproxy />和下面创建Bean的方式选择一种即可启用@AspectJ支持):

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

上面配置的是一个Bean后处理器,该处理器将会为容器中Bean生成AOP代理。

为了在Spring应用中启动@AspectJ支持,还需要在用用的类加载路径下增加两个AspectJ库:aspectweaver.jar和aspectjrt.jar,直接使用AspectJ安装路径下的lib目录下的这两个Jar文件即可,当然,也可以在Spring解压缩文件夹的lib/aspectj路径下找到它们。下面是项目内容的截图:

这里写图片描述

1、定义切面Bean

当启用了@AspectJ支持后,只要我们在Spring容器中配置一个带@AspectJ注释的Bean,Spring将会自动识别该Bean,并将该Bean作为切面处理。下面是一个例子:

@Aspect
public class LogAspect {

}

切面类(用@Aspect修饰的类)和其他类一样可以有方法和属性的定义,还可能包括切入点、通知的定义。当我们使用@Aspect来修饰一个Java类后,Spring将不会把该Bean当成组件Bean处理,因此当Spring容器检测到某个Bean使用了@AspectJ标注之后,负责自动增强的后处理Bean将会忽略该Bean,不会对该Bean进行任何通知。

2、使用Before通知

当我们在一个切面类里使用@Before来标注一个方法时,该方法将作为Before通知。使用@Before标注时,通常需要指定一个value属性值,该属性值指定一个切入点表达式(既可以是一个已有的切入点,也可以直接定义切入点表达式),用于指定该通知将被织入哪些切入点。看例子:

package com.abc.advice;

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

@Aspect
public class BeforeAdviceTest {
    //匹配com.abc.service下的类中以before开始的方法
    @Before("execution(* com.abc.service.*.before*(..))")
    public void permissionCheck() {
        System.out.println("模拟权限检查");
    }
}

上面的程序使用@Aspect修饰了BeforeAdviceTest类,这表明该类是一个切面类,在该贴面里定义了一个permissionCheck方法——这个方法本来没有什么特殊之处,但因为使用了@Before来标注该方法,这就将该方法转换成一个Before通知。这个@Before注解中,直接指定了切入点表达式,指定com.abc.service包下的类中以before开始的方法的执行作为切入点。现假设我们在com.abc.service下有一个这样一个类:

package com.abc.service;
import org.springframework.stereotype.Component;

@Component
public class AdviceManager {
    //这个方法将被BeforeAdviceTest类的permissionCheck匹配到
    public void beforeAdvice() {
        System.out.println("方法: beforeAdviceTest");
    }
}

从上面的代码来看,这个AdviceManager是一个纯净的Java类,它丝毫不知道将被谁来增强,也不知道将被进行怎样的增强——正式因为AdviceManager类的这种“无知”,才是AOP的最大魅力:目标类可以被无限的增强。

在Spring配置文件中配置自动搜索Bean组件,配置自动搜索切面类,SpringAOP自动对Bean组件进行增强,下面是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:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <!-- 启动@AspectJ支持 -->
    <aop:aspectj-autoproxy/>

    <!-- 指定自动搜索Bean组件,自动搜索切面类 -->
    <context:component-scan base-package="com.abc.service,com.abc.advice">
        <context:include-filter type="annotation" 
            expression="org.aspectj.lang.annotation.Aspect" />
    </context:component-scan>
</beans>

主程序非常简单,通过Spring容器获取AdviceManager Bean,并调用Bean的beforeAdvice方法:

package com.abc.main;

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

import com.abc.service.AdviceManager;

@SuppressWarnings("resource")
public class AOPTest {
    public static void main(String[] args) {
        ApplicationContext context = 
            new ClassPathXmlApplicationContext("applicationContext.xml");
        AdviceManager manager = context.getBean(AdviceManager.class);
        manager.beforeAdvice();
    }
}

执行主程序,将看到以下结果:

这里写图片描述

使用Before通知只能在目标方法执行之前织入增强,使用Before通知无需理会目标方法的执行,所以Before处理无法阻止目标方法的执行。Before通知执行时,目标方法还未获得执行机会,所以Before通知无法访问目标方法的返回值。

3、使用AfterReturning通知

和使用@Before注解的使用类似,使用@AfterReturning来标注一个AfterReturning通知,该处理将在目标方法正常完成后被织入。使用@AfterReturning时可以指定两个属性:

  • pointcut/value:这两个属性的作用是一样的,都用于指定该切入点对应的切入表达式。同样的,既可以是一个已有的切入点,也可以是直接定义的切入点。当指定了pointcut属性后,value的属性值将会被覆盖

  • returning:指定一个返回值形参名,通知定义的方法可以通过该形参名来访问目标方法的返回值。

在com.abc.advice包下面增加AfterReturningAdviceTest,这个类定义了一个AfterReturning通知:

package com.abc.advice;

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

@Aspect
public class AfterReturningAdviceTest {
    //匹配com.abc.service下的类中以afterReturning开始的方法
    @AfterReturning(returning="returnValue", 
        pointcut="execution(* com.abc.service.*.afterReturning(..))")
    public void log(Object returnValue){
        System.out.println("目标方法返回值:" + returnValue);
        System.out.println("模拟日志记录功能...");
    }
}

并在AdviceManager类中增加以下内容:

//将被AfterReturningAdviceTest的log方法匹配
public String afterReturning() {
    System.out.println("方法:afterReturning");
    return "afterReturning方法";
}

正如上面程序中看到的,程序中使用@AfterReturning注解时,指定了一个returning属性,该属性的返回值是returnValue,这表明允许在增强方法log中使用名为returnValue的形参,该形参代表目标方法的返回值。在测试类AOPTest的main方法中增加调用本方法的语句,运行测试类,可以看到以下结果:

这里写图片描述

@AfterReturning注解的returning属性所指定的形参名必须对应通知中的一个形参名,当目标方法执行以后,返回值作为相应的参数传入给通知方法。

需要注意的是,使用@AfterReturning属性还有一个额外的作用,它可用于限定切入点之匹配具有对应返回值类型的方法——假设上面的log方法的参数returnValue的类型为String,那么该切入点只匹配com.abc.service.impl包下的返回值为String的所有方法。当然,上面的log方法返回值类型为Object,表明该切入点可匹配任何返回值的方法。除此之外,虽然AfterReturning通知可以访问到目标方法的返回值,但它不可改变这个返回值。

4、使用AfterThrowing通知

使用@AfterThrowing注解可用于标注一个AfterThrowing通知,这个处理主要用于处理陈旭中未处理的异常。使用这个注解时可以指定两个属性:

  • pointcut/value:这两个属性的作用是一样的,都用于指定该切入点对应的切入表达式。同样的,既可以是一个已有的切入点,也可以是直接定义的切入点。当指定了pointcut属性后,value的属性值将会被覆盖

  • throwing:指定一个返回值形参名,通知定义的方法可通过该形参名来访问目标方法中所抛出的异常对象。

    在com.abc.advice包下面增加AfterThrowingAdviceTest,这个类定义了一个AfterThrowing通知:

package com.abc.advice;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AfterThrowingAdviceTest {
    @AfterThrowing(throwing="ex",
        pointcut="execution(* com.abc.service.*.afterThrow*(..))")
    public void handleException(Throwable ex) {
        System.out.println("目标方法抛出异常:" +ex);
        System.out.println("模拟异常处理");
    }
}

并在AdviceManager类中增加以下内容:

//将被AfterThrowingAdviceTest的handleException方法匹配
public void afterThrowing() {
    System.out.println("方法: afterThrowing");
    try {
        int a = 10 / 0;
    } catch (ArithmeticException ae) {
        System.out.println("算术异常已被处理");
    }
    String s = null;
    System.out.println(s.substring(0,3));
}

正如上面程序中看到的,程序中使用@AfterThrowing注解时,指定了一个throwing属性,该属性的值是ex,这表明允许在增强方法log中使用名为ex的形参,该形参代表目标方法的抛出的异常对象。运行测试类,可以看到以下结果:

这里写图片描述

需要注意的是:如果一个异常在程序内部已经处理,那么Spring AOP将不会处理该异常。只有当目标方法抛出一个未处理的异常时,该异常将会作为对应的形参传给通知的方法。和AfterReturning类似的是,正确方法的参数类型可以限定切点只匹配指定类型的异常——假如上面的handleException方法的参数类型为NullPointerException,那么如果目标方法只抛出了ArithmaticException,则Spring AOP将不会处理这个异常。当然,handleException的参数类型为Throwable,则匹配了所有的Exception。

从测试结果中可以看到,AfterThrowing处理虽然可以对目标方法的异常进行处理,但这种处理与直接使用catch捕捉不同:catch捕捉意味着完全处理该异常,如果catch块中没有重新抛出新异常,则该方法可以正常结束;而AfterThrowing处理虽然处理了该异常,但它不能完全处理该异常,这个异常依然会传播到上一级调用者(本例中为JVM,故会导致程序终止)。

5、使用After通知

Spring还提供了一个After通知,它与AfterReturning优点类似,但也有区别:

  • AfterReturning通知只有在目标方法正确完成后才会被织入

  • After通知不管目标方法如何结束(正确还是异常),它都会被织入

正是因为这个特点,因此After通知必须准备处理正常返回和异常返回两种情况,这种通知通常用于释放资源。使用@After注解标注一个方法,即可将该方法转换为After通知。使用@After注解是需要指定一个value属性,用于指定该通知的切入点,既可以是一个已有的切入点,也可以直接定义切入点表达式。

在com.abc.advice包下面增加AfterAdviceTest,这个类定义了一个After通知:

@Aspect
public class AfterAdviceTest {
    @After(value="execution(* com.abc.servie.impl.*.afterAdvice*(..))")
    public void releaseResource() {
        System.out.println("模拟释放数据库连接");
    }
}

并在AdviceManager类中增加以下内容:

//将被AfterAdvice的releaseResource方法匹配
public void afterAdvice() {
    System.out.println("方法: afterAdvice");
}

上面定义了一个After通知,不管切入点的目标方法如何结束,该通知都会被织入。下面是测试结果:

这里写图片描述

6、使用Around通知

@Around注解用于标注Around通知,它近似等于Before通知和AfterReturning通知的总和,Around通知既可以在执行目标方法前织入增强动作,也可以在目标方法之后织入增强动作。

与@Before和@AfterReturning不同的是,@Around甚至可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。@Around可以修改目标方法的参数值,也可以修改目标方法的返回值。

@Around的功能虽然强大,但通常需要在线程安全的环境下使用,因此,如果使用普通的@Before和@AfterReturning就能解决的问题,就没有必要使用@Around了。如果需要目标方法执行之前和执行之后共享某种数据状态,则应该考虑使用@Around;尤其是需要使用通知阻止目标方法的执行,或者需要改变目标方法的参数和执行后的返回值时,就只能使用@Around了。

可以想象,使用@Around时,也需要指定一个value属性,这个属性依然是用于指定切入点。另外,当定义一个Around通知时,该方法的第一个形参必须是ProceedingJoinPoint类型(就是说至少包含一个形参),在通知方法体内,调用ProceedingJoinPoint的proceed()方法才会执行目标方法——这就是Around通知可以完全控制目标方法的执行时机、如何执行的关键,如果通知的方法体内没有调用这个proceed()方法,则目标方法不会执行。

调用proceed()方法时,还可以传入一个Object[]对象,该数组中的值将被传入目标方法作为执行方法的实参。因此我们可以通过这个参数,修改方法的参数值。

在com.abc.advice包下面增加AroundAdviceTest,这个类定义了一个Around通知:

package com.abc.advice;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AroundAdviceTest {
    @Around(value="execution(* com.abc.service.*.around*(..))")
    public Object process(ProceedingJoinPoint point) throws Throwable {
        System.out.println("模拟执行目标方法前的增强处理:事务开始...");
        //修改目标方法的参数
        String[] params = new String[]{"param1"};
        //执行目标方法,并保存目标方法执行后的返回值
        Object returnValue = point.proceed(params);
        System.out.println("模拟执行目标方法后的增强处理:事务结束...");
        //返回修改后的返回值
        return "方法实际返回值:" + returnValue + ",这是返回值的后缀";
    }
}

上面定义了一个AroundAdviceTest切面,该切面包含了一个Around通知:process()方法,该方法中第一行代码用于模拟调用目标方法之前的处理,第二行修改了目标方法的第一个参数,接下来调用目标方法,后面模拟调用目标方法之后的处理和对返回值的修改。正如前面说的,通过这个process方法,我们可以增加类似于@Before和@AfterReturning的通知,可以决定什么时候执行目标方法,可以修改目标方法的参数值,还可以修改目标方法的返回值,真是想做什么就做什么啊!

在AdviceManager类中增加以下内容:

//将被AroundAdvice的process方法匹配
public String aroundAdvice(String param1) {
    System.out.println("方法: aroundAdvice");
    return param1;
}

在com.abc.main.AOPTest中加入方法的调用,触发切点:

String result = manager.aroundAdvice("param1");
System.out.println("返回值:" + result);

执行测试类,结果如下:

这里写图片描述

需要注意的是,当调用ProceedingJoinPoint的proceed()方法时,传入的Object[]参数值将作为目标方法的参数,如果这个数组长度与目标方法的参数个数不等,或者数组元素的类型和目标方法的参数类型不匹配,程序就会出现异常。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值