Spring 核心 - AOP 面向切面编程入门, 通俗易懂
撰写本文目的只有一个,让你畅快阅读 AOP 知识,并搞定以下几个问题。
- AOP 面向切面编程到底是什么?
- AOP 术语:连接点,切入点,通知,切面,织入是什么东西?
- @Aspect 注解为啥用的是 aspectj 包,不是 AOP 包?它们啥关系?
- Spring 框架是如何实现 AOP ? 代理技术。
不急解释 AOP 的概念, 我们先来看 AOP 解决了什么问题。
引言
在写项目的时,你是否遇到过随着系统体积增大,有一些新的需求——比如日志、用户鉴权、指标上报、redis 缓存填充/失效、事务等——需要对系统的多个类、方法改动,这种大量**人工改动**代码的行为容易遗漏、逻辑改动大,侵入风险高。可见,在这种场景下,手动改写代码必然是下下策。很明显本文讲的是 AOP , AOP 必然可以解决此类问题,做到不侵入原有代码新增日志、用户鉴权等功能。
- 举一个例子,让场景更形象化。
public class UserServiceImpl implements UserService {/*** find user list.** @return user list*/@Overridepublic List<User> findUserList() {System.out.println("执行方法: findUserList()");return Collections.singletonList(new User("fency", 18));}/*** add user*/@Overridepublic void addUser() {System.out.println("执行方法: addUser()");// do something}@Overridepublic void deleteUser() {System.out.println("执行方法: deleteUser()");}
}
要想对这三个方法加日志和鉴权,逐个添加是下策。如图:
下面有请主角登场。
AOP
介绍
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程。AOP 最早是 AOP 联盟的组织提出的,指定的一套规范,Spring 将 AOP 的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护的一种技术。
AOP 是设计思想,Spring AOP 、AspectJ 才是技术实现, AOP 思想本质的作用解耦。我们将记录日志、鉴权功能解耦为切面,进而引出 AOP 理念:将分散在各个业务逻辑代码中相同的代码,通过横向切割的方式抽取到一个独立的模块中。用图表示:
意思是写一次日志代码,即可让三个方法实现日志记录功能。
下面用 Spring AOP 实战演示统一加日志。
实战 - 统一添加日志
创建一个 spring boot 项目,引一个 lombok 依赖。1)引入 aop 依赖包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2)新建 UserServiceImpl 类, 我们要对它进行统一添加日志。实现 CommandLineRunner 接口的作用是:当Spring 启动完毕时,执行一次 CommandLineRunner.run() 方法, 模拟方法调用。
@Service
public class UserServiceImpl implements UserService {/*** find user list.** @return user list*/@Overridepublic List<User> findUserList() {System.out.println("执行方法: findUserList()");return Collections.singletonList(new User("fency", 18));}/*** add user*/@Overridepublic void addUser() {System.out.println("执行方法: addUser()");// do something}@Overridepublic void deleteUser() {System.out.println("执行方法: deleteUser()");}
}
单元测试执行
@SpringBootTest
class UserServiceImplTest {@Resourceprivate UserService userService;@Testvoid invokeMethod() {userService.addUser();userService.findUserList();userService.deleteUser();}
}
效果如图,没有日志。
3)添加 Aspect 切面类,统一为 UserService 的三个方法添加日志。
@Slf4j
@Aspect
@Component
public class LogAspect {/*** 切点:拦截所有 controller 包下的公共方法*/@Around("execution(* com.codebear.springboothelloword.service..*(..))")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().toShortString();Object[] args = joinPoint.getArgs();log.info("Entering {} with args: {}", methodName, args);try {Object result = joinPoint.proceed(); // 执行业务方法log.info("Exiting {} with result: {}", methodName, result);return result;} catch (Throwable ex) {log.error("Exception in {}: {}", methodName, ex.getMessage(), ex);throw ex;}}
}
再次执行,可以看到每个方法前后都有日志,解决了最初的痛点,避免大量手动修改代码。
初学 AOP 可能会觉得不会灵活,所有的方法都有日志了,如果只想两个方法有日志怎么办?
探索一下切点表达式,便有答案。拿示例的切点表达式来说:
// 第一个 * 代表匹配任意方法的返回类型
// service..* 代表匹配这个包下的所有类都是切点
// (..) 代表任意方法的参数类型、个数都匹配
@Around("execution(* com.codebear.springboothelloword.service..*(..))")
AOP 术语
开始念经:**连接点、切入点、通知、切面、引入、目标对象、织入、AOP代理**,第一次看到这一堆还是会恶心一下吧。其实这些术语不是 Spring AOP 独有的, 而是 AOP 设计思想的组成,所以这些术语也会比较抽象。
先说明,作为开发者,我认为不需要掌握全部的 AOP 思想,理解切面、通知、切入点、连接点就能写出 LogAspect
切面编程。
下面介绍一下,并且把术语代入到 Spring AOP 中。
连接点(JoinPoint):上述示例中这里有三个方法,任意单个方法就叫一个连接点。官方一点的解释,表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,在AOP中表示为在哪里干。
切入点(Pointcut):上述示例中,service包下的所有子包、类和方法都是切入点,因为切入点表达式编写成这样,切入点说白了就是表达式规则。官方解释:选择一组相关连接点,即可以认为连接点的集合,在AOP中表示为在哪里干的集合。
通知(Advice):真正要织入的代码逻辑,比如上述例子,我们要给切入点织入日志记录逻辑,这段日志逻辑便是通知,为什么叫通知?那么抽象,因为有前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),允许你在原有方法执行前、后、环绕等加入日志逻辑。在AOP中表示为干什么。
切面(Aspect): 面向切面编程AOP的主角就是这位,以上述例子来讲,打上 @Aspect 注解的 LogAspect
类就是切面,目前这个类中只有一个环绕通知 logAround()
, 当然可以增加通知,所以这一整个类编程范式就是切面编程,那么切面就是 LogAspect
。
引入(Introduction): 这个就偏概念了,上述示例中,UserService
有三个方法,加一个新的方法,这就叫引入,新增的方法也是一个新的连接点。在切面编程已经实现的情况下,UserService
新增方法,就叫做引入。
目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象。在AOP中表示为对谁干。
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入,运行时生成动态代理代码的这个过程叫做织入。在 AOP 中表示为怎么实现的。
AOP 代理(AOP): AOP 框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式;
织入和代理应该有点难分清吧?
织入是生成代理对象的过程,有了代理对象,通知功能得以实现。可以看出 Spring AOP 本质上是通过动态代理实现。
一图胜千言
如果坚持读到这里,我相信你搞明白了 AOP 和 Spring AOP 的概念,一个设计思想,一个是具体实现,另外AspectJ 也是实现 AOP 的一门技术。 细心的你已经发现了上述示例 @Aspect 注解来源于 aspectj 包,并不是 aop 包的注解。
如下图所示,我们引入的 starter - aop 依赖包含了 aspectj 和 aop 两个技术,说明 spring 开发小组直接使用了 aspectj 包作为 Spring AOP 实现的一部分。
那必须要讲一讲 Spring AOP 和 AspectJ 之间的瓜葛。
Spring AOP 和 AspectJ 的渊源
**Aspect 是什么呢?**AspectJ 是一个 java 语言实现的 AOP 框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有 AspectJ 的 AOP 功能(当然需要特殊的编译器)。
可以这样说 AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ 与 java 程序完全兼容,几乎是无缝关联,因此对于有 java 编程基础的工程师,上手和使用都非常容易。
Spring AOP 和 AspectJ 是什么关系?
1)AspectJ 是更强的 AOP 框架,是实际意义的 AOP 标准;
2)Spring 为何不写类似 AspectJ 的框架? Spring AOP 使用纯 Java 实现, 它不需要专门的编译过程, 它一个重要的原则就是无侵入性(non-invasiveness); Spring 小组完全有能力写类似的框架,只是 Spring AOP 从来没有打算通过提供一种全面的 AOP 解决方案来与 AspectJ 竞争。Spring 的开发小组相信无论是基于代理(proxy-based)的框架如 Spring AOP 或者是成熟的框架如 AspectJ 都是很有价值的,他们之间应该是互补而不是竞争的关系。
3) Spring 小组喜欢 @AspectJ 注解风格更胜于 Spring XML 配置; 所以在 Spring 2.0 使用了和 AspectJ 5 一样的注解,并使用 AspectJ 来做切入点解析和匹配。但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver), 但是注解和切点解析用的都是 AspectJ 的技术。
4)Spring 2.5 对 AspectJ 的支持:在一些环境下,增加了对 AspectJ 的装载时编织支持,同时提供了一个新的bean切入点。
看样子 AspectJ 哪哪都好呀,更强大,更全面,完全按照 AOP 标准来实现,为啥 Spring 小组还要开发 Spring AOP 呢?
以下Spring官方的回答:(总结来说就是 Spring AOP更易用,AspectJ更强大)。
- Spring AOP 比完全使用 AspectJ 更加简单, 因为它不需要引入 AspectJ 的编译器/织入器到你开发和构建过程中。 如果你仅仅需要在 Spring bean 上通知执行操作,那么 Spring AOP 是合适的选择。
- 如果你需要通知 domain 对象或其它没有在 Spring 容器中管理的任意对象,那么你需要使用 AspectJ。
- 如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。
他们的关系稍微了解即可,回归 Spring AOP 的学习。
Spring AOP 的配置方式
Spring AOP 支持对 **XML 模式**和基于 **@AspectJ 注解**的两种配置方式。本文例子使用的是 @AspectJ 注解式开发切面编程,XML 就是写一堆 XML 配置,用的很少,知道可以 XML 配置即可,不做实战演示。
Spring 使用了 @AspectJ 框架为 AOP 的实现提供了一套注解,下面归纳一下。
注解名称 | 解释 |
---|---|
@Aspect | 用来定义一个切面。 |
@pointcut | 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@After-Throwing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 |
@After | 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor (不要求掌握)。 |
Spring AOP 的实现方式
Spring AOP 的实现方式是动态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的;**如 Java JDK的动态代理( Proxy,底层通过反射实现)或者 CGLIB 的动态代理(底层通过继承实现)**,Spring AOP 采用的就是基于运行时增强的代理技术。所以我们看下如下的两个例子
- 基于 JDK 代理例子
- 基于 Cglib 代理例子
经典面试题,接口使用 JDK 动态代理,类使用 Cglib 动态代理。Spring Boot 2.x 开始默认优先使用 Cglib 动态代理技术,无论你的类是否实现了接口,Spring Boot 默认都会尝试使用 CGLIB 来创建代理对象。
参考文章:https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1
spring boot 2.0 前默认优先使用 JDK 代理,2.0 后默认优先使用 Cglib 代理,Spring AOP 的底层实现方式是“同时支持 JDK 动态代理和 CGLIB,并根据配置和目标对象的情况,智能选择其中一种来工作”,不改配置默认就是 Cglib 。
再来一道经典面试题。
JDK 动态代理和 Cglib 代理动态代理的区别?
一张表格看懂特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
核心原理 | 基于接口 | 基于继承 |
代理对象类型 | 生成目标接口的新类 (**com.sun.proxy.$Proxy** ) |
生成目标类的子类 (**...$$EnhancerBySpringCGLIB$$...** ) |
对目标类的要求 | 必须实现至少一个接口 | 可以是任何类(但不能是 **final** 类) |
对方法的要求 | 只能代理接口中的方法 | 可以代理类中的 **public** /**protected** 方法,不能代理 **final** 或 **static** 方法 |
依赖 | JDK 原生支持,无需外部库 | 需要引入 **cglib** (或 Spring 内置的 **spring-core** ) |
下面我们抛开 Spring,用最原生的代码来分别实现这两种代理,你会立刻明白它们的工作方式。
先从 Cglib 开始。
Cglib 动态代理技术原理
**CGLIB 动态代理**:它在运行时为你创建一个**目标类的子类**。这个子类会重写父类(你的原始类)的所有非 `**final**` 方法,并在重写的方法中加入额外的逻辑(切面代码)。**它和你的原始类是父子关系。**场景预设
我们想在调用 OrderService
的 createOrder
方法前后,打印日志。
创建一个包 cglib ,一顿复制。
1) 添加 CGLIB 依赖,Spring boot 项目不用导入依赖。
<!-- Maven -->
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
2)创建一个目标类(无需接口)
public class OrderService {public void createOrder(String productName) {System.out.println("【核心业务】正在创建订单: " + productName);}
}
3)创建 **MethodInterceptor**
(核心逻辑拦截器),下一步发挥作用。
public class LogMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {// 1. 前置增强(日志)System.out.println("[CGLIB代理] - 准备执行方法: " + method.getName());// 2. 调用父类(目标对象)的原方法// 注意:这里用的是 proxy.invokeSuper,而不是 method.invokeObject result = proxy.invokeSuper(obj, args);// 3. 后置增强(日志)System.out.println("[CGLIB代理] - 方法执行完毕");return result;}
}
4)创建代理对象并使用
public class CglibProxyDemo {public static void main(String[] args) {// 1. 创建 Enhancer 对象,类似于 JDK 中的 Proxy 类Enhancer enhancer = new Enhancer();// 2. 设置父类(目标类)enhancer.setSuperclass(OrderService.class);// 3. 设置回调(拦截器)enhancer.setCallback(new LogMethodInterceptor());// 4. 创建代理对象OrderService proxyInstance = (OrderService) enhancer.create();// 5. 使用代理对象调用方法System.out.println("代理对象的类型: " + proxyInstance.getClass().getName());proxyInstance.createOrder("iPhone 15");}
}
运行结果:
JDK 动态代理技术原理
**JDK 动态代理**:它不关心你的类是什么,只关心你实现了哪些接口。它在运行时**通过反射**为你创建一个**全新的类**,这个新类实现了你指定的所有接口,并把所有方法调用都转发到一个 `**InvocationHandler**` 上。**它和你的原始类没有父子关系。**场景预设
我们想在调用 UserService
的 addUser
方法前后,打印日志。
1)定义一个接口
public interface UserService {void addUser(String username);
}
2)创建接口的实现类(目标对象)
public class UserServiceImpl implements UserService {@Overridepublic void addUser(String username) {System.out.println("【核心业务】正在添加用户: " + username);}
}
3)创建 **InvocationHandler**
(核心逻辑处理器)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class LogInvocationHandler implements InvocationHandler {// 1. 持有目标对象的引用private Object target;public LogInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 2. 前置增强(日志)System.out.println("[JDK代理] - 准备执行方法: " + method.getName());// 3. 调用目标对象的原方法Object result = method.invoke(target, args);// 4. 后置增强(日志)System.out.println("[JDK代理] - 方法执行完毕");return result;}
}
4)创建代理对象并使用,可以看到 JDK 是反射技术实现代理。
import java.lang.reflect.Proxy;public class JdkProxyDemo {public static void main(String[] args) {// 1. 创建目标对象UserServiceImpl target = new UserServiceImpl();// 2. 创建代理对象UserService proxyInstance = (UserService) Proxy.newProxyInstance(target.getClass().getClassLoader(), // 类加载器target.getClass().getInterfaces(), // 目标对象实现的接口new LogInvocationHandler(target) // 事件处理器);// 3. 使用代理对象调用方法System.out.println("代理对象的类型: " + proxyInstance.getClass().getName());proxyInstance.addUser("张三");}
}
执行结果:
参考资料
https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1
https://pdai.tech/md/spring/spring-x-framework-aop.html
本文由mdnice多平台发布