Spring 核心 之 AOP

什么是 AOP

AOP(Aspect-Oriented Programming):面向切面编程,是对传统的面向对象编程的补充。
什么意思呢?
在这里插入图片描述
比如上图中,在不同的方法中,有许多相同的功能代码,那我们就可以把这些相同的功能代码抽取出来,放到类中,那么这个类就被叫做切面。

实际上,AOP 的原理就是利用了动态代理,当我们需要调用目标对象的时候,Spring 就会帮我们生成一个代理对象,将切面和核心的业务逻辑代码组装起来,形成完整的模块。即使我们将代码抽离出来,也并不会影响我们的正常使用。

这样做的好处是:

  1. 业务模块更简洁,只包含核心业务代码,在我们编码的时候就可以更加专注于核心代码的编写上。其次在维护、调试的时候也更加容易定位问题的所在。
  2. 当我们需要修改公共功能的代码时,只需修改公共功能所在的切面即可,不必再去一个一个的方法中修改。

注解配置

对于面向切面编程,我们可以使用 ASpectJ 框架,AspectJ 是 Java 社区里最完整最流行的 AOP 框架。

准备工作

因此我们想要使用 AscpectJ 框架,就必须要先导入依赖的 jar 包:

  • aopalliance.jar
  • aspectj.weaver.jar
  • spring-aspects.jar

其次我们还需要将 aop Schema 命名空间添加到配置文件中。

基于注解的 AOP

接下来我们就可以使用注解实现 AOP 了。

面向切面编程,那么首先我们得有切面。上文说到切面就是一个类,那难道说我们创建了一个类,这个类就是一个切面吗?Spring 如何识别这是一个切面呢?

我们可以使用 @Aspect 注解,只要在对应的类上标注这个注解,那么此类就是一个切面。

切面中是一个一个的通知,一个切面中可以有多个通知,通知就是切面要完成的工作,在我们想要调用业务方法时,会将这些通知加入到业务方法中的某个位置,比如方法前、后等,从而形成一个完整的业务功能。通知在代码中的体现就是加了某种注解的 Java 方法。

AspectJ 一共支持 5 种类型的通知,它们对应的注解分别是:

  • @Before:前置通知,在方法执行之前执行。
  • @After:后置通知,在方法执行之后执行,无论方法中是否有异常都会执行。
  • @AfterRunning:返回通知,在方法返回结果之后执行。
  • @AfterThrowing:异常通知,在方法抛出异常之后执行。
  • @Around:环绕通知,需要我们自己在方法中控制。

前置通知

(1)

@Before
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));
}

(2)

配置了通知之后,还缺一样东西,把这些通知应用到哪些方法上呢,我们要告诉 Spring,所以我们还要配置切入点表达式。
例如:

@Before("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")

以下是一些示例:

表达式含义
execution(* com.atguigu.spring.ArithmeticCalculator.*(…))ArithmeticCalculator 接口中声明的所有方法。第一个“*”代表任意修饰符及任意返回值。第二个“*”代表任意方法。“…”匹配任意数量、任意类型的参数。若目标类、接口与该切面类在同一个包中可以省略包名。
execution(public * ArithmeticCalculator.*(…))ArithmeticCalculator 接口的所有公有方法。
execution(public double ArithmeticCalculator.*(…))ArithmeticCalculator 接口中返回double类型数值的方法。
execution(public double ArithmeticCalculator.*(double, …))第一个参数为double类型的方法。“…” 匹配任意数量、任意类型的参数。
execution(public double ArithmeticCalculator.*(double, double))参数类型为 double,double 类型的方法。
execution (* *.add(int,…)) || execution(* *.sub(int,…))切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

(3)

除此之外,要想让这些注解起作用,还需要在配置文件中配置一样东西。在 Spring IOC 容器中启用 AspectJ 注解支持,需要在配置文件中定义一个空的 XML 元素:

<!-- 配置自动为匹配 aspectJ 注解的 Java 类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

当 Spring IOC 容器侦测到 bean 配置文件中的 <aop:aspectj-autoproxy> 元素时,会自动为与 AspectJ 切面匹配的 bean 创建代理。

JoinPoint 类
通过 JoinPoint 类我们就可以访问一些链接细节,比如当前方法的名称、参数等。
在这里插入图片描述

后置通知

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

返回通知

在返回通知中,可以访问到方法的返回值,只需要将 returning 属性加入到 @AfterReturning 注解中,返回值就会传给 returning 属性对应的值。此外还要在方法中添加一个同名参数。

@AfterReturning(value="declareJointPointExpression()",returning="result")
public void afterReturning(JoinPoint joinPoint, Object result){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("The method " + methodName + " ends with " + result);
}

异常通知

异常通知中,可以定义发生何种异常时,才执行异常通知,并且可以访问到异常对象。和返回通知类似,我们需要在注解中添加 throwing 属性,以及在方法中添加一个和 throwing 属性值同名的参数,此参数即指定了发生何种异常执行此通知。

/**
  * 在目标方法出现异常时会执行的代码.
  * 可以访问到异常对象;,且可以指定在出现特定异常时再执行通知代码
  */
@AfterThrowing(value="declareJointPointExpression()",
throwing="e")
public void afterThrowing(JoinPoint joinPoint, Exception e){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("The method " + methodName + " occurs excetion:" + e);
}

环绕通知

环绕通知类似于动态代理的全过程,需要我们手动控制在何时(方法前?后?)执行什么代码。

与上述通知不同的是:环绕通知的连接点参数类型必须是 ProceedingJoinPoint,它是 JoinPoint 的子接口,如果想要执行被代理的方法,必须调用 ProceedingJoinPointproceed() 方法。此外,环绕通知还必须有返回值,返回值即为目标方法的返回值,即 ProceedingJoinPoint.proceed() 方法的返回值。

/**
   * 环绕通知需要携带 ProceedingJoinPoint 类型的参数. 
   * 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
   * 且环绕通知必须有返回值, 返回值即为目标方法的返回值
*/
@Around("execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){

    Object result = null;
    String methodName = pjd.getSignature().getName();

    try {
        //前置通知
        System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
        //执行目标方法
        result = pjd.proceed();
        //返回通知
        System.out.println("The method " + methodName + " ends with " + result);
    } catch (Throwable e) {
        //异常通知
        System.out.println("The method " + methodName + " occurs exception:" + e);
        throw new RuntimeException(e);
    }
    //后置通知
    System.out.println("The method " + methodName + " ends");

    return result;
}

切入点可重用

如果我们在每一个注解的后面都指定切入点表达式,则非常麻烦,如果修改还需要一个一个修改。因此我们可不可以将切入点表达式抽离出来呢?

答案是:可以的。我们可以使用 @Pointcut 注解来配置统一的切入点表达式,我们只需要在一个方法上方用 @Pointcut 注解标注,其他的注解直接引用该方法名即可。

切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。

/**
  * 定义一个方法, 用于声明切入点表达式。 一般地,该方法中再不需要添入其他的代码。
  * 使用 @Pointcut 来声明切入点表达式。
  * 后面的其他通知直接使用方法名来引用当前的切入点表达式。
  */
@Pointcut("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
public void declareJointPointExpression(){

}

@Before("declareJointPointExpression()")

配置切面的优先级

如果我们有好几个切面,Spring 就不知道谁改先执行,谁该后执行。不过,我们可以明确指定它们之间的执行顺序,切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定。

实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。@Order 注解也类似,数值越小,优先级越高。

@Aspect
@Order(1)
public class LoggingAspect {

}

XML 配置

切面除了支持注解配置,还支持使用配置文件的方式来配置。不过正常情况下,基于注解的声明要优先于基于 XML 的声明。

(1)
和注解配置类似,首先我们也要配置一个切面。切面所在的类要先实例化。

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

<!-- 配置 AOP -->
<aop:config>
    <!-- 配置切面 -->
    <aop:aspect ref="loggingAspect" order="2">

    </aop:aspect>	
</aop:config>

(2)
第二步我们要配置切入点表达式。使用 <aop:pointcut> 标签,如果配置在 <aop:config> 标签下,则所有的切面都可使用,如果配置在 <aop:aspect> 标签下,则只能在此切面中使用。

<!-- 配置 AOP -->
<aop:config>
    <!-- 配置切点表达式 -->
    <aop:pointcut id="pointcut" expression="execution(*com.spring.aop.xml.ArithmeticCalculator.*(int, int))" />
</aop:config>

(3)
第三步就是配置各个通知了,每种通知都对应这不同的 aop 标签,在通知中可以使用 pointcut 属性来单独配置切入点表达式,也可以使用 pointcut-ref 属性来引用已经配置好的切入点表达式。

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

<bean id="vlidationAspect" class="com.atguigu.spring.aop.xml.VlidationAspect"></bean>

<!-- 配置 AOP -->
<aop:config>
    <!-- 配置切点表达式 -->
    <aop:pointcut id="pointcut" expression="execution(* com.spring.aop.xml.ArithmeticCalculator.*(int, int))" />
    
    <!-- 配置切面及通知 -->
    <aop:aspect ref="loggingAspect" order="2">
        <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
        <aop:after method="afterMethod" pointcut-ref="pointcut"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
        <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
        <!--  
            <aop:around method="aroundMethod" pointcut-ref="pointcut"/>
        -->
    </aop:aspect>	
    
    <aop:aspect ref="vlidationAspect" order="1">
        <aop:before method="validateArgs" pointcut-ref="pointcut"/>
    </aop:aspect>
</aop:config>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值