面向切面编程(Aspect Oriented Programming)简称 AOP,是一种编程范式,用于弥补OOP很难优雅处理横跨在程序多个模块的公共行为,例如日志记录、事务管理、缓存管理等。AOP通过将这些行为从业务逻辑中分离,降低了代码的耦合度,使得业务层能专注于业务的实现,公共行为对业务逻辑无代码侵入。示意图如下所示:
1.AOP概念
要学习AOP,需要先了解其核心概念及相关术语,以下这些概念及术语并非Spring独有。
Aspect:切面,切面封装了横切关注点的模块,如日志记录切面、事务管理切面等。
Join point:连接点,程序执行中的某个特定点,如方法执行或异常处理等。
Advice:通知,指的是切面在特定连接点采取的动作。Advice 包括环绕(Around)、前置(Before)和后置(After)通知。许多 AOP 框架,包括 Spring,在模型设计上将 Advice 视作拦截器,并围绕连接点维护一系列拦截器链。
Pointcut:切入点,匹配连接点的表达式。Advice 与 Pointcut 表达式关联,并在由 Pointcut 匹配的任何连接点运行(例如,执行具有特定名称的方法)。
Introduction:引入,声明新的接口及其实现,并将其绑定到已存在的Bean上。 Target object:目标对象,是由一个或多个切面所增强的对象。
AOP proxy:AOP代理,由AOP框架创建的对象,用于实现具体的切面逻辑。
Weaving:织入,将切面代码插入到连接点的过程。可以在编译时、加载时或运行时完成。
2.Spring AOP
Spring AOP是对AOP的一种实现,其目标是为了提供简单通用的AOP功能,用于覆盖日常编程中的大部分场景,但对于如作用域级别等切入点,Spring AOP无法解决,需要通过引入如AspectJ等外部组件来实现。
Spring AOP通过动态代理来实现 AOP 功能。当一个类被声明为切面后,Spring 会为这个类创建一个代理对象。当应用程序中的方法被调用时,实际上是调用了代理对象的方法。代理对象根据切点表达式判断是否需要执行切面中的通知代码。
2.1 动态代理
Spring 支持两种类型的动态代理:
- JDK动态代理:适用于实现了接口的类。
- CGLIB代理:适用于未实现接口的类。
Spring为什么要选用两种动态代理,而不是直接使用CGLIB代理呢?我们可以先看一下以下表格:
代理类型 | 优点 | 局限性 |
---|---|---|
JDK 动态代理 | 1.性能相对较好。 2.生成的代理类更加简洁。 3.支持多个接口的代理。 | 1.只能对实现了接口的类生成代理。 |
CGLIB 代理 | 1.可以为任何类生成代理,即使是没有实现接口的类。 2.支持final method的代理。 | 1.性能略低于 JDK 动态代理。 2.生成的代理类通常比 JDK 动态代理更复杂。 3.不支持final class的代理 。 |
通过两种动态代理的优点及局限性,Spring为了灵活性、性能优化等方面考虑,因此采用了组合的方式。
2.1.1 JDK动态代理
JDK动态代理是由JDK内部提供的java.lang.reflect.Proxy实现的代理功能,其只支持代理实现了接口的类。代码示例如下:
先定义接口及其实现类:
java
代码解读
复制代码
public interface OriginServiceInterface { void test(); } public class OriginService implements OriginServiceInterface { @Override public void test() { System.out.println("origin..."); } }
使用Proxy生成代理对象:
整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
java
代码解读
复制代码
// 原始对象 OriginService originService = new OriginService(); // 创建代理对象,由于代理对象是基于接口生成的,因此返回的对象类型是接口类型 OriginServiceInterface proxiedOrigService = (OriginServiceInterface) Proxy.newProxyInstance(OriginService.class.getClassLoader(), new Class[]{OriginServiceInterface.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("jdk before"); // 执行被代理的方法 Object res = method.invoke(originService, args); System.out.println("jdk after"); return res; } }); // 通过代理对象调用代理方法 proxiedOrigService.test();
执行结果:
java
代码解读
复制代码
jdk before origin... jdk after
2.1.2 CGLIB代理 CGLIB(Code Generation Library)是Spring引入的第三库,CGLIB 是一个高性能的、动态的字节码生成库,它可以为现有的类生成子类,并在子类中加入增强逻辑。Spring通过引入此库来支持AOP特性,弥补了JDK动态代理无法基于类生成代理类的缺口。其通过提供的org.springframework.cglib.proxy.Enhancer进行代理对象创建,代码示例如下:
定义普通类:
java
代码解读
复制代码
public class OriginService { public void test() { System.out.println("origin..."); } }
使用CGLIB生成代理对象:
java
代码解读
复制代码
// 原始对象 OriginService originService = new OriginService(); // CGLIB Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OriginService.class); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("cglib before"); // 执行被代理的方法 Object res = methodProxy.invoke(originService, objects); System.out.println("cglib after"); return res; } }); // 创建代理对象 OriginService proxiedOrigService = (OriginService) enhancer.create(); // 通过代理对象调用代理方法 proxiedOrigService.test();
执行结果:
java
代码解读
复制代码
cglib before origin... cglib after
2.1.3 ProxyFactory
在上述内容中介绍了两种动态代理技术,Spring为了便于代理对象的创建,对其进行了封装,对应封装类为org.springframework.aop.framework.ProxyFactory。代码示例如下:
java
代码解读
复制代码
OriginService originService = new OriginService(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(originService); proxyFactory.addAdvice(MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("before"); Object res = invocation.proceed(); System.out.println("after"); return res; } }); OriginService proxiedOrigService = (OriginService) proxyFactory.getProxy(); proxiedOrigService.test();
通过ProxyFactory,我们可以不用关注被代理的类是否实现了接口以及选择代理类实现方式,简化了代理对象的创建。
此处通过构建Advice对象,作为通知,可在其内部实现功能增强。此处对应了Spring AOP的基础结构的三部分,分别是Advisor、Advice、Pointcut,具体见2.2节。
2.2 Spring AOP的基础结构
在Spring AOP中,Advisor、Advice、Pointcut是核心组成部分,它们一起构成了Spring AOP的基础结构。
- Advisor
- 定义: Advisor 是 Advice 和 Pointcut 的组合。它告诉Spring AOP框架在何处(由 Pointcut 定义)应用何种通知(由 Advice 定义)。
- 作用: Advisor 封装了切面的行为和触发时机,使得AOP框架可以基于此创建对应代理对象。
- 类型: 在Spring AOP中默认实现是DefaultPointcutAdvisor。
- Advice
- 定义: Advice 定义了切面所执行的操作。它是切面的真正业务逻辑部分。
- 作用: Advice 在程序的特定连接点执行一些行为,比如在方法调用前或后执行某些操作。
- 类型:Before advice、After returning advice、After throwing advice、After (finally) advice、Around advice,详见2.3节。
- Pointcut (切点)
- 定义: Pointcut 定义了通知应该应用到哪些连接点上。它是一种匹配规则,用于确定何时以及在哪里应用通知。
- 作用: Pointcut 通常用来选择一组连接点(如方法调用),并指定通知应该在这些连接点上执行。
- 表达式: Pointcut 可以使用表达式语言来定义,Spring AOP 支持基于方法名、类、返回值等的切点表达式
2.3 通知类型
Spring支持多种类型的通知,如下表所示:
通知类型 | 说明 |
---|---|
Before advice | 在目标方法执行前执行 |
After returning advice | 只有当方法成功执行后才会执行 |
After throwing advice | 只有当方法抛出异常时才会执行 |
After (finally) advice | 无论方法成功或失败都会执行,在方法执行之后执行 |
Around advice | 环绕通知,可以控制方法的执行流程 |
3.Spring AOP使用示例
Spring支持XML配置、注解配置方式来声明切面,以下将以注解配置的方式来提供示例。
注解方式的首创是AspectJ,Spring直接将其定义的注解拿了过来,对应注解的具体解析还是Spring自己实现的。因此在使用注解前,项目需先引入aspectjweaver依赖(具体版本视实际情况而定)。 1.依赖引入:
java
代码解读
复制代码
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
2.要代理的类如下:
java
代码解读
复制代码
@Component public class OriginService { public void test() { System.out.println("origin..."); } }
3.开启Spring对AOP的支持:
在使用Spring AOP之前首先需要开启对AOP的支持,可以通过在配置类中使用注解 @EnableAspectJAutoProxy来开启对AOP的支持,代码示例如下:
java
代码解读
复制代码
@Configuration @EnableAspectJAutoProxy @ComponentScan(basePackages = {"spring.aop.example"}) public class AppConfig { //... }
4.声明切面:
java
代码解读
复制代码
public class LogAspect { @Pointcut("execution(* spring.aop.example.service.OriginService.test())") public void pointcut() {} @Before("pointcut()") public void logBefore() { System.out.println("Log before..."); } @Around("pointcut()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("Log around before method: " + joinPoint.getSignature().getName()); // 执行目标方法 Object result = joinPoint.proceed(); System.out.println("Log around after method: " + joinPoint.getSignature().getName()); return result; } @After("pointcut()") public void logAfter() { System.out.println("Log after..."); } @AfterReturning("pointcut()") public void logAfterReturning() { System.out.println("Log after returning..."); } }
5.测试类:
java
代码解读
复制代码
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(AppConfig.class); context.refresh(); OriginService originService = context.getBean(OriginService.class); originService.test();
6.输出结果:
java
代码解读
复制代码
Log around before method: test Log before... origin... Log after returning... Log after... Log around after method: test
4.Spring AOP的使用场景
Spring AOP 在实际开发中有广泛的应用,以下是几个常见的应用场景:
- 日志记录:在方法执行前后记录日志,方便调试和追踪。
- 权限验证:在访问资源前进行权限验证。
- 事务管理:自动管理数据库事务,简化代码。
- 性能监控:记录方法执行时间,进行性能分析。
- 缓存处理:缓存方法的结果,提高性能。
- 异常处理:统一处理方法抛出的异常。
5.总结
AOP作为一种编程范式,弥补OOP很难优雅处理横跨在程序多个模块的公共行为,其将这些行为从业务逻辑中分离,降低了代码的耦合度,使得业务层能专注于业务的实现。其核心概念有切面、连接点、通知、切点、织入。
Spring AOP作为AOP的实现框架之一,通过代理模式实现了AOP,其代理对象的创建方式有两种:①JDK动态代理;②CGLIB。其核心组成为Advisor、Advice、Pointcut,通过这三个接口的实现,完成了切面相关概念的抽象,方便了与AspectJ的结合。
Spring AOP常用于事务管理、权限验证、缓存管理等场景。