面向切面的AOP编程的引入:
1. 代码缺陷
- 非核心代码对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能代码重复,分散在各个业务功能方法中!冗余,且不方便统一维护!
2. 解决思路
核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
将重复的代码统一提取,并且[[动态插入]]到每个业务方法!
3. 技术困难
解决问题的困难:提取重复附加功能代码到一个类中,可以解决。
如何实现这种提取的技术?——需要使用代理模式。
一、代理模式的概述(sketch)
1.代理模式
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
相关术语:
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)
- 动词:指做代理这个动作,或这项工作
- 名词:扮演代理这个角色的类、对象、方法
- 目标:被代理“套用”了核心逻辑代码的类、对象、方法。(房东)
代理在开发中实现的方式具体有两种:静态代理,[动态代理技术]
2.静态代理
主动创建代理类
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将非核心代码集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
代码举例:
目标接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
目标(实现类):
public class CalculatorPureImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
静态代理类:(继承目标接口,实现接口方法,使用构造函数传入目标,这样就可以通过目标类的对象调用核心代码;在方法中写非核心代码,核心代码由目标对象调用;当我们测试时我们调用代理的方法,而代理又调用了目标的方法)
public class StaticProxyCalculator implements Calculator {
//使用构造函数传入目标
private Calculator calculator;
public StaticProxyCalculator(Calculator target){
this.calculator=target;
}
@Override
public int add(int i, int j) {
//非核心代码
System.out.println("i = " + i + ", j = " + j);
//调用目标
int result = calculator.add(i, j);
System.out.println("result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
return 0;
}
@Override
public int mul(int i, int j) {
return 0;
}
@Override
public int div(int i, int j) {
return 0;
}
}
3.动态代理
- JDK动态代理
JDK原生的实现方式,需要被代理的目标类必须实现接口!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)
- cglib动态代理
通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹)
4.代理模式的优缺点
优点:
可以解决附加功能代码干扰核心代码和不方便统一维护的问题。
实现方式主要是:将附加功能代码提取到代理中执行,不干扰目标核心代码!
缺点:
无论使用静态代理和动态代理(jdk,cglib),程序员的工作都比较繁琐,需要自己编写代理工厂等。
但是,在实际开发中,不需要编写代理代码,我们可以使用[Spring AOP]框架,会简化动态代理的实现!
二、面向切面的编程思维AOP
AOP:Aspect Oriented Programming面向切面编程
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP,可以在不修改原来代码的基础上添加新功能。
1. AOP思想主要的应用场景
AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以应用于各种场景,以下是一些常见的AOP应用场景:
1. 日志记录
在系统中记录日志是非常重要的,使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
2. 事务处理
在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
3. 安全控制
在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
4. 性能监控
在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
5. 异常处理
系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
6. 缓存控制
在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
7. 动态代理
AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
综上所述,AOP可以应用于各种场景,它的作用是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。
AOP代码,用于处理非核心代码的冗余业务。
2.AOP八个核心名词
1-横切关注点
在各个类中具有相似形式的非核心代码。
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开。
2-通知(增强)
增强:提取冗余代码。并且可以在方法前和方法后执行逻辑代码。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(**寿终正寝**)
- 异常通知:在被代理的目标方法异常结束后执行(**死于非命**)
- 后置通知:在被代理的目标方法最终结束后执行(**盖棺定论**)
- 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
3-连接点 joinpoint
连接点:目标方法
这也是一个纯逻辑概念,不是语法定义的。
指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法
4-切入点 pointcut
定位连接点的方式,或者可以理解成被选中的连接点!
是一个表达式,比如execution(* com.spring.service.impl.*.*(..))。符合条件的每个方法都是一个具体的连接点。
5-切面 aspect
切点+增强
是一个类。
6-目标 target
被代理的目标对象。
7-代理 proxy
向目标对象应用通知之后创建的代理对象。
8-织入 weave
指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
三、Spring-aop框架
1. AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!
2. 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐。
3. Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架!SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!
基于注解方式实现spring-AOP
1.导入依赖
2.正常编写核心业务,加入ioc容器
3,编写ioc的配置类和文件
4.测试环境
5.增强类,定义三个增强方法
6.增强类的配置
7.开启aop的配置
四、实现aop切面编程
1、总体过程
(1)准备接口和接口的实现类
(2)准备一个切面类(@Aspect,@Component)
在切面类中使用增强方法。
注意:需要使用@Aspect表示这个类是一个切面类,使用@Component注解放入ioc容器中
1.定义方法存储增强代码
* 核心代码中,在不同位置的非核心代码出现一次,对应一个增强方法。
* 使用注解配置,可以指定插入目标方法的位置
* 2. 使用注解配置 指定插入目标方法的位置
* 前置 @Before
* 后置 @AfterReturning
* 异常 @AfterThrowing
* 最后 @After
* 环绕 @Around
* 3.配置注解的内容
* 4.完善注解,@Component
*
* @Author Qum
* @Create 2025/3/19 10:28
*/
@Aspect
@Component
public class LogAdvice {
//注解需要指定插入的方法
@Before("execution(* com.atguigu.service.impl.*.*(..))")
public void start(){
System.out.println("function started");
}
@After("execution(* com.atguigu.service.impl.*.*(..))")
public void after(){
System.out.println("function ended");
}
@AfterThrowing("execution(* com.atguigu.service.impl.*.*(..))")
public void error(){
System.out.println("function has error");
}
}
(3)创建一个注解类文件(@EnableAspectJAutoProxy,@Configuration,@ComponentScan)
@Configuration
@ComponentScan(basePackages = "com.atguigu")
//作用等于 <aop:aspectj-autoproxy /> 配置类上开启 Aspectj注解支持!
@EnableAspectJAutoProxy
public class MyConfig {
}
2、获取增强方法的具体信息
* 1、定义方法
* 2、使用注解指定对应的位置
* 3、配置切点表达式选中方法
* 4、切面和ioc 的配置
* 5、开启aspect的注解
*
* 增强方法中,获取目标方法的信息
* 在方法中传入JoinPoint joinPoint,使用joinPoint获取各种信息
* 返回的结果-@AfterReturning
* 在注解中指定返回变量,Object result
* 异常的信息-@AtferThrowing
* 在注解中指定异常的参数
(1) JointPoint接口
需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。
- 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
- 要点2:通过目标方法签名对象获取方法名
- 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
```Java
// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
public void printLogBeforeCore(JoinPoint joinPoint) {
// 1.通过JoinPoint对象获取目标方法签名对象
// 方法的签名:一个方法的全部声明信息
Signature signature = joinPoint.getSignature();
// 2.通过方法的签名对象获取目标方法的详细信息
String methodName = signature.getName();
System.out.println("methodName = " + methodName);
int modifiers = signature.getModifiers();
System.out.println("modifiers = " + modifiers);
String declaringTypeName = signature.getDeclaringTypeName();
System.out.println("declaringTypeName = " + declaringTypeName);
// 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
Object[] args = joinPoint.getArgs();
// 4.由于数组直接打印看不到具体数据,所以转换为List集合
List<Object> argList = Arrays.asList(args);
System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}
```
(2) 方法返回值
在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值!
```Java
// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
returning = "targetMethodReturnValue"
)
public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
}
```
(3)异常对象捕捉
在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象
```Java
// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
throwing = "targetMethodException"
)
public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}
```
3、切点表达式的规则
* 切点表达式固定语法execution(1 2 3.4.5(6))
* 1.访问修饰符
* 2.方法的返回值类型
* String int void
* 如果不考虑访问修饰符和返回值,则整合声明为*
* 如果不考虑,则两个都不考虑。
* 3.包的位置
* 具体包;com.atguigu.service
* 单层模糊:com.atguigu.service.*
* 多层模糊:com..impl 任意层的模糊
* *..*.*(..)表示全部包下的全部类的全部方法。..不能开头但是*可以
* 细节:..不能开头
* 4.类的名称
* 具体:CalculatorPureImpl
* 模糊:*
* 部分模糊:*Impl
* 5. 方法名 语法和类名一直
* 6.形参列表
* 没有参数:()
* 有具体参数(String) 若有两个参数:(String,Int)
* 部分具体和模糊:
* 第一个参数是字符串的方法 (String..)
* 最后一个参数是字符串 (..String)
* 字符串开头,int结尾 (String..int)
* 包含int类型(..int..)
4、统一切点管理(@PointCut)
@Pointcut("execution(* com.atguigu.service.impl.*.*(..))")
public void pc(){
}
//切点的统一管理
//1.定义一个空方法
//2.加上注解@Pointcut(“写入切点表达式”)
//2.可以在增强注解的括号内写入定义的空方法
推荐:
/创建一个切点类,存储统一管理的切点表达式
@Component
public class MyPointCut {
@Pointcut("execution(* com.atguigu.service.impl.*.*(..))")
public void pc(){
}
@Pointcut("execution(* com..impl.*.*(..))")
public void mypc(){
}
}
对于增强方法括号内切点表达式的引用:
@Component
@Aspect
@Order(5)
public class TXAdvice {
@Before("com.atguigu.pointcut.MyPointCut.pc()")
public void start(){
System.out.println("事务开启");
}
@AfterReturning("com.atguigu.pointcut.MyPointCut.pc()")
public void commit(){
System.out.println("事务提交");
}
}
5、环绕通知(@Around)
环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。
@Component
@Aspect
public class TXAroundAdvice {
@Around("com.atguigu.pointcut.MyPointCut.pc()")
//在方法上使用@Around的注解,同样需要传入切点方法
public Object transaction(ProceedingJoinPoint proceedingJoinPoint){
// 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型的对象传给我们
//环绕通知 需要再通知中定义目标方法的执行
Object[] args = proceedingJoinPoint.getArgs();
//通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
// 通过ProceedingJoinPoint对象获取目标方法的签名对象
Signature signature = proceedingJoinPoint.getSignature();
// 声明变量用来存储目标方法的返回值
Object result=null;
try {
System.out.println("开启事务");//相当于@Before
result = proceedingJoinPoint.proceed(args);
System.out.println("结束事务");//相当于@AfterReurning
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
}
return result;
}
}
6、切面优先级设置(@Order)
使用 @Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
7、CGLib动态代理生效
在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。
使用总结:
a. 如果目标类有接口,选择使用jdk动态代理
b. 如果目标类没有接口,选择cglib动态代理
c. 如果有接口,接口接值
d. 如果没有接口,类进行接值
使用aop的总结:
xml配置aop
<!-- 配置目标类的bean -->
<bean id="calculatorPure" class="com.atguigu.aop.imp.CalculatorPureImpl"/>
<!-- 配置切面类的bean -->
<bean id="logAspect" class="com.atguigu.aop.aspect.LogAspect"/>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
<!-- aop:aspect标签:配置切面 -->
<!-- ref属性:关联切面类的bean -->
<aop:aspect ref="logAspect">
<!-- aop:before标签:配置前置通知 -->
<!-- method属性:指定前置通知的方法名 -->
<!-- pointcut-ref属性:引用切入点表达式 -->
<aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
<!-- aop:after-returning标签:配置返回通知 -->
<!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
<aop:after-returning
method="printLogAfterCoreSuccess"
pointcut-ref="logPointCut"
returning="targetMethodReturnValue"/>
<!-- aop:after-throwing标签:配置异常通知 -->
<!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
<aop:after-throwing
method="printLogAfterCoreException"
pointcut-ref="logPointCut"
throwing="targetMethodException"/>
<!-- aop:after标签:配置后置通知 -->
<aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
<!-- aop:around标签:配置环绕通知 -->
<!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
</aop:aspect>
</aop:config>
五、aop对于获取bean的影响
1. 情景一
- bean 对应的类没有实现任何接口
- 根据 bean 本身的类型获取 bean
- 测试:IOC容器中同类型的 bean 只有一个
正常获取到 IOC 容器中的那个 bean 对象
- 测试:IOC 容器中同类型的 bean 有多个
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个
解决:根据唯一的id值获取
2. 情景二
- bean 对应的类实现了接口,这个接口也只有这一个实现类
- 测试:根据接口类型获取 bean
- 测试:根据类获取 bean
- 结论:上面两种情况其实都能够正常获取到 bean,而且是同一个对象
3. 情景三
- 声明一个接口
- 接口有多个实现类
- 接口所有实现类都放入 IOC 容器
- 测试:根据接口类型获取 bean
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个
- 测试:根据类名获取bean
正常
4. 情景四
- 声明一个接口
- 接口有一个实现类
- 创建一个切面类,对上面接口的实现类应用通知
- 测试:根据接口类型获取bean
正常
- 测试:根据类获取bean
无法获取
原因分析:
- 应用了切面后,真正放在IOC容器中的是代理类的对象
- 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的
情景五
- 声明一个类
- 创建一个切面类,对上面的类应用通知
- 测试:根据类获取 bean,能获取到
使用总结
目标类有接口时,放入ioc容器的是jdk动态代理实现目标接口的代理对象,此时只能根据接口类型获取bean,实现类没有被放入ioc容器中,无法通过实现类得到对应的bean。