Spring (六) ---------- AspectJ 对 AOP 的实现


前言

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。


一、AspectJ 简介

AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。

官网地址:http://www.eclipse.org/aspectj/

AspetJ 是 Eclipse 的开源项目,官网介绍如下:

在这里插入图片描述

二、AspectJ 的通知类型

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知

三、AspectJ 的 切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是

execution(modifiers-pattern? ret-type-pattern 
declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)

解释:

  • modifiers-pattern 访问权限类型
  • ret-type-pattern 返回值类型
  • declaring-type-pattern 包名类名
  • name-pattern(param-pattern) 方法名(参数类型和参数个数)
  • throws-pattern 抛出异常类型
  • ?表示可选的部分

execution(访问权限 方法返回值 方法声明(参数) 异常类型)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中访问权限、可省略,各部分间用空格分开。在其中可以使用以下符号:

在这里插入图片描述
举例:

execution(public * *(..))

指定切入点为:任意公共方法

execution(* set*(..))

指定切入点为:任何一个以 "set"开始的方法

execution(* com.xyz.service.*.(..))

指定切入点为:定义在 service 包里的任意类的任意方法

execution(* com.xyz.service..*.*(..))

指定切入点为:定义在 service 包或者子包里的任意类的任意方法。" … “出现在类名中时,后面必须跟” * ",表示包、子包下的所有类。

excution(* *..service.*.*(..)) 

指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

四、AspectJ 开发环境

(1) 添加Maven 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.15</version>
</dependency>

(2) 引入 AOP 约束

在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的。AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式。

其相关约束文件,idea会在使用时自动进行导入。

五、AspectJ 基于注解的 AOP 实现

实现步骤

(1) 定义业务接口与实现类

public interface SomeService {
    void doSome(String name, int age);
}
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, int age) {
        System.out.println("执行了业务方法doSome");
    }
}

(2) 定义切面类

类中定义了若干普通方法,将作为不同的通知方法,用来增强功能。

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

// @Aspect : 是aspect框架的注解, 表示当前类是切面类
@Aspect
public class MyAspect {

    //@Before : 前置通知
    //属性 : value 切入点表达式, 表示切面执行的位置
    //位置 : 方法定义的上面
    @Before(value="execution(* com.fancy.SomeServiceImpl.doSome(..))")
    public void myBefore() {
        //就是切面代码的功能, 例如日志输出, 事务的处理等
        System.out.println("前置通知: 在目标方法之前先执行, 例如输出日志");
    }
}

(3) 声明目标对象和切面类对象

<!--声明目标类对象-->
<bean id="someServiceTarget" class="com.fancy.SomeServiceImpl"></bean>
<!--声明切面类对象-->
<bean id="myAspect" class="com.fancy.MyAspect"></bean>

(4) 注册 AspectJ 的自动代理

在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成 "目标类+ 切面” 的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。

<!--声明自动代理生成器-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

在这里插入图片描述
<aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。

从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。

其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

(5) 测试类中使用目标对象的 id

public class test {
    @Test
    public void test01() {
        String resource = "applicationContext.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(resource);
        //从spring容器中获取目标对象, 目标就是经过aspectj修改过后的代理对象
        SomeService proxy = (SomeService) ctx.getBean("someServiceTarget");
        //通过代理执行业务方法, 实现功能增强
        proxy.doSome("hh", 20);
    }
}

在这里插入图片描述

@Before 前置通知,方法有 JoinPoint 参数

在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。

//    通知方法: 使用了通知注解修饰的方法
//    通知方法可以有参数, 但是参数不是任意
//    1. JoinPoint表示连接点的方法        
@Before(value="execution(* com.fancy.SomeServiceImpl.doSome(..))")
public void myBefore(JoinPoint jp) {
	//JoinPoint 能够获取到方法的定义, 方法的参数信息等
	System.out.println("连接点的方法定义: " + jp.getSignature());
	System.out.println("连接点方法的参数个数: " + jp.getArgs().length);
	
	//方法参数信息
	Object args[] =jp.getArgs();
	
	for (Object arg : args) {
	    System.out.println(arg);
	}
	System.out.println("前置通知: 在目标方法之前先执行, 例如输出日志");
}

在这里插入图片描述

@AfterReturning 后置通知 - 注解有 returning 属性

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

接口增加方法:

public interface SomeService {
    void doSome(String name, int age);
    String doOther(String name, String age);
}

实现方法:

@Override
public String doOther(String name, String age) {
    System.out.println("执行了业务方法doOther");
    return "abc";
}

定义切面

@AfterReturning(value = "execution( * *..SomeServiceImpl.doOther(..))", returning = "result")
public void myAfterReturning(Object result) {
    if (result != null) {
        String s = (String) result;
        result = s.toUpperCase();
    }
    System.out.println("后置通知 : 在目标方法之后执行的功能增强, 例如执行事务处理(切面)" + result);
}

测试结果
在这里插入图片描述

@Around 环绕通知-增强方法有 ProceedingJoinPoint参数

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

接口增加方法:

public interface SomeService {
    void doSome(String name, int age);
    String doOther(String name, int age);
    String doFirst(String name, int age);
}

接口方法的实现:

@Override
public String doFirst(String name, int age) {
     System.out.println("执行了业务方法doFirst");
     return "doFirst";
}

定义切面:

@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
     Object obj = null;
     System.out.println("环绕通知: 在目标方法之前执行的, 例如输出日历");
     // 执行目标方法的调用, 等同于 method.invoke(target, args)
     obj = pjp.proceed();
     System.out.println("环绕通知: 在目标方法之后执行的,例如处理事物");
     return obj;
}

测试结果:
在这里插入图片描述

@AfterThrowing 异常通知-注解中有 throwing 属性

在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。

增加接口方法:

public interface SomeService {
    void doSome(String name, int age);
    String doOther(String name, int age);
    String doFirst(String name, int age);
    void doSecond(String name, int age);
}

方法实现:

@Override
public void doSecond(String name, int age) {
    System.out.println("执行了业务方法doSecond" + (10/0));
}

定义切面:

@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
public void myAfterThrowing(Throwable ex) {
    //把异常发生的时间, 位置, 原因记录到数据库, 日志文件等等等
    //可以在异常发生时, 把异常信息通过短信, 邮件发送给开发人员

    System.out.println("异常通知: 在目标方法抛出异常时执行的, 异常原因:" + ex.getMessage());
}

测试结果:
在这里插入图片描述

@After 最终通知

无论目标方法是否抛出异常,该增强均会被执行。

增加方法:

public interface SomeService {
    void doSome(String name, int age);
    String doOther(String name, int age);
    String doFirst(String name, int age);
    void doSecond(String name, int age);
    void doThird(String name, int age);
}

实现方法:

@Override
public void doThird(String name, int age) {
    System.out.println("执行了业务方法doThird" + (10/0));
}

定义切面:

@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter() {
    System.out.println("最终通知:总是会被执行的方法");
}

在这里插入图片描述

@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。

其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。

代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在森林中麋了鹿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值