第 3 章 Spring的AOP

Spring AOP 模块,是 Spring 框架体系结构中十分重要的内容,该模块中提供了面向切面编程实现 。本章将对 Spring AOP 的相关知识进行详细讲解

Spring AOP 简介
  • 什么是 AOP
  • AOP 的全称是 Aspect-Oriented Programming ,即面向切面编程(也称面向方面编程)是面向对象编程 (OOP) 的一种补充,目前已成为一种比较成熟的编程方式。
  • 在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作 虽然使用 OOP 可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能( 如日志记录),同样的代码仍然会分散到各个方法中 这样,如果想要关闭某个功能,或者对其进行修改,就必须要修改所有的相关方法 这不但增加了开发人员的工作量,而且提高了代码的出错率。
  • 为了解决这一问题, AOP 思想随之产生 AOP 采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方这种采用横向抽取机制的方式,采用传统的 OOP 思想显然是无法办到的,因为 OOP 只能实现父子关系的纵向的重用 虽然 AOP 是一种新的编程思想,但却不是 OOP 的替代品,它只是 OOP的延伸和补充。
  • AOP 思想中,类与切面的关系如图所示:



    从图中可以看出,通过 Aspect (切面)分别在 Class1 和 Class2 的方法中加入了事务、日志、权限和异常等功能。
    AOP 的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多地关注于其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性。
    目前最流行的 AOP 框架有两个,分别为 Spring AOP 和 AspectJ 。Spring AOP 使用纯 Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入增强的代码。AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始, Spring AOP 引入了对 AspectJ的支持, AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的织入。

AOP 术语

在学习使用 AOP 之前,首先要了解一下 AOP 的专业术语 这些术语包括 Aspect、Joinpoint 、Pointcut 、Advice、 Target Object 、Proxy 和 Weaving ,对于这些专业术语的解释,具体如下:


  • Aspect (切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、曰志等)的类,如图中的 Aspect 该类要被 Spring 容器识别为切面,需要在配置文件中通<bean>元素指定。
  • Joinpoint (连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在 Spring AOP 中,连接点就是指方法的调用。
  • Pointcut (切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点,如图所示。 通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以 add方法中,那么所有满足这一规则的方法都是切入点。
  • Advice( 通知/增强处理): AOP 框架在特定的切入点执行的增强处理,即在定义好的切入点处所要执行的程序代码。 可以将其理解为切面类中的方法,它是切面的具体实现。
  • Target Object (目标对象):是指所有被通知的对象,也称为被增强对象。 如果 AOP框架采用的是动态的 AOP 实现,那么该对象就是一个被代理对象。
  • Proxy (代理):将通知应用到目标对象之后,被动态创建的对象。
  • Weaving (织入):将切面代码插入到目标对象上,从而生成代理对象的过程。
动态代理

通过前面的学习,我们已经知道 AOP 中的代理就是由 AOP 框架动态生成的一个对象,该对象可以作为目标对象使用。 Spring 中的 AOP 代理,可以是 JDK 动态代理,也可以是 CGLIB代理。 接下来的两个小节中,将结合相关案例,来演示这两种代理方式的使用。

  • JDK 动态代理

JDK 动态代理是通过 java.lang. reflect. Proxy 类来实现的,我们可以调用 Proxy 类的newProxyl nstance() 方法来创建代理对象 对于使用业务接口的类, Spring 默认会使用 JDK动态代理来实现 AOP。
接下来,通过一个案例来演示 Spring 中JDK 动态代理的实现过程,具体步骤如下。
(1)创建一个名为 spring03 的Web 项目,导入 Spring 框架所需 JAR 包到项目的 lib 目录中,并发布到类路径下。
(2)在 src 目录下,创建一个com.neuedu.jdk 包,在该包下创建接口 UserDao ,并在该接口中编写添加和删除的方法,文件如下所示。

package com.neuedu.jdk;
public interface UserDao {
  public void addUser();
  public void deleteUser();
}

(3)在 com.neuedu.jdk 包中,创建 UserDao 接口的实现类 UserDaolmpl ,分别实现接口中的方法,并在每个方法中添加一条输出语句,文件如下所示。

package com.neuedu.jdk;
//目标类
public class UserDaolmpl implements UserDao {

  @Override
  public void addUser() {
      System.out.println("添加用户");
  }

  @Override
  public void deleteUser() {
      System.out.println("删除用户");
  }

}

需要注意的是,本案例中会将实现类UserDaolmpl 作为目标类,对其中的方法进行增强处理。
(4)在 src 目录下,创建一个 com.neuedu.aspect 包,并在该包下创建切面类 MyAspect,在该类中定义一个模拟权限检查的方法和一个模拟记录日志的方法,这两个方法就表示切面中的通知,文件如下所示。

package com.neuedu.aspect;
//切面类:可以存在多个通知 Advice (即增强的方法) 
public class MyAspect {
  public void check_Permissions(){
      System.out.println("模拟检查权限......");
  }
  
  public void log(){
      System.out.println("模拟记录日志......");
  }
}

(5)在 com.neuedu.jdk 包下,创建代理类 JdkProxy ,该类需要实现InvocationHandler接口,并编写代理方法。 在代理方法中,需要通过 Proxy 类实现动态代理,文件如下所示。

package com.neuedu.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.neuedu.aspect.MyAspect;
/**
* JDK代理类
*/
public class JdkProxy implements InvocationHandler {
  //声明目标类接口 
  private UserDao userDao;
  //创建代理方法 
  public Object createProxy(UserDao userDao){
      this.userDao = userDao;
      //1.类加载器 
      ClassLoader classLoader = JdkProxy.class.getClassLoader();
      //2.被代理对象实现的所有接口 
      Class[] clazz = userDao.getClass().getInterfaces();
      //3.使用代理类,进行增强,返回的是代理后的对象 
      return Proxy.newProxyInstance(classLoader, clazz, this);
  }
  
  /**
   * 所有动态代理类的方法调用,都会交由 invoke()方法去处理  
   * proxy: 被代理后的对象  
   * method: 将要被执行的方法信息(反射) 
   * args: 执行方法时需要的参数 
   * 
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //声明切面
      MyAspect myAspect = new MyAspect();
      //前增强
      myAspect.check_Permissions();
      //在目标类上调用方法,并传入参数 
      Object obj = method.invoke(userDao, args);
      //后增强
      myAspect.log();
      return obj;
  }

}

在上述文件中, JdkProxy 类实现了InvocationHandler 接口,并实现了接口中的 invoke()方法,所有动态代理类所调用的方法都会交由该方法处理。 在创建的代理方法 createProxy() 中,使用了 Proxy 类的 newProxyl nstance()方法来创建代理对象 。newProxyl nstance() 方法中包含3个参数,其中第 1 个参数是当前类的类加载器,第 2个参数表示的是被代理对象实现的所有接口,第 3 个参数 this 代表的就是代理类 JdkProxy 本身。 invoke() 方法中,目标类方法执行的前后,会分别执行切面类中的 check_Permissions() 方法和 log() 方法。
(6)在 com.neuedu.jdk 包中,创建测试类 JdkTest 。在该类中的 main() 方法中创建代理对象和目标对象,然后从代理对象中获得对目标对象 userDao 增强后的对象,最后调用该对象中的添加和删除方法,文件如下所示。

package com.neuedu.jdk;

public class JdkTest {

  public static void main(String[] args) {
      //创建代理对象 
      JdkProxy jdkProxy = new JdkProxy();
      //创建目标对象 
      UserDao userDao = new UserDaolmpl();
      //从代理对象中获取增强后的日标对象
      UserDao userDaoProxy = (UserDao)jdkProxy.createProxy(userDao);
      //执行方法 
      userDaoProxy.addUser();
      userDaoProxy.deleteUser();
  }

}

执行程序后,控制台的输出结果如图所示。



从图中可以看出, userDao 实例中的添加用户和删除用户的方法已被成功调用,并且在调用前后分别增加了检查权限和记录日志的功能。 这种实现了接口的代理方式,就是 Spring中的 JDK 动态代理。

  • CGLlB 代理

JDK 动态代理的使用非常简单,但它还有一定的局限性一一使用动态代理的对象必须实现一个或多个接口。 如果要对没有实现接口的类进行代理,那么可以使用 CGLIB代理。
CGLIB( Code Generation Library )是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。在 Spring 的核心包中已经集成CGLIB所需要的包,所以开发中不需要另外导入 JAR 包。
接下来,通过一个案例来演示 CGLIB代理的实现过程,具体步骤如下。
( 1 )在 src 目录下,创建一个 com.neuedu.cglib 包,在包中创建一个目标类 UserDao ,UserDao不需要实现任何接口,只需定义一个添加用户的方法和一个删除用户的方法,文件如下所示。

package com.neuedu.cglib;
//目标类
public class UserDao {
  public void addUser() {
      System.out.println("添加用户");
  }

  public void deleteUser() {
      System.out.println("删除用户");
  }
}

(2 )在 com.neuedu.cglib 包中,创建代理类 CglibProxy ,该代理类需要实现 MethodInterceptor接口,并实现接口中的 intercept()方法,文件如下所示。

package com.neuedu.cglib;
import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import com.neuedu.aspect.MyAspect;

public class CglibProxy implements MethodInterceptor{

  //代理方法
  public Object createProxy(Object target){
      //创建一个动态类对象
      Enhancer enhancer = new Enhancer();
      //确定需要增强的类,设置其父类
      enhancer.setSuperclass(target.getClass());
      //添加回调函数
      enhancer.setCallback(this);
      //返回创建的代理类
      return enhancer.create();
  }
  
  /**
   * proxy: CGlib 根据指定父类生成的代理对象 
   * method: 拦截的方法   
   * args: 拦截方法的参数数组   
   * methodProxy: 方法的代理对象,用于执行父类的方法 
   */
  @Override
  public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      //创建切面类对象
      MyAspect myAspect = new MyAspect();
      //前增强
      myAspect.check_Permissions();
      //目标方法执行
      Object obj = methodProxy.invokeSuper(proxy, args);
      //后增强
      myAspect.log();
      return obj;
  }

}

在上述文件的代理方法中,首先创建了一个动态类对象 Enhancer ,它是 CGLIB的核心类;然后调用了 Enhancer 类的setSuperclass()方法来确定目标对象;接下来调用了 setCallback()方法添加回调函数,其中的 this 代表的就是代理类 CglibProxy 本身;最后通过 return 语句将创建的代理类对象返回。intercept() 方法会在程序执行目标方法时被调用,方法运行时将会执行切面类中的增强方法。
(3 )在 com.neuedu.cglib 包中,创建测试类 CglibTest 。在该类的 main() 方法中首先创建代理对象和目标对象,然后从代理对象中获得增强后的目标对象,最后调用对象的添加和删除方法,文件如下所示。

package com.neuedu.cglib;

public class CglibTest {

  public static void main(String[] args) {
      //创建代理对象
      CglibProxy cglibProxy = new CglibProxy();
      //创建目标对象
      UserDao userDao = new UserDao();
      //从代理对象中获取增强后的日标对象
      UserDao userDaoProxy = (UserDao)cglibProxy.createProxy(userDao);
      //执行方法
      userDaoProxy.addUser();
      userDaoProxy.deleteUser();

  }

}

执行程序后,控制台的输出结果如图所示。



从图中可以看出,目标类 UserDao 中的方法被成功调用并增强了。 这种没有实现接口的代理方式,就是 CGLIB代理。

基于代理类的 AOP 实现

通过前面小节的学习,大家对 Spring 中的两种代理模式已经有了一定的了解。 实际上, Spring中的 AOP 代理默认就是使用 JDK 动态代理的方式来实现的。在 Spring 中,使用 ProxyFactoryBean 是创建 AOP 代理的最基本方式。 接下来的两个小节中,将对 Spring 中基于代理类的 AOP 实现的相关知识进行详细讲解。

  • Spring 的通知类型

在讲解具体的代理类之前,我们需要先了解一下 Spring 的通知类型。 Spring 中的通知按照在目标类方法的连接点位置,可以分为以下 5 种类型。

  • org. aopalliance.intercept.MethodInterceptor (环绕通知)
    在目标方法执行前后实施增强,可以应用于曰志、事务管理等功能。
  • org.springframework.aop.MethodBeforeAdvice (前置通知)
    在目标方法执行前实施增强,可以应用于权限管理等功能。
  • org.springframework.aop.AfterReturningAdvice (后置通知)
    在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能。
  • org.springframework.aop.ThrowsAdvice (异常通知)
    在方法抛出异常后实施增强,可以应用于处理异常记录曰志等功能。
  • org.springframework.aop.IntroductionInterceptor (引介通知)
    在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。
  • ProxyFactoryBean

ProxyFactoryBean 是 FactoryBean 接口的实现类, FactoryBean 负责实例化一个 Bean ,而ProxyFactoryBean 负责为其他 Bean 创建代理实例。在 Spring 中,使用 ProxyFactoryBean 是创建 AOP 代理的基本方式。
ProxyFactoryBean 类中的常用可配置属性如下表所示。

属性名称 描述
target 代理的目标对象
proxylnterfaces 代理要实现的接口,如果是多个接口,可以使用以下格式赋值
<Iist>
                  <value></value>
.....
</Iist>
proxyTargetClass 是否对类代理而不是接口,设置为 true时, 使用 CGLlB 代理
interceptorNames 需要织入目标的 Advice
singleton 返回的代理是否为单实例,默认为 true (即返回单实例)
optimize 当设置为 true 时,强制使用 CGLlB

对ProxyFactoryBean 类有了初步的了解后,接下来通过一个典型的环绕通知案例,来演示Spring使用 ProxyFactoryBean 创建 AOP 代理的过程,具体步骤如下。
( 1 )在核心 JAR 包的基础上,再向 spring03 项目的 lib 目录中导入 AOP 的 JAR包
spring-aop-4.3 6.RELEASE.jar 和 aopalliance-1.0.jar,如图所示。



关于这两个 JAR 包的介绍如下。

  • spring-aop-4.3.6.RELEASE. jar: Spring AOP 提供的实现包, Spring 的包中已经提供。
  • aopalliance-1.0.jar: 是AOP 联盟提供的规范包,该 JAR包可以通过地址" http://mvnrepository.com/artifact/aopalliance/aopalliance/1.0" 下载。
    ( 2 )在 src 目录下,创建一个 com.neuedu.factorybean 包,在该包中创建切面类 MyAspect。 由于实现环绕通知需要实现
    org.aopalliance.intercept.MethodInterceptor 接口,所以MyAspect 类需要实现该接口,并实现接口中的 invoke() 方法,来执行目标方法,文件如下所示。
package com.neuedu.factorybean;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//切面类
public class MyAspect implements MethodInterceptor {

  @Override
  public Object invoke(MethodInvocation mi) throws Throwable {
      //执行目标方法
      Object obj = mi.proceed();
      log();
      return obj;
  }
  public void check_Permissions(){
      System.out.println("模拟检查权限......");
  }
  
  public void log(){
      System.out.println("模拟记录日志......");
  }

}

这里为了演示效果,在目标方法前后分别执行了检查权限和记录日志的方法,这两个方法也就是增强的方法 ,也就是通知。
(3 )在 com.neuedu.factorybean 包中,创建配置文件 applicationContext.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"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
  <!-- 1.目标类 -->
  <bean id="userDao" class="com.neuedu.jdk.UserDaolmpl"></bean>
  <!-- 2.切面类 -->
  <bean id="myAspect" class="com.neuedu.factorybean.MyAspect"></bean>
  <!-- 3.使用 Spring 代理工厂定义一个名称为 userDaoProxy 的代理对象 -->
  <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
      <!-- 3.1.指定代理实现的接口 -->
      <property name="proxyInterfaces" value="com.neuedu.jdk.UserDao"></property>
      <!-- 3.2.指定目标对象 -->
      <property name="target" ref="userDao"></property>
      <!-- 3.3.指定切面,植入环绕通知 -->
      <property name="interceptorNames" value="myAspect"></property>
      <!-- 3.4.指定代理方式, true: 使用 cglib, false (默认) : 使用 jdk 动态代理 -->
      <property name="proxyTargetClass" value="true"></property>
  </bean>
  
</beans>        

在上述文件中,首先通过<bean>元素定义了目标类和切面,然后使用 ProxyFactoryBean类定义了代理对象。 在定义的代理对象中,分别通过<property> 子元素指定了代理实现的接口、代理的目标对象、需要织入目标类的通知以及代理方式。
(4 )在 com.neuedu.factorybean 包中,创建测试类 ProxyFactoryBeanTest ,在类中通过 Spring 容器获取代理对象的实例,并执行目标方法,文件如下所示。

package com.neuedu.factorybean;

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

import com.neuedu.jdk.UserDao;

//测试类
public class ProxyFactoryBeanTest {

  public static void main(String[] args) {
      String xmlPath = "com/neuedu/factorybean/applicationContext.xml";
      ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
      //从Spring容器获得内容
      UserDao userDao = (UserDao)applicationContext.getBean("userDaoProxy");
      //执行方法
      userDao.addUser();
      userDao.deleteUser();
  }
}

执行程序后,控制台的输出结果如图所示。


AspectJ 开发

AspectJ 是一个基于 Java 语言的 AOP 框架,它提供了强大的 AOP 功能。 Spring 2.0 以后,Spring AOP 引入了对 AspectJ 的支持,并允许直接使用 AspectJ 进行编程,而 Spring 自身的AOP API 也尽量与 AspectJ保持一致。 新版本的 Spring 框架,也建议使用 AspectJ 来开发 AOP。使用 AspectJ 实现 AOP 有两种方式:一种是基于 XML 的声明式 AspectJ ,另一种是基于注解的声明式 AspectJ。 接下来的两个小节中,将对这两种 AspectJ 的开发方式进行讲解。

  • 基于 XML 的声明式 AspectJ

基于 XML 的声明式 AspectJ 是指通过 XML 文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在<aop:config> 元素内。 <aop:config> 元素及其子元素如图所示。



在上图中, Spring 配置文件中的 <beans> 元素下可以包含多个<aop:config> 元素,一个<aop:config> 元素中又可以包含属性和子元素,其子元素包括<aop:pointcut>、<aop:advisor>和<aop :aspect> 。在配置时,这 3 个子元素必须按照此顺序来定义。在<aop:aspect>元素下,同样包含了属性和多个子元素,通过使用 <aop:aspect>元素及其子元素就可以在 XML 文件中配置切面 、切入点和通知。图中灰色部分标注的元素即为常用的配置元素,这些常用元素的配置代代码如下所示。

<!-- 定义切面Bean -->
     <bean id="myAspect" class="com.neuedu.aspectj.xml.MyAspect"></bean>
     <aop:config>
          <!-- 配置切面  -->
          <aop:aspect id="aspect" ref="myAspect">
              <!-- 配置切入点  -->
              <aop:pointcut expression="execution(* com.neuedu.jdk.*.*(..))" id="myPointCut"/>
              <!-- 配置通知 -->
              <!-- 前置通知 -->
              <aop:before method="myBefore" pointcut-ref="myPointCut"/>
              <!-- 后置通知  -->
              <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
              <!-- 环绕通知  -->
              <aop:around method="myAround" pointcut-ref="myPointCut"/>
              <!-- 异常通知  -->
              <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
              <!-- 最终通知  -->
              <aop:after method="myAfter" pointcut-ref="myPointCut"/>
          </aop:aspect>
     </aop:config>

为了让大家能够清楚地掌上述代码中的配置信息,下面对上述代码的配置内容进行详细讲解。

  1. 配置切面
    在 Spring 的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已经定义好的 Spring Bean 转换成切面 Bean,所以要在配置文件中先定义一个普通的 Spring Bean(如上述代码中定义的 myAspect)。 定义完成后, 通过 <aop:aspec t>元素的 ref 属性即可引用该 Bean。
    配置<aop:aspect> 元素时, 通常会指定 id 和 ref 两个属性,如下表所示。
属性名称 描述
id 用于定义该切面的唯一标识名称
ref 用于引用普通的 Spring Bean
  1. 配置切入点
    在 Spring 的配置文件中,切入点是通过<aop:pointcu t>元素来定义的。 当<aop:pointcut>元素作为 <aop:config> 元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享 ;<aop:pointcut> 元素作为 <aop:aspect> 元素的子元素时,表示该切入点只对当前切面有效。
    在定义<aop:poíntcut> 元素时,通常会指定 id 和 expresslon 两个属性,如下表所示。
属性名称 描述
id 用于指定切入点的唯一标识名称
expresslon 用于指定切入点关联的切入点表达式

在上述配置代码片段中, execution(* com. neuedu.jdk..(..))就是定义的切入点表达式,该切入点表达式的意思是匹配 com.neuedu.jdk 包中任意类的任意方法的执行。 其中 execution()表达式的主体,第 1 个表示的是返回类型,使用代表所有类型; com.neuedu.jdk 表示的是需要拦截的包名,后面第 2个表示的是类名,使用代表所有的类;第 3 个表示的是方法名,使用表示所有方法;后面(..)表示方法的参数,其中的" .."表示任意参数 。需要注意的是,第 1 个*与包名之间有一个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方式,而 Spring AOP 中切入点表达式的基本格式如下:
上述格式中,各部分说明如下。

  • modifiers-pattern: 表示定义的目标方法的访问修饰符,如 public、prívate等。
  • ret-type-pattern: 表示定义的目标方法的返回值类型,如 void、String等。
  • declaring-type-pattern: 表示定义的目标方法的类路径,如 com.neuedu.jdk.UserDaolmpl。
  • name-pattern: 表示具体需要被代理的目标方法,如 add()方法。
  • param-pattern 表示需要被代理的目标方法包含的参数,本章示例中目标方法参数都为空。
  • throws-pattern: 表示需要被代理的目标方法抛出的异常类型。
    其中带有问号(?)的部分,如 modifiers-pattern、 declaring-type-pattern 和 throws-pattern表示可配置项;而其他部分属于必须配置项。
    想要了解更多切点表达式的配置信息,大家可以参考 Spring 官方文档的切入点声明部分( Declaring a pointcut )。
  1. 配置通知
    在配置代码中,分别使用 <aop:aspect> 的子元素配置了 5 种常用通知,这 5 个子元素不支持使用子元素,但在使用时可以指定一些属性,如下表所示。
属性名称 描述
pointcut 该属性用于指定一个切入点表达式, Spring 将在匹配该表达式的连接点时织入该通知
pointcut-ref 该属性指定一个已经存在的切入点名称,如配置代码中的 myPointCut 。通常pointcut 和 pointcut-ref 两个属性只需要使用其中之一
method 该属性指定一个方法名,指定将切面 Bean 中的该方法转换为增强处理
throwing 该属性只对 <after-throwing> 元素有效, 用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常
returning 该属性只对<after-returning>元素有效, 它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值

了解了如何在 XML 中配置切面、切入点和通知后,接下来通过一个案例来演示如何在 Spring中使用基于 XML 的声明式 AspectJ ,具体实现步骤如下。
( 1 )导入 AspectJ 框架相关的 JAR 包,具体如下

  • spring-aspects-4.3.6.RELEASE.jar: Spring 为AspectJ 提供的实现, Spring 的包中已经提供。
  • aspectjweaver-1.8.10.jar:是AspectJ 框架所提供的规范,大家可以通过网址"http://mvnrepository.com/artifact/org.aspectj/aspectjweaver/1.8.10" 下载。
    (2 )在spring03 项目的 src 目录下,创建一个 com.neuedu.aspectj.xml 包,在该包中创建切面类 MyAspect ,并在类中分别定义不同类型的通知,文件如下所示。
package com.neuedu.aspectj.xml;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

/**
* 切面类,在此类中编写通知
*
*/
public class MyAspect {
  //前置通知
  public void myBefore(JoinPoint joinPoint) {
      System.out.print("前置通知:模拟执行权限检查......");
      System.out.print("目标类是:"+joinPoint.getTarget());
      System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
  }
  //后置通知
  public void myAfterReturning(JoinPoint joinPoint){
      System.out.print("后置通知:模拟记录日志...... ," ); 
      System.out.println("被植入增强处理的目标方法为: "+ joinPoint.getSignature().getName()); 
      
  }
  /**
   * 环绕通知
   * proceedingJoinPoint:是JoinPoint子接口,表示可执行目标方法
   * 1.必须是Object类型的返回值
   * 2.必须接收一个参数,类型为ProceedingJoinPoint
   * 3.必须Throws Throwable
   */
  public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
      //开始
      System.out.println("环绕开始:执行目标方法之前,模拟开启事务......");
      //执行当前目标方法
      Object obj = proceedingJoinPoint.proceed();
      //结束
      System.out.println("环绕结束:执行目标方法之后,模拟关闭事务......");
      return obj;
  }
  //异常通知
  public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
      System.out.println("异常通知:" + "出错了" + e.getMessage ()); 
  }
  //最终通知
  public void myAfter(){
      System.out.println("最终通知: 模拟方法结束后的释放资源 ......");
  }
}

在上述文件中,分别定义了 种不同类型的通知,在通知中使用JoinPoint 接口及其子接口 ProceedingJoinPoint 作为参数来获得目标对象的类名、目标方法名和目标方法参数等。
需要注意的是,环绕通知必须接收一个类型为 ProceedingJoinPint 的参数,返回值也必须是Object 类型,且必须抛出异常。 异常通知中可以传入 Throwable 类型的参数来输出异常信息。
( 3 )在com.neuedu.aspectj.xml 包中,创建配置文件 applicationContext.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:aop="http://www.springframework.org/schema/aop" 
     xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.3.xsd  
     http://www.springframework.org/schema/aop 
     http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> 
     
     <!-- 1.目标类 -->
     <bean id="userDao" class="com.neuedu.jdk.UserDaolmpl"></bean>
     <!-- 2.切面类 -->
     <bean id="myAspect" class="com.neuedu.aspectj.xml.MyAspect"></bean>
     <!-- 3.AOP编程 -->
     <aop:config>
          <!-- 配置切面  -->
          <aop:aspect ref="myAspect">
              <!-- 3.1 配置切入点,通知最后增强哪些方法 -->
              <aop:pointcut expression="execution(* com.neuedu.jdk.*.*(..))" id="myPointCut"/>
              <!-- 3.2 关联通知Advice和切入点pointCut -->
              <!-- 3.2.1 前置通知 -->
              <aop:before method="myBefore" pointcut-ref="myPointCut"/>
              <!--  3.2.2 后置通知 , 在方法返回之后执行, 就可以获得返回值  
              returning 属性: 用于设置后置通知的第二个参数的名称, 类型是 Object -->
              <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
              <!--  3.2.3 环绕通知  -->
              <aop:around method="myAround" pointcut-ref="myPointCut"/>
              <!-- 3.2.4 抛出通知:用于处理程序发生异常 -->
              <!-- * 注意:如果程序没有发生异常 -->
              <!-- * throwing属性:用于设置通知第二个参数的名称,类型 Throwable  -->
              <aop:after method="myAfter" pointcut-ref="myPointCut"/>
          </aop:aspect>
     </aop:config>
     
</beans>

在上述文件中,分别引入了 AOP 的 Schema 约束,然后在配置文件中分别定义了目标类、切面和 AOP 的配置信息。
小提示:
在 AOP 的配置信息中,使用 <aop:after-returning> 配置的后置通知和使用 <aop:after> 配置的最终通知虽然都是在目标方法执行之后执行,但它们也是有所区别的。 后置通知只有在目标方法成功执行后才会被织入,而最终通知不论目标方法如何结束(包括成功执行和异常中止两种情况) ,它都会被织入。
( 4 )com.neuedu.aspectj.xml 包下,创建测试类 TestXmlAspectj ,在类中为了更加清晰地演示几种通知的执行情况,这里只对 addUser()方法进行增强测试,文件如下所示。

package com.neuedu.aspectj.xml;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.neuedu.jdk.UserDao;
//测试类
public class TestXmlAspectj {
  public static void main(String[] args) {
      String xmlPath = "com/neuedu/aspectj/xml/applicationContext.xml";
      ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
      //从Spring容器获得内容
      UserDao userDao = (UserDao)applicationContext.getBean("userDao");
      //执行方法
      userDao.addUser();
  }
}

执行程序后,控制台的输出结果如图所示



要查看异常通知的执行效果,可以在 UserDaolmpl 类的 addUser() 方法中添加错误代码,如"int i = 10/0;" ,重新运好测试类,将可以看到异常通知的执行,此时控制台的输出结果如图所示。



从图中可以看出,使用基于 XML 的声明式AspectJ 已经实现了 AOP 开发。
  • 基于注解的声明式 AspectJ

与基于代理类的 AOP 实现相比,基于 XML 的声明式 ApectJ 要便捷得多,但是它也存在着一些缺点,那就是要在 Spring 文件中配置大量的代码信息。 为了解决这个问题, AspectJ 框架为 AOP的实现提供了一套注解,用以取代 Spring 配置文件中为实现 AOP 功能所配置的臃肿代码。
关于 AspectJ 注解的介绍,如下表所示。

注解名称 描述
@Aspect 用于定义一个切面
@Pointcut 用于定义切入点表达式 在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称 实际上,这个方法签名就是一个返回值为 void ,且方法体为空的普通的方法
@Before 用于定义前置通知,相当于 BeforeAdvice 。在使用时,通常需要指定一个 value 属性值,该属性值用于指定一个切入点表达式( 可以是己有的切入点,也可以直接定义切 点表达式)
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice 。在使用时可以指定pointcutlvalue 和 returning 属性,其中pointcut/value 这两个属性的作用一样,都用于指定切入点表达式。returning 属性值用于表示Advice 方法中可定义与此罔名的形参,该形参可用于访问目标方法的返回值
@Around 用于定义环绕通知,相当于 Method Interceptor。 在使用时需要指定一个 value 属性,该属性用于指定该通知被植入的切入点
@AfterThrowing 用于定义异常通知采处理程序中来处理的异常,相当于 ThrowAdvice 。在使用时可指定pointcutlvalue 和 throwing 属性 其中 pointcut/value 用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示 Adv ice 方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常
@After 用于定义最终 final 通知, 不管是否异常,该通知都会执行。使用时需要指定一个 value 属性,该属性用于指定该通知被植入的切入点
@DeclareParents 用于定义引介通知,相当于 Introductionlnterceptor (不要求掌握)

为了让大家可以快速地掌握这些注解,接下来重新使用注解的形式来实现前面小节的案例,具体步骤如下。
( 1 )在spring03 项目的 src 目录下,创建 com.neuedu.aspectj.annotation 包,将前面文件切面类 MyAspect 复制到该包下,并对该文件进行编辑,文件如下所示。

package com.neuedu.aspectj.annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
* 切面类,在此类中编写通知
*
*/
@Aspect
@Component
public class MyAspect {
  //定义切入点表达式
  @Pointcut("execution(* com.neuedu.jdk.*.*(..))")
  //使用一个返回值为void、方法体为空的方法来命名切入点
  private void myPointCut(){}
  //前置通知
  @Before("myPointCut()")
  public void myBefore(JoinPoint joinPoint) {
      System.out.print("前置通知:模拟执行权限检查......");
      System.out.print("目标类是:"+joinPoint.getTarget());
      System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
  }
  //后置通知
  @AfterReturning(value="myPointCut()") 
  public void myAfterReturning(JoinPoint joinPoint){
      System.out.print("后置通知:模拟记录日志...... ," ); 
      System.out.println("被植入增强处理的目标方法为: "+ joinPoint.getSignature().getName()); 
      
  }
  /**
   * 环绕通知
   * proceedingJoinPoint:是JoinPoint子接口,表示可执行目标方法
   * 1.必须是Object类型的返回值
   * 2.必须接收一个参数,类型为ProceedingJoinPoint
   * 3.必须Throws Throwable
   */
  @Around("myPointCut()") 
  public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
      //开始
      System.out.println("环绕开始:执行目标方法之前,模拟开启事务......");
      //执行当前目标方法
      Object obj = proceedingJoinPoint.proceed();
      //结束
      System.out.println("环绕结束:执行目标方法之后,模拟关闭事务......");
      return obj;
  }
  //异常通知
  @AfterThrowing(value="myPointCut()",throwing="e") 
  public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
      System.out.println("异常通知:" + "出错了" + e.getMessage ()); 
  }
  //最终通知
  @After("myPointCut()") 
  public void myAfter(){
      System.out.println("最终通知: 模拟方法结束后的释放资源 ......");
  }

}

在上述文件中,首先使用 @Aspect 注解定义了 切面类,由于该类在 Spring 中是作为组件使用的,所以还需要添加@Component 注解才能生效。 然后使用了@Poincut 注解来配置切入点表达式, 并通过定义方法来表示切入点名称。 接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称 "myPointCut" 作为参数传递给需要执行增强的通知方法。 如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。
(2) 在目标类 com.neuedu.jdk.UserDaoImpl 中添加注解@Repository("userDao")。
(3)在 com.neuedu.aspectj.annotation 包下,创建配置文件applicationContext.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: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-4.3.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.3.xsd">
                   
  <!-- 指定需要扫描的包,使注解生效  -->
  <context:component-scan base-package="com.neuedu"></context:component-scan>
  <!-- 启动基于注解的声明式 AspectJ 支持 -->
  <aop:aspectj-autoproxy /> 
  
</beans>   

在上述文件中,首先引入了 context 约束信息,然后使用 <context> 元素设置了需要扫描的包,使注解生效。 由于此案例中的目标类位于 com.neuedu.jdk 包中,所以这里设置base-package 的值为 "com.neuedu" 最后,使用 <aop:aspectj-autoproxy />来启动 Spring对基于注解的声明式 AspectJ 的支持。
(4)在com.neuedu.aspectj.annotation 包中,创建测试类 TestAnnotation ,该类与前面小节中文件基本一致 ,只是配置文件的路径有所不同,文件如下所示。

package com.neuedu.aspectj.annotation;

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

import com.neuedu.jdk.UserDao;

//测试类
public class TestAnnotation {

  public static void main(String[] args) {
      String xmlPath = "com/neuedu/aspectj/annotation/applicationContext.xml";
      ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
      //从Spring容器获得内容
      UserDao userDao = (UserDao)applicationContext.getBean("userDao");
      //执行方法
      userDao.addUser();
  }
}

执行程序后,控制台的输出结果如图所示。


在前面小节的方式来演示异常通知的执行,控制台的输出结果如上图所示。

从两张图中可以看出,基于注解的方式与基于 XML 的方式的执行结果相同,只是在目标方法前后通知的执行顺序发生了变化。 相对来说,使用注解的方式更加简单 、方便,所以在实际开发中推荐使用注解的方式进行 AOP 开发。
注意:如果在同一个连接点有多个通知需要执行,那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知的。

本章小结

本章主要讲解了 Spring 框架中 AOP 的相关知识 。首先对 AOP 进行了简单的介绍,然后讲解了 Spring 中的两种动态代理,接下来讲解了 Spring 中基于代理类的 AOP 实现,最后讲解了如何使用 AspectJ 框架来进行 AOP 开发。 通过本章的学习,大家可以了解 AOP 的概念和作用,理解 AOP 中的相关常用术语,熟悉 Spring 中两种动态代理方式的区别,并能够掌握基于代理类和AspectJ 框架的 AOP 开发方式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容