软件设计之SSM(5)
路线图推荐:
【Java学习路线-极速版】【Java架构师技术图谱】
尚硅谷新版SSM框架全套视频教程,Spring6+SpringBoot3最新SSM企业级开发
资料可以去尚硅谷官网免费领取
学习内容:
AOP面向切面编程
- 代理
- AOP面向切面编程
- 获取通知细节信息
- 切点表达式
- 环绕通知
- 切面优先级
1、代理
代理模式
是一种结构型设计模式,它的核心思想是:在访问对象时,通过代理对象进行间接访问。
静态代理
静态代理
是指在编译时,代理类就已经存在,并且程序运行前,代理类的字节码已经生成。代理类和被代理类实现相同的接口,代理类内部调用被代理类的方法,同时在方法前后添加额外的逻辑。
静态代理的结构:
- 接口:定义代理类和被代理类都要实现的方法。
- 被代理类:实现接口,包含实际的业务逻辑。
- 代理类:也实现接口,在调用被代理类方法的前后加入额外逻辑。
- 缺点: 静态代理的一个主要缺点是,代理类与被代理类实现了相同的接口,当业务类(
被代理类
)变多时,你需要为每个类创建一个代理类,代码量大,且难以维护。
动态代理
动态代理相比静态代理更加灵活,它允许在运行时
动态生成代理对象,避免了手动编写代理类的繁琐。Java 提供了两种动态代理机制:
- JDK 动态代理: 适用于代理实现了接口的类(需要被代理的目标类必须
实现接口
,他会根据目标类的接口动态生成一个代理对象)。 -
- 这个代理类对象:是一个动态生成的类的实例,这个动态生成的类实现你提供的接口。
- CGLIB 动态代理: 适用于代理没有实现接口的类,通过字节码技术生成子类实现代理。
JDK动态代理介绍
Proxy.newProxyInstance()
方法是 JDK 动态代理的关键,用来生成代理对象。它在运行时创建一个代理它接受三个参数:-
- ClassLoader: 代理类的类加载器,一般使用目标对象的类加载器。
-
- Interfaces: 代理对象实现的接口列表,这些接口也必须是目标对象实现的接口。
-
- InvocationHandler: 代理对象的方法调用处理器。[实现具体要执行的非核心动作]
InvocationHandler
是 JDK 动态代理的核心。它用于拦截代理对象的方法调用,并在此基础上执行代理逻辑。通过实现 InvocationHandler 接口的 invoke() 方法,代理可以在方法调用之前、之后或替代原始方法逻辑。- invoke() 方法接收三个参数(invoke() 方法会通过反射调用目标对象的实际方法):
-
- proxy: 代理对象本身
-
- method: 被调用的方法对象(目标类的方法)
-
- args: 调用该方法时传递的参数。
2、AOP面向切面编程
AOP与OOP思维差异
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过分离横切关注点(Cross-Cutting Concerns)来增强代码的模块化。而OOP(Object-Oriented Programming,面向对象编程)则是一种通过类和对象来封装数据和行为的编程范式。
思维 | OOP(面向对象编程) | AOP(面向切面编程) |
---|---|---|
核心理念 | 通过类和对象封装数据和行为,实现代码复用、封装和多态等特性。 | 通过分离横切关注点,将与业务逻辑无关的代码(如日志、事务等)从主业务逻辑中剥离。 |
关注点 | 主要关注对象及其行为,强调如何设计对象以及对象之间的交互关系。 | 主要关注横切关注点,即那些分散在不同模块中的公共需求,如日志、权限、事务管理等。 |
代码组织 | OOP关注将相关的数据和行为放到同一个类中,使对象内部的实现细节对外部不可见。 | AOP关注如何将某些行为(如日志记录)提取到独立的模块(切面),通过切入点与业务逻辑无缝结合。 |
模块化 | 强调使用类来组织模块。类是最基本的模块化单位。 | 强调通过**切面(Aspect)**将横切关注点与业务逻辑分离,以增强模块化。 |
扩展方式 | 使用继承和实现接口来扩展功能。 | 使用切面和切入点动态注入功能,无需修改现有代码。 |
代码侵入性 | 如果需要引入日志、权限管理等,可能需要在类的多个方法中显式编写这些代码。 | 通过声明式的方式(如注解),将横切关注点注入业务逻辑,减少代码的侵入性。 |
适用场景 | 适合封装具有特定职责的对象或类,强调业务逻辑的实现。 | 适合处理分散的、重复的横切关注点,如日志、事务、异常处理等。 |
AOP核心名词理解
名词 | 含义说明 |
---|---|
横切关注点 | 系统中跨多个模块的需求,如日志或事务(非核心代码处理场景 )。 |
通知(增强) | 在特定连接点执行的增强代码,如前置通知或后置通知。 |
连接点 | 程序执行中的特定点,如方法调用。 |
切入点 | 匹配连接点的表达式,用于定义哪些方法/类需要增强。 |
切面 | 横切关注点的具体实现,包含通知和切入点。 |
目标对象 | 被增强(要被切入 )的对象。 |
代理 | 实际执行增强逻辑的对象,代替目标对象执行方法。 |
织入 | 将切面与目标对象绑定的过程,通常在运行时完成。 |
快速实现
spring-aop: 用于引入Spring的AOP功能,支持基于代理的AOP(JDK动态代理和CGLIB代理)。
spring-aspects: 用于引入对AspectJ的支持,提供编译时、加载时和注解式的AspectJ功能。
@Aspect: 表示这个类是一个切面类
@Before: 是 Spring AOP 中的一个通知类型,表示在目标方法执行之前要执行的增强逻辑。
切入点表达式: 用于定义在哪些连接点上应用增强逻辑。
步骤 | 关键点描述 |
---|---|
1. 引入依赖 | 在pom.xml 中加入spring-aop 、spring-aspects 依赖,确保项目支持AOP。 |
2. 编写目标对象 | 创建业务类或接口,定义需要被增强的业务逻辑。 |
3. 定义切面类 | 使用@Aspect 注解定义切面类,利用@Before 、@After 等通知注解添加增强逻辑,使用@Pointcut 定义切入点。 |
4. 启用AspectJ支持 | 在配置类中使用@EnableAspectJAutoProxy 注解,开启Spring AOP的注解支持。 |
5. 编写控制器或测试 | 创建控制器或其他测试类,调用目标类的方法,验证切面的增强逻辑。 |
6. 测试并验证 | 运行项目,通过访问控制器或测试类,检查AOP增强是否按预期工作。 |
3、获取通知细节信息
获取通知细节信息
指的是在通知方法(如@Before、@After等)中获取有关被增强的目标方法的信息,例如方法名、参数、目标对象、类名等。
JointPoint接口
需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参
。
// @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);
}
获取返回值
在返回通知中,通过@AfterReturning
注解的returning属性获取目标方法的返回值!
// @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);
}
异常对象捕捉
在异常通知中,通过@AfterThrowing
注解的throwing属性获取目标方法抛出的异常对象
// @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());
}
4、切点表达式
- 权限修饰符:public private 直接描述对应修饰符即可
- 方法返回值 int String void 直接描述返回值类型,其中execution(*) 表示 不考虑权限修饰符与方法返回值
- 指定包的地址
固定的包: com.atguigu.api | service | dao
单层的任意命名: com.atguigu.* = com.atguigu.api com.atguigu.dao * = 任意一层的任意命名
任意层任意命名: com.. = com.atguigu.api.erdaye com.a.a.a.a.a.a.a ..任意层,任意命名 用在包上!
注意: ..不能用作包开头 public int .. 错误语法 com..
找到任何包下: *..
- 指定类名称
固定名称: UserService
任意类名: *
部分任意: com..service.impl.*Impl
任意包任意类: *..*
- 指定方法名称:语法和类名一致
- 方法参数
具体值: (String,int) != (int,String) 没有参数 ()
模糊值: 任意参数 有 或者 没有 (..) ..任意参数的意识
部分具体和模糊:
第一个参数是字符串的方法 (String..)
最后一个参数是字符串 (..String)
字符串开头,int结尾 (String..int)
包含int类型(..int..)
切点表达式的复用
增强方法的切点表达式容易冗余,也不方便统一维护。我们可以将切点提取,在增强上进行引用
即可!
切点统一管理:将需要被引用的切点表达式方法统一放在类中管理
- 在需要使用表达式的地方调用即可
@Before(value = "com.atguigu.AtguiguPointCut.atguiguGlobalPointCut()")
@Component
public class AtguiguPointCut {
@Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
public void atguiguGlobalPointCut(){}
@Pointcut(value = "execution(public int *..Calculator.add(int,int))")
public void atguiguSecondPointCut(){}
@Pointcut(value = "execution(* *..*Service.*(..))")
public void transactionPointCut(){}
}
5、环绕通知
环绕通知(@Around
)是 Spring AOP 中的一种通知类型,允许你在目标方法执行的前后插入自定义的逻辑。它是最强大和灵活的通知类型,因为你可以控制目标方法的执行,也可以决定是否执行该方法,并可以修改方法的参数和返回值。
userServiceMethods()
方法定义了切入点,匹配 UserService 中的所有方法。logAround(ProceedingJoinPoint joinPoint)
方法是环绕通知。- 调用
joinPoint.proceed()
方法执行目标方法
@Aspect
@Component
public class LoggingAspect {
// 定义切入点,匹配 UserService 中的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 环绕通知
@Around("userServiceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) {
Object result = null; // 用于存储目标方法的返回值
try {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 获取方法参数
Object[] methodArgs = joinPoint.getArgs();
// 在方法执行前执行的逻辑
System.out.println("Logging: Before method: " + methodName);
System.out.println("Arguments: " + Arrays.toString(methodArgs));
// 调用目标方法
result = joinPoint.proceed(); // 执行目标方法
// 在方法执行后执行的逻辑
System.out.println("Logging: After method: " + methodName);
System.out.println("Result: " + result);
} catch (Throwable throwable) {
// 处理目标方法抛出的异常
System.err.println("Logging: Exception in method: " + joinPoint.getSignature().getName());
System.err.println("Exception message: " + throwable.getMessage());
// 重新抛出异常,便于后续处理
throw throwable;
}
return result; // 返回目标方法的返回值
}
}
6、切面优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用 @Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低