欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring框架(三)——AOP

程序员文章站 2022-05-06 20:35:18
...

一、AOP简介

  AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在 OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
  AOP 技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
  使用”横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
  AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。

二、使用AOP 的好处

  每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级。
 
  业务模块更简洁, 只包含核心业务代码。

三、AOP 中的一些术语

  横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
  切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。是对横切关注点的抽象。
  通知(Advice): 切面必须要完成的工作,即增强处理,指拦截到连接点之后要执行的代码。通知分为前置、后置、异常、返回、环绕通知五类。
  目标(Target): 被通知的对象。
  代理(Proxy): 向目标对象应用通知之后创建的对象。
  连接点(JoinPoint)程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。在Spring中连接点指的就是被拦截到的方法。连接点由两个信息确定:方法表示的程序执行点,相对点表示的方位。
  切点(pointcut)对连接点进行拦截的定义每个类都拥有多个连接点:例如 CalculatorImpl的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

四、Spring对AOP的支持

  在 Spring2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP。
  AspectJ:Java 社区里最完整、最流行的 AOP 框架。
  
  Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。
  
  Spring创建代理的规则为:
  
  1、默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  2、当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。
  3、如果目标对象实现了接口,并指定为CGLIB代理,则使用CGLIB动态代理。

  AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:
  1、定义普通业务组件。
  2、定义切入点,一个切入点可能横切多个业务组件。
  3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作。
  
  所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

五、基于 AspectJ 注解的方式配置AOP

接下来看一个例子:

  在一个支持加、减、乘、除的计算器类中,为加、减、乘、除的方法添加Log,为了避免代码的混乱与代码的冗余,利用AspectJ框架,进行AOP编程。
  

一、在 Spring 中启用 AspectJ 注解支持

  要在 Spring 应用中使用 AspectJ 注解,必须在 classpath 下包含 AspectJ 类库: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar。
  通过Maven完成对项目的构建,pom文件中的代码如下。
  

  <dependencies>

    <!-- Spring核心包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.6.RELEASE</version>
    </dependency>

    <!-- pring与aspectJ的整合-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>4.1.6.RELEASE</version>
    </dependency>

  </dependencies>

  首先,将 aop Schema 添加到 < beans > 根元素中。要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>。当 Spring IOC 容器侦测到 Bean 配置文件中的<aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理。
  配置文件如下:
  

<?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="com.spring.aop"/>

    <!-- 使 AspectJ 的注解起作用 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

二、编写Calculator接口类与其实现类,将CalculatorImpl 添加到IOC容器内进行管理

package com.spring.aop;


public interface ICalculator {

    int add(int i, int j);
    int sub(int i, int j);

    int mul(int i, int j);
    int div(int i, int j);
}
package com.spring.aop;

import org.springframework.stereotype.Component;


@Component("iCalculator")
public class CalculatorImpl implements ICalculator{

    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

三、用 AspectJ 注解声明切面

  要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例。当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。
  在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。
  通知是标注有某种注解的简单的 Java 方法。
  AspectJ 支持 5 种类型的通知注解: 

  • @Before: 前置通知,在方法执行之前执行。
  • @After: 后置通知,在方法执行之后执行。
  • @AfterRunning: 返回通知, 在方法返回结果之后执行。
  • @AfterThrowing: 异常通知, 在方法抛出异常之后。
  • @Around: 环绕通知,围绕着方法执行。

切面类LoggingAspect如下:

package com.spring.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;


@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.spring.aop.ICalculator.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();

        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }
}

  注1:@Aspect标识这个类是一个切面。@Before标识这个方法是个前置通知, 切点表达式execution(* com.spring.aop.ICalculator.*(..))表示执行 ICalculator接口的 所有 方法. * 代表匹配任意修饰符及任意返回值,参数列表中的 .. 代表匹配任意数量的参数(见下面的利用通配符编写 AspectJ 切入点表达式)。
  注2:可以在通知方法中声明一个类型为 JoinPoint 的参数, 然后就能访问链接细节。 如方法名称和参数值。

四、测试类:

public class Test {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("aopApplicationContext.xml");
        ICalculator calculator = (ICalculator) ctx.getBean("iCalculator");
        int result = calculator.add(3,2);
        System.out.println(result);
        result = calculator.sub(3,2);
        System.out.println(result);
        result = calculator.mul(3,2);
        System.out.println(result);
        result = calculator.div(6,3);
        System.out.println(result);

    }
}

结果如下:

The method add begins with [3, 2]
5
The method sub begins with [3, 2]
1
The method mul begins with [3, 2]
6
The method div begins with [6, 3]
2

利用通配符编写 AspectJ 切入点表达式

*用来匹配任意数量的字符。
+用来匹配制定类及其子类。
..一般用于匹配任意数的子包或参数。

  来看下面几个示例:
  
  execution(* com.spring.aop.ICalculator.*(..)): 匹配 ICalculator中声明的所有方法。第一个 * 代表任意修饰符及任意返回值。 第二个 * 代表任意方法。 .. 匹配任意数量的参数。 若目标类与接口与该切面在同一个包中,可以省略包名。
  execution(public * com.spring.aop.ICalculator.*(..)): 匹配 ICalculator接口的所有公有方法。
  execution(public double.ICalculator.*(..)): 匹配 ICalculator中返回 double 类型数值的共有方法。
  execution(public double ICalculator.*(double, ..)): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数。
  execution(public double ICalculator.*(double, double)):匹配参数类型为 double, double 类型的方法。

合并切入点表达式

  在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来。
  例如:切点表达式只匹配 ICalculator中声明的add()方法或者sub()方法。可将代码修改如下:
  

   @Before("execution(* com.spring.aop.ICalculator.add(..))||execution(* com.spring.aop.ICalculator.sub(..))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();

        System.out.println("The method " + methodName + " begins ");
    }

前置通知

  前置通知:在方法执行之前执行的通知。
  前置通知使用 @Before 注解, 并将切入点表达式的值作为注解值。

后置通知

  后置通知是在连接点完成之后执行的。 即连接点返回结果或者抛出异常的时候,。一个切面可以包括一个或者多个通知。
  例如:在LoggingAspect类中添加一个后置通知,用来记录方法的结束。
  

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.spring.aop.ICalculator.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }

    @After("execution(* com.spring.aop.ICalculator.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }

}

运行结果如下:

The method add begins with [3, 2]
The method add ends
5
The method sub begins with [3, 2]
The method sub ends
1
The method mul begins with [3, 2]
The method mul ends
6
The method div begins with [6, 3]
The method div ends
2

返回通知

  无论连接点是正常返回还是抛出异常,后置通知都会执行。 如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
  在返回通知中,只要将 returning 属性添加到 @AfterReturning 注解中,就可以访问连接点的返回值,该属性的值即为用来传入返回值的参数名称。
  必须在通知方法的签名中添加一个同名参数。 在运行时, Spring AOP 会通过这个参数传递返回值。
  原始的切点表达式需要出现在 pointcut 属性中。

    @AfterReturning(pointcut = "execution(* com.spring.aop.ICalculator.div(..))", returning = "result")
    public void LogAfterReturn(JoinPoint joinPoint, int result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends with" +  result);
    }

异常通知

  异常通知:只在连接点抛出异常时才执行异常通知。
  将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常。Throwable 是所有错误和异常类的超类。所以在异常通知方法可以捕获到任何错误和异常。
  如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。

    @AfterThrowing(pointcut = "execution(* com.spring.aop.ICalculator.div(..))", throwing = "e")
    public void LogAfterReturn(JoinPoint joinPoint, Exception e){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " throwing with" +  e);
    }

环绕通知

  环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点, 甚至可以控制是否执行连接点
  对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint 。 它是 JoinPoint 的子接口,允许控制何时执行, 是否执行连接点。
  在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行。
  注意:环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed()的返回值,否则会出现空指针异常。

 @Around("execution(* com.spring.aop.ICalculator.div(..))")
    public int LogAround(ProceedingJoinPoint joinPoint) throws Throwable{
        String methodName = joinPoint.getSignature().getName();
        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
         try{
             int  result ;
             result = (int) joinPoint.proceed();
             System.out.println("The method " + methodName + " ends with " + result);
             return result;
         } catch (Throwable throwable) {
             System.out.println("The method " + methodName + " throwing with" +  throwable);
             throw throwable;
         }
    }

重用切入点定义

  在编写 AspectJ 切面时, 可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
  在 AspectJ 切面中,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。其他通知可以通过方法名称引入该切入点切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的。
  切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面*用, 最好将它们集中在一个公共的类中。 在这种情况下,它们必须被声明为 public, 在引入这个切入点时,必须将类名也包括在内。 如果类没有与这个切面放在同一个包中, 还必须包含包名。
  重用切入点定义示例代码:
  

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.spring.aop.ICalculator.*(..))")
    private void Log(){

    }

    @Before("Log()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }

    @After("Log()")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }
 }

指定切面的优先级

  在同一个连接点上应用不止一个切面时,除非明确指定, 否则它们的优先级是不确定的。
  切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定。
  实现 Ordered 接口, getOrder() 方法的返回值越小,优先级越高
  

@Aspect
@Component
@Order(0)
public class LoggingAspect {

六、基于 XML 的方式配置AOP

  除了使用 AspectJ 注解声明切面,Spring 也支持在 Bean 配置文件中声明切面。这种声明是通过 aop schema 中的 XML 元素完成的。
  正常情况下,基于注解的声明要优先于基于 XML 的声明。通过 AspectJ 注解,切面可以与 AspectJ 兼容, 而基于 XML 的配置则是 Spring 专有的。由于 AspectJ 得到越来越多的 AOP 框架支持, 所以以注解风格编写的切面将会有更多重用的机会

基于 XML —— 声明切面

  当使用 XML 声明切面时, 需要在 < beans > 根元素中导入 aop Schema。
  在 Bean 配置文件中, 所有的 Spring AOP 配置都必须定义在 <aop:config>元素内部。对于每个切面而言, 都要创建一个 <aop:aspect> 元素来为具体的切面实现引用后端 Bean 实例。切面 Bean 必须有一个标示符, 供 <aop:aspect> 元素引用。
  

基于 XML —— 声明切入点

  切入点使用 <aop:pointcut>元素声明。切入点必须定义在<aop:aspect>元素下, 或者直接定义在<aop:config>元素下.定义在 <aop:aspect> 元素下: 只对当前切面有效。定义在 <aop:config> 元素下: 对所有切面都有效。
  基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点.。

基于 XML —— 声明通知

  在 aop Schema 中, 每种通知类型都对应一个特定的 XML 元素。
  通知元素需要使用 <pointcut-ref>来引用切入点, 或用 <pointcut> 直接嵌入切入点表达式.,method 属性指定切面类中通知方法的名称。
  
基于 XML 的方式配置AOP示例代码如下:

<?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.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置bean-->
    <bean id="calculator" class="com.spring.aop.CalculatorImpl"></bean>

    <!--配置切面的bean-->
    <bean id="loggingAspect" class="com.spring.aop.LoggingAspect"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切点表达式-->
        <aop:pointcut id="pointcut"
                      expression="execution(* com.spring.aop.ICalculator.*(..))"/>
        <!--配置切面及通知-->
        <aop:aspect ref="loggingAspect" order="0">
            <!--前置通知-->
            <!--<aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before>-->
            <!--后置通知-->
            <!--<aop:after method="afterMethod" pointcut-ref="pointcut"></aop:after>-->
            <!--返回通知-->
            <!--<aop:after-returning method="LogAfterReturn" pointcut-ref="pointcut" returning="result"></aop:after-returning>-->
            <!--异常通知-->
            <!--<aop:after-throwing method="LogAfterThrowing" pointcut-ref="pointcut" throwing="e"></aop:after-throwing>-->
            <!--环绕通知-->
            <aop:around method="LogAround" pointcut-ref="pointcut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

切面类示例代码如下:

public class LoggingAspect {


    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }

    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }

    public void LogAfterReturn(JoinPoint joinPoint, int result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends with " +  result);
    }

    public void LogAfterThrowing(JoinPoint joinPoint, Exception e){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " throwing with" +  e);
    }


    public int LogAround(ProceedingJoinPoint joinPoint) throws Throwable{
        String methodName = joinPoint.getSignature().getName();
        Object [] args = joinPoint.getArgs();

        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
         try{
             int  result ;
             result = (int) joinPoint.proceed();
             System.out.println("The method " + methodName + " ends with " + result);
             return result;
         } catch (Throwable throwable) {
             System.out.println("The method " + methodName + " throwing with" +  throwable);
             throw throwable;
         }
    }
}

七、一些需要了解的注解表达式

Spring框架(三)——AOP