Spring AOP使用教程

AOP 简介

AOP 思想是Spring的核心设计思想之一,通过基于切面的编程设计理念可以将业务逻辑与系统逻辑有效的分隔开来。使得系统的架构更加清晰,模块之间的界限也变的更加明确。

AOP 全称为 Aspect Oriented Programming,即面向切面的编程。这种编程方式可以为某些方法提供行为增强,亦或者是行为监控能力。通过对切片统一编程可以在相比于不使用AOP的情况下减少了重复代码的开发量,同时使得代码功能更加明确。

从整体上来看 AOP的加持使得相对应的方法执行时看起来就好像奥利奥饼干一样,黑白分明。
在这里插入图片描述

如果说之前的编程是二维的话,AOP模式的编程可以理解为2.5维。直观的感觉就是叠了两层,在不同层上编写不同逻辑的代码。如下图,我编写的日志服务权限服务就是基于 用户服务订单服务等业务逻辑上运行的
在这里插入图片描述

Spring AOP 使用

我们在创建了 Spring 应用后,是无法直接使用 Spring 的 AOP服务的,需要添加两个依赖包:

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>

其中 aspectjrt 中提供了我们编写切片过程中所需要的 @Pointcut,@After,@Aspect,@Before,@Around等注解。

aspectjweaver则提供了 AOP 切片执行的主逻辑。

在添加好依赖后,正式编写前,我们还需要熟悉一些 AOP中的概念:

  • 切面(Aspect):一般添加在类上,用于声明为一个AOP逻辑
  • 切入点(Ppointcut):添加在AOP逻辑中,一般为一个表达式,用来标记被AOP的目标方法
  • 通知(Advice):表示在AOP所标记的目标方法前后执行的AOP增强逻辑,按照类型可以分为:
    • 在目标方法前执行@Before
    • 在目标方法后执行@After
    • 在目标方法前后执行@Around
    • 在目标方法返回执行 @AfterReturning
    • 在目标方法异常执行 @AfterThrowing

上述三个概念就是完成一个 AOP 逻辑编程所需要的基本概念了。
其中切入点(@Pointcut)+通知(@Advice) = 切面(@Aspect)。
在这里插入图片描述

除此之外,还有一些进阶概念,这里简单介绍一下:

  • 连接点(Jointpoint):AOP 将目标方法抽象为连接点的概念提供给增强方法去处理,比如下面代码:
@Before("execution(* com.example.demo07.controller.*.*(..))")
public void before(JoinPoint joinPoint){
    log.info("------------"+Arrays.toString(joinPoint.getArgs()));
}

这里编写了一个增强方法,该方法会在 com.example.demo07.controller包中的所有类对应的方法执行前执行,@Before 作为一个通知标识,表示下面的before()方法会在切入点 execution(* com.example.demo07.controller.*.*(..)) 之前执行。

而下面的增强方法可以通过 入参 JoinPoint(连接点) 获取到目标方法的上下文信息,包括方法名称,方法入参等。

还有一些其他概念是在 AOP 运行过程中出现的,它们和 AOP 机制的运转原理有关:

  • 介绍(Introduction) Introduction可以为原有对象增加新的属性和方法,例如,你可以使用 introduction 使 bean 实现 isModified 接口,以简化缓存
  • 目标对象(Target Object)由一个或者多个切面代理的对象。也被称为"切面对象"。有 Spring AOP 使用运行时代理实现的,因此该对象始终是代理对象。
  • AOP代理(AOP Proxy) 有 AOP 框架创建的对象,在 Spring 框架中,AOP代理对象有两种 :JDK动态代理(基于接口的代理)和 cglib 代理(非接口代理)
  • 织入(Weaving) 是指吧增强应用到目标对象来创建新的代理对象的过程,它(例如 AspectJ 编译器)可以在编译时期,加载时期或者运行时期完成。与其他纯 Java AOP框架一样,Spring AOP 在运行时进行织入。

接下来我们开始编写一个切面(Aspect),在添加好依赖之后,首先我们需要开启对 AspectJ 的支持:

@SpringBootApplication
@EnableAspectJAutoProxy
public class Demo07Application {
...
}

在配置类上添加 @EnableAspectJAutoProxy 注解,表示对 AspectJ 的支持。接下来我们就可以编写一个切面了:

@Slf4j
@Aspect
@Component
public class DemoLogger {

    @Around("execution(* com.example.demo07.controller.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        final long start = System.currentTimeMillis();
        final Object proceed = joinPoint.proceed();
        log.info("{}() has cost {} ms", joinPoint.getSignature().getName(), System.currentTimeMillis() - start);
        return proceed;
    }
}

作为一个 AOP 切面,首先需要通过 @Component将当前类注册成 Spring 组件;其次需要通过 @Aspect 注解将其声明为 AOP切面。

下面我们就可以在类中编写增强方法了,这里我编写了一个简单的增强方法,用于记录方法执行的时间。方法around()上的@Around注解表示下面的方法是一个环绕Advice(增强方法),而里面的表达式则作为Pointcut切入点,用于指定目标方法。Spring 会根据该表达式扫描IOC容器中符合特征的组件,并通过 AspectJ 实现方法增强。

这里我使用了 @Around注解实现了方法增强,此时我在这里传入了一个 ProceedingJoinPoint(连接点) 作为入参,Spring 会自动将增强方法的上下文封装成该格式传给我们,我们可以通过该参数获取到所有想要的目标方法的信息。

那么在上面代码执行后,当我请求 com.example.demo07.controller.MainController#index方法时,我会获取到如下日志:

[nio-7010-exec-2] com.example.demo07.filter.DemoFilter     : index() has cost 0 ms

切入点(Pointcut)表达式编写

Spring 提供了灵活多变的切点表达式以满足我们的业务场景:

AspectJ描述符描述
args()限制连接点匹配参数为指定类型的执行方法
@args()限制连接点匹配参数由指定注解标注的执行方法
execution()用于匹配是连接点的执行方法
this()限制连接点匹配的AOP代理的bean引用为指定类型的类
target限制连接点匹配目标对象为指定类型的类
@target()限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within()限制连接点匹配指定的类型
@within()限制连接点匹配指定注解所标注的类型
@annotationn限定匹配带有指定注解的连接点

@Around("execution(* com.example.demo07.controller.*.*(..))")为例,当使用了 execution 来描述符来声明切入点,这里表示执行在 com.example.controller包(不包含子包)中的任何Spring Component对应的方法都会作为切入点实现增强。
在这里插入图片描述

execution()表达式

其中execution表示执行表达式对应的方法作为切入点进行增强。表达式中支持使用通配符:
execution(java.lang.String com.example.demo07.controller.*.*(..))

execution(<return-type> <package>.<class-name>.<method-name>(<parameter-type>))
# * 表示任意类型或者任意方法名称,下面表达式表示返回任意方法类型
# controller 下的所有类,类中的所有方法
# .. 表示多参数,如下面表达式指方法的任何入参参数类型
execution(* com.example.demo07.controller.*.*(..))
# .. 还可以表示任意包,如下面表达式指controller 包及其子包下的任意类
# 此时 controller 包及其子包下的任意类就都会被扫到了
execution(* com.example.demo07.controller..*.*(..))
# -----以下表达式使用一般会报错
# 表达式会将匹配任意类中任意方法
execution(* *(..))
# 表达式会匹配任意类中方法传参为空
execution(* *())
args()表达式

args 表达式用于限制方法在调用时传入的参数类型,如:

execution(* com.example.controller..*.*(..)) && args(com.example.dto.User)

表示切面的执行条件为 com.example.controller 包下任何的 Spring 组件且调用方法入参有且只有一个com.example.dto.User 类型 的对象。

execution()args()表达式的区别

args 表达式可以限制连接点匹配参数为指定类型的执行方法,而 execution 表达式同样可以实现限制连接点匹配参数的指定类型,那么这两个表达式具体匹配的效果有什么区别呢?如下有两个表达式:

args(java.io.Serializable)
#--------------------
execution(* *(java.io.Serializable))

第一个args表达式表示对于任意方法,匹配方法第一个入参(唯一一个)实现了序列化接口。
第二个表达式表示的含义类似。

args 表达式匹配在方法执行期间传入目标方法的实际参数是否实现了序列化接口,而execution表达式则匹配方法定义的形参是否实现了序列化接口。

换言之,args表达式专注于那些在运行期间方法实际传入的参数是否实现了序列化接口,而execution则专注于方法的定义里面形参是否实现了序列化接口。

@Data
public class User implements Serializable{
	private String name;
}

@Service
public class DemoService{
	// 符合 args(java.io.Serializable) 表达式,但不限定与该方法
	public void method_1(Object obj){
		log.info("method_1's param is {}",obj)
	}

	// 符合 execution(* *(java.io.Serializable)) 表达式
	public void method_2(User user){
		log.info("method_2's param is {}",user);
	}
}

@Aspect
@Component
public class DemoAspect{
	
    @Pointcut("execution(* com.example..*.*(..))")
    public void cut1(){}

    @Before("cut1() && execution(* *(java.io.Serializable))")
    public void execution(JoinPoint joinPoint) throws Throwable {
        log.info("{} has executed",joinPoint.getSignature().getName());
    }

    @Before("cut1() && args(java.io.Serializable)")
    public void args(JoinPoint joinPoint) throws Throwable {
        log.info("{} has executed",joinPoint.getSignature().getName());
    }
}

@RestController
public class DemoController{

    @Autowired
    DemoService demoService;

    @GetMapping("test1")
    public void test1(){
        final UserDTO user = new UserDTO();
        user.setName("ghimi");
        demoService.method_1(user);
    }

    @GetMapping("test2")
    public void test2(){
        final UserDTO user = new UserDTO();
        user.setName("ghimi");
        demoService.method_2(user);
    }
}

对于上述示例,当我请求 test1() 时,切面 method_1被匹配并调用,当我请求 test2()时,切面method_2被匹配并调用。由此可见args匹配的是运行时期传入的参数,而 execution匹配的是方法定义的参数。

当两者同时使用时,优先匹配 execution 条件而args条件不生效。如上面的示例,当我请求 demoService.method_2()时,传入的参数其实是满足args(java.io.Serializable)表达式的,但由于优先匹配了 execution(* *(java.io.Serializable))表达式,因此 args(java.io.Serializable) 表达式便失效了。

在这里插入图片描述
由于 execution 匹配的是方法定义时的形参,而定义

execution(* *(..))execution(* *()) 分析

对于 execution(* *(..)) 表达式而言,意为在任何Spring Component的任何组件的任何方法上添加注解;execution(* *()) 类似,只是方法没有入参而已。这两种表达式本身并没有问题,关键在于他们指定的切点范围太广,使得Spring在加载完切面后,之后执行的启动流程中的任何方法都会作为切点去执行上述表达式。从而与 Spring 启动流程发生冲突,导致启动失败。
在这里插入图片描述

参考资料

AOP with Spring
Introduction of Spring AOP
Join Point Matching based on Annotations

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值