AOP(Aspect-Oriented Program,即面向切面编程)和OOP(Object-Oriented Programming, 面向对象编程)是Java中两种不同的抽象设计架构。AOP是通过预编译方式,在运行期通过代理的方式向目标类织入增强代码实现程序功能的统一维护的一种技术。Spring 中 AOP 代理由 Spring 的 IoC 容器负责生成和管理,其依赖关系也由 IoC 容器负责管理,因此两者相辅相成。
AOP与OOP是面向不同领域的两种设计思想。
OOP实际上是对对象的属性和行为的封装,而是处理某个步骤和阶段的,从中进行切面的提取。也就是说,如果几个或更多个逻辑过程中,有重复的操作行为,AOP就可以提取出来,运用动态代理,达到程序功能统一维护的效果。
这么说来可能太含蓄,如果说到权限判断,日志记录等,可能就明白了。如下图所示:
上面我们提到了AOP与OOP,但是如果我们单纯使用OOP,那么权限判断怎么办?在每个操作前都加入权限判断?日志记录怎么办?在每个方法里的开始、结束、异常的地方手动添加日志?所以,如果使用AOP就可以借助代理完成这些重复的操作,就能够在逻辑过程中,降低各部分之间的耦合了。二者扬长补短,互相结合最好。
首先,在面向切面编程的思想里面,把功能分为核心业务功能和周边功能。
周边功能(一般为重复的功能)在 Spring 的面向切面编程AOP思想里,即被定义为切面。
在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 "编织" 在一起,这就叫AOP。
AOP主要应用于日志记录,性能统计,安全控制,事务处理以及缓存等方面,它是为程序员解耦而生,并且对核心业务代码零侵入。
因此我们知道了为什么要使用AOP以及AOP做的事情了:
下面详细了解一些AOP的概念:
AOP指示器参数
通知方法注解
总结
- 切入点(Pointcut) 在哪些类,哪些方法上切入(where)
- 通知(Advice) 在方法执行的什么实际(when: 方法前/方法后/方法前后)做什么(what: 增强的功能)
- 切面(Aspect) 切面 = 切入点 + 通知,通俗点就是:在什么时机,什么地方,做什么增强!
- 织入(Weaving) 把切面加入到对象,并创建出代理对象的过程。(由 Spring 来完成)
AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。
AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,其实就是代理模式的典型应用。 按照 AOP 框架修改源代码的时机,可以将其分为四类:
因此Spring AOP是基于JDK和CGLib动态代理实现的,AspectJ是基于ASM与自定义ClassLoader实现的。
Spring AOP的实现是JDK和CGLib动态代理,这两种代理方式的使用了策略模式。
- 既然Spring AOP的实现是动态代理,那么JDK和CGLIB动态代理有什么区别呢?
- 何时使用JDK和CGLib?
因此,Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理。
- 那么我们开发时,JDK代理和CGLib代理该用哪个呢?
在《精通Spring4.x 企业应用开发实战》一书中给出了建议:
原因:JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。
总结:
类型 | 机制 | 回调方式 | 适用场景 | 效率 |
---|---|---|---|---|
JDK动态代理 | 委托机制,代理类和目标类都实现了同样的接口,InvocationHandler持有目标类,代理类委托InvocationHandler去调用目标类的原始方法 | 反射 | 目标类是接口类 | 效率瓶颈在反射调用稍慢 |
CGLIB动态代理 | 继承机制,代理类继承了目标类并重写了目标方法,通过回调函数MethodInterceptor调用父类方法执行原始逻辑 | 通过FastClass方法索引调用 | 非接口类,非final类,非final方法 | 第一次调用因为要生成多个Class对象较JDK方式慢,多次调用因为有方法索引较反射方式快,如果方法过多switch case过多其效率还需测试 |
AOP的实现有Spring AOP、AspectJ、JBoss AOP等。
本篇文章只在一定维度上讨论目前主流的AOP框架:Spring AOP 和 AspectJ。
AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件。
而Spring借鉴了AspectJ很多非常有用的做法,融合了AspectJ实现AOP的功能,可以说Spring AOP是新世纪的AspectJ。但Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器配合(为了编织代码的各个方面, 它引入了称为 AspectJ 编译器 (ajc) 的编译器, 通过它编译我们的程序, 然后通过提供一个小型 (100K) 运行时库来运行它。)。
Spring aop 旨在提供一个跨 Spring IoC 的简单的 aop 实现, 以解决程序员面临的最常见问题。它不打算作为一个完整的 AOP 解决方案 —— 它只能应用于由 Spring 容器管理的 bean。
另一方面, AspectJ 是原始的 aop 技术, 目的是提供完整的 aop 解决方案。它更健壮, 但也比 Spring AOP 复杂得多。还值得注意的是, AspectJ 可以在所有域对象中应用。
对比总结
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能,基准测试AspectJ 速度比 Spring AOP 快8到35倍 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
Spring AOP只是针对Spring IOC容器进行设计的aop实现,因此适用于Spring进行管理的项目;
AspectJ更复杂,因为它引入了 AspectJ Java 工具 (包括编译器 (ajc)、调试器 (ajdb)、文档生成器 (ajdoc)、程序结构浏览器 (ajbrowser)), 我们需要将它们与我们的 IDE 或生成工具。要使用 AspectJ,我们需要引入 AspectJ 编译器 (ajc) 并重新打包所有的库 (除非我们切换到编译后或加载时间的织入)。
Spring AOP更加简单、开箱即用,AspectJ性能更高但更复杂,但需要特定编译器配合。
具体应用场景有:日志记录,性能统计,安全控制,事务处理以及缓存等。
Spring AOP项目用于Spring项目,AspectJ应用于Spring生态外的项目。
AOP 的思想:减少重复的工作,让重复代码与业务代码分离!
在上图中,用户购物的核心业务就是下单和付款。而浏览商品、咨询商品以及售后都是重复且边缘的工作,每一个人在进行交易之前以及交易之后都需要重复进行这些工作,因此可以交给边缘业务其他人代理完成。因此,AOP的实现就是:围绕核心业务,以核心业务的时间先后为切入点来做核心业务的切面。
开发环境为SpringBoot FrameWork,故使用AOP只需添加下述依赖即可:
pom.xml
org.springframework.boot
spring-boot-starter-aop
2.6.4
目标类就是我们的核心,以方法执行先后时间为切入点,下单前、下单和下单后等地方都是插入切面通知的地方。
这些地方的集合就是连接点,而某一个具体的位置就是切点,连接点就是切点的集合。
OrderService.java
package com.deepinsea.springbootaop.springaop.service;
/**
* @author deepinsea
* @date 2022/4/12
*/
public interface OrderService {
/**
* 目标方法(订单)
*/
void order();
}
OrderServiceImpl.java
package com.deepinsea.springbootaop.springaop.service.impl;
import com.deepinsea.springbootaop.springaop.service.OrderService;
import org.springframework.stereotype.Service;
/**
* @author deepinsea
* @date 2022/4/12
*/
@Service
public class OrderServiceImpl implements OrderService {
@Override
public void order() {
// 仅仅是实现了核心的业务功能
System.out.println("下单");
System.out.println("付款");
}
}
创建一个类,将这个类变为切面。
spring aop中的五种通知方式:
- @Before:前置通知,在目标方法被调用之前调用通知功能
- @After:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
- @After-returning:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
- @After-throwing:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
- @Around:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务。
前置(@Before)与最终通知(@After)
OrderAspect.java
package com.deepinsea.springbootaop.springaop.common.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author deepinsea
* @date 2022/4/11
* 切面类
*/
@Component
@Aspect
public class OrderAspect {
@Before("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
public void beforeOrder() {
System.out.println("下单之前--浏览商品");
System.out.println("下单之前--咨询商品");
}
@After("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
public void afterOrder(){
System.out.println("下单之后--售后");
}
}
在定义完切面类后,我们发现所有方法的execution表达式内容完全一样,因此可以提取为一个公共的。这个也叫做切点(pointcut),是Spring AOP的高级抽象。使用@Pointcut实现重复内容的提取。
切点(@Pointcut)
OrderAspectPointcut.java
package com.deepinsea.springbootaop.springaop.common.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @author deepinsea
* @date 2022/4/12
* 切面类
*/
@Component
@Aspect
public class OrderAspectPointcut {
/**
* 通过注解@Pointcut定义切点,iOrder()只是一个切点标识,无所谓是什么,
* 方法中内容本身也是空的,使用该切点的地方直接通过切点方法iOrder引用切点表达式。
*/
@Pointcut("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
public void iOrder(){}
@Before("iOrder()")
public void beforeOrder() {
System.out.println("下单之前--浏览商品");
System.out.println("下单之前--咨询商品");
}
@After("iOrder()")
public void afterOrder(){
System.out.println("下单之后--售后");
}
}
环绕通知(@Around)
环绕通知,顾名思义,其会在目标方法被调用前后均会执行切面代码的一种通知,其可将前置通知、后置通知的代码放在一个通知方法当中,使得代码逻辑更加清晰。
这是Spring AOP中最强大的通知类型,因为它集成了前面所有通知的功能,同时保留了连接点原有的方法的功能,因此一个可以干掉上面4个注解。
注意
OrderAspectAround.java
package com.deepinsea.springbootaop.springaop.common.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @author deepinsea
* @date 2022/4/12
* 切面类
*/
@Component
@Aspect
public class OrderAspectAround {
// 环绕通知同样可以使用切点来简化
@Pointcut("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
public void pointcutOrder(){}
/**
* 使用 @Around 注解来同时完成前置和最终通知
* Spring AOP提供的ProceedingJoinPoint接口的proceed方法可以执行切点方法(并划分切点前后执行界限)
* 以joinPoint.proceed()为界限,划分出前置通知、后置通知和最终通知以及异常通知
*/
@Around("pointcutOrder()")
public void aroundOrder(ProceedingJoinPoint joinPoint){
System.out.println("下单之前--浏览商品");
System.out.println("下单之前--咨询商品");
try {
joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("下单之后--售后");
}
/**
* 在有返回值的核心业务(切点)方法中,使用环绕通知需要给返回值,否则核心方法返回值为null;
* 上面切点方法本身返回值为void,则不需要给返回值
*/
// @Around("pointcutOrder()")
// public Object aroundOrder(ProceedingJoinPoint joinPoint){
// Object returnValue = null;
// // 获取切点方法入参(形参)
// Object[] args = joinPoint.getArgs();
// // 执行前置方法
// System.out.println("前置通知方法");
// try {
// returnValue = joinPoint.proceed(args);
// System.out.println("后置通知方法");
// } catch (Throwable e) {
// e.printStackTrace();
// System.out.println("异常通知方法");
// }finally {
// System.out.println("最终通知方法");
// }
//
// return returnValue;
// }
}
上面三种方法都可以(使用其中一个,另外两个注释即可),只是一个更符合常规逻辑,一个进行了常规逻辑上的公共提取,另一个进行两者功能上的融合而已。推荐使用环绕通知进行aop配置。
扩展:Spring框架的设计非常优美,对多种设计模式进行了实现。在多切面执行时,就采用了责任链设计模式。
切面类,目标方法都创建完了,但是切面类在启动时还不能自动代理目标方法。因为现在切面类和目标方法只是注入到Spring IOC容器的Bean,没有开启 @EnableAspectJAutoProxy注解启动AspectJ自动代理,这个开关默认是关闭的。说到底,Spring AOP是一种除了Spring本身以外,另一种由用户代理注册到Spring的Bean的一种方式。
注意:SpringBoot中无需显式指定@EnableAspectJAutoProxy注解,默认为开启;但Spring FrameWork需要手动开启
(1)注解的方式
我们可以以配置类的方式,通过@EnableAspectJAutoProxy注解启动AspectJ自动代理:
AspectConfig.java
package com.deepinsea.springbootaop.springaop.common.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author deepinsea
* @date 2022/4/12
* 切面配置类
*
* Jdk代理:基于接口的代理,一定是基于接口,会生成目标对象的接口的子对象。
* Cglib代理:基于类的代理,不需要基于接口,会生成目标对象的子对象。
*
* 1. 注解@EnableAspectJAutoProxy开启代理;
* 2. 如果属性proxyTargetClass默认为false, 表示使用jdk动态代理织入增强;
* 3. 如果属性proxyTargetClass设置为true,表示使用Cglib动态代理技术织入增强;
* 4. 如果属性proxyTargetClass设置为false,但是目标类没有声明接口,spring aop还是会使用Cglib动态代理,也就是说非接口的类要生成代理都用Cglib。
*/
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
//@ComponentScan("com.deepinsea.springbootaop.springaop")
public class AspectConfig {
}
(2)配置文件的方式
application.yml
spring:
# AOP 配置
aop:
auto: true #spring aop开关, 相当于标注了@EnableAspectJAutoProxy
proxy-target-class: true #开启cglib代理
注意:默认EnableAspectJAutoProxy是关闭的,并且需要proxy-target-class选项一定需要开启,否则会报错。
如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。即使你未声明 proxy-target-class="true" ,但运行类没有继承接口,spring也会自动使用CGLIB代理(高版本spring自动根据运行类选择 JDK 或 CGLIB 代理)
默认情况下,我们会在接口实现类加上@service注解以及进行事务注解的声明(全局声明或方法上声明都可),这种做法提供接口类服务供rpc调用是没问题的,客户端只需引入相应的jar包,spring中配置bean,使用时@Resource或@Autowired注解注入都可以。
SpringbootAopApplicationTests.java
package com.deepinsea.springbootaop;
import com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootAopApplicationTests {
@Autowired
private OrderServiceImpl orderService;
/**
* 测试AOP切面功能
*/
@Test
void testAop() {
orderService.order();
}
}
启动后,测试结果:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |___, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.4)
2022-04-12 03:25:04.600 INFO 57392 --- [ main] c.d.s.SpringbootAopApplicationTests : Starting SpringbootAopApplicationTests using Java 1.8.0_311 on aries with PID 57392 (started by deepinsea in E:\IdeaProjects\MyProjects\springboot-in-action\springboot-aop)
2022-04-12 03:25:04.609 INFO 57392 --- [ main] c.d.s.SpringbootAopApplicationTests : No active profile set, falling back to 1 default profile: "default"
2022-04-12 03:25:05.079 INFO 57392 --- [ main] c.d.s.SpringbootAopApplicationTests : Started SpringbootAopApplicationTests in 0.661 seconds (JVM running for 1.44)
下单之前--浏览商品
下单之前--咨询商品
下单
付款
下单之后--售后
Process finished with exit code 0
原文链接:https://juejin.cn/post/7129100142749155336