AOP
简介OOP(Object Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并不适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
而 AOP(Aspect Oriented Programming)面向切面编程,则作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑(比如日志、事务等),抽取并封装为一个可重用的模块,这个模块被命名为切面(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
为了深入理解 AOP,我们需要理解一个叫横切关注点(cross-cutting concern)的概念,它描述的是我们应用中的功能特点,即假如有一个功能,它在应用程序中很多个地方都用到了,那么我们把这样的功能称之为横切关注点。
日常开发中,我们都会将不同的业务场景抽象出对应的模块进行开发,而不同的模块,除了那些针对特定领域的核心功能外,还有一些相同的辅助功能,比如日志管理、安全管理、事务管理等等。横切关注点这个概念其实就点明了:类似这样的功能就是我们面向切面编程需要关注的地方。这也是面向切面编程的意义所在:它帮助我们实现横切关注点和他们所影响的对象之间的解耦。
面向切面编程的实质,就是将横切关注·点模块化成被称为切面的特殊的类。
SpringAOP
简介AOP 实现的关键在于代理模式,AOP 代理主要分为静态代理和动态代理。静态代理的代表为 AspectJ
,动态代理则以 Spring AOP
为代表:
Spring AOP 中的动态代理主要有两种方式:JDK
动态代理和 CGLIB
动态代理:
InvocationHandler
接口和 Proxy
类,InvocationHandler 通过 invoke()
方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy 利用 InvocationHandler 动态创建一个符合某一接口的的实例,生成目标类的代理对象。final
,那么它是无法使用 CGLIB 做动态代理的。静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译器进行处理,而 Spring AOP 则无需特定的编译器处理。
SpringAOP
相关术语Aspect
)切面就是被抽取的公共模块。可能会横切多个对象。 在 Spring AOP 中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @AspectJ
注解来实现。
Join point
)连接点就是程序中我们需要应用通知的地方。这个点可以是我们调用方法时、抛出异常时或甚至是修改某一个字段的时候。切面代码可以通过这些点插入到应用的正常流程中,使原本的功能增添新的行为。
在 Spring AOP 中,一个连接点总是代表一个方法。
Advice
)切面的工作被称为通知。也就是定义了切面的要做什么,以及什么时候做的问题。Spring 切面有5种类型的通知,相应的在 SpringBoot 中有对应的五个注解:
@Before
。@After
。@AfterReturning
。@AfterThrowing
。Nanning
和 JBoss4
,都只提供环绕通知;对应 SpringBoot 的注解为 @Around
。注意: 这里有两个重要的区别需要留意:
同一个切面 Aspect,不同通知 Advice 的执行顺序:
Pointcut
)我们的应用程序可能会有很多个连接点需要我们应用通知,所以我们有必要把连接点进行分类汇总,抽象出相同的特点,好让正确的切面切入到正确的地方去,各司其职,而不是切入所有的连接点。切点定义了一个切面需要在哪里进行切入,是一堆具有特定切面切入需求的连接点的共性抽象。
我们通常通过明确类和方法名、或者匹配正则表达式的方式来指定切点。
Target Object
)被一个或者多个切面(Aspect)所通知(Advise)的对象。也可以把它叫做被通知对象。 既然 Spring AOP 是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied) 对象。
Introduction
)引入也被称为内部类型声明(inter-type declaration),声明额外的方法或者某个类型的字段。Spring 允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使 bean 实现 IsModified 接口,以便简化缓存机制。
引入能够让我们在不修改原有类的代码的基础上,添加新的方法或属性。
Weaving
)织入是指把切面应用到目标对象并创建新的代理对象的过程。
SpringAOP
实战我们以这样一个场景为例:打印服务的开始和结束信息,并输出服务处理的时长。
我们以 SpringBoot 为例,SpringBoot 引入 AOP 依赖后,spring.aop.auto
属性默认是开启的,也就是说只要引入了 AOP 依赖后,默认已经增加了 @EnableAspectJAutoProxy
注解。
1)引入依赖
首先在项目中引入 SpringAOP 的依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
2)创建注解
创建一个自定义注解 @MyLog
,用 boolean 类型的字段来决定是否开启日志输入功能,代码如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
boolean isOn() default true;
}
3)定义切面
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
private void onePointcut() { }
@Around("onePointcut()")
private Object around(ProceedingJoinPoint joinPoint) throws Throwable{
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的注解,根据参数判断是否开启AOP
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog == null || !myLog.isOn()) {
System.out.println("MyLog is off, stop working aop.");
return new Object();
}
System.out.println("MyLog is on.");
// 获取目标方法参数信息
Object[] args = joinPoint.getArgs();
String serviceName = (String)args[0];
System.out.println("--- " + serviceName + " start ---");
// 计算方法花费时长
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("--- " + serviceName + " end,耗时: " + (end - start) + "ms ---");
return result;
}
}
代码中,我们设置了一个切点:
@Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
private void onePointcut() { }
@Pointcut
注解表示声明一个切点,里面需要配置切点指示符,这里我们使用 execution
指示器,它是一个用来匹配连接点为方法的切点指示器,匹配规则解析如下:
*
号表示不关心方法的返回值类型;com.example.demo.controller.AspectController.*
为目标方法的特点;*
号表示匹配 AspectController 类下的任意方法;(..)
表示不关心参数个数和类型。想了解更多关于切点指示符的话,可以参考我的另一篇博客:【Spring】之 SpringAOP 指示符详解
4)编写测试接口
@RestController
@RequestMapping("/aop")
public class AspectController {
@GetMapping("/{name}")
@MyLog(isOn = true)
public void testMyLog(@PathVariable("name") String serviceName) throws InterruptedException {
System.out.println("This is a controller for testing MyLog AOP!");
// 模拟业务耗时
Random random = new Random();
Thread.sleep(random.nextInt(100));
}
}
测试结果如下:
MyLog is on.
--- 业务1 start ---
This is a controller for testing MyLog AOP!
--- 业务1 end,耗时: 76ms ---
上面的实战例子简单介绍了如何使用 AOP,接下来我们完善所有通知类型,分析各种通知的执行顺序,包括没有发生异常时执行顺序和发生异常后的执行顺序。
代码如下:
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
private void onePointcut() { }
@Before("onePointcut()")
private void before() {
System.out.println("--- before");
}
@After("onePointcut()")
private void after() {
System.out.println("--- after");
}
@AfterReturning("onePointcut()")
private void afterReturning() {
System.out.println("--- afterReturning");
}
@AfterThrowing("onePointcut()")
private void afterThrowing() {
System.out.println("--- afterThrowing");
}
@Around("onePointcut()")
private Object around(ProceedingJoinPoint joinPoint) throws Throwable{
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的注解,根据参数判断是否开启AOP
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog == null || !myLog.isOn()) {
System.out.println("MyLog is off, stop working aop.");
return new Object();
}
System.out.println("MyLog is on.");
// 获取目标方法参数信息
Object[] args = joinPoint.getArgs();
String serviceName = (String)args[0];
System.out.println("--- " + serviceName + " start ---");
// 计算方法花费时长
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("--- " + serviceName + " end,耗时: " + (end - start) + "ms ---");
return result;
}
}
然后修改测试接口,增加业务名称的一个长度判断,如果名称太长,抛出异常,来模拟方法执行过程中发生异常:
@RestController
@RequestMapping("/aop")
public class AspectController {
@GetMapping("/{name}")
@MyLog(isOn = true)
public void testMyLog(@PathVariable("name") String serviceName) throws InterruptedException {
System.out.println("This is a controller for testing MyLog AOP!");
if (serviceName.length() > 5) {
throw new RuntimeException("名称过长!");
}
Random random = new Random();
Thread.sleep(random.nextInt(100));
}
}
1)正常执行的结果如下:
MyLog is on.
--- 业务1 start ---
--- before
This is a controller for testing MyLog AOP!
--- afterReturning
--- after
--- 业务1 end,耗时: 103ms ---
从中可以看到正常执行的结果顺序如下:
注意:在环绕通知中的
joinPoint.proceed();
代码表示调用目标方法,在它之前就是around before advice
,之后的就是around after advice
。
2)发生异常的结果如下:
MyLog is on.
--- 这时长业务名称 start ---
--- before
This is a controller for testing MyLog AOP!
--- afterThrowing
--- after
java.lang.RuntimeException: 名称过长!
从中可以看到发生异常后的执行顺序如下:
并且,当发生异常后,先执行异常通知,不执行返回通知,并且后置通知不管什么情况都会执行。