SpringFramework框架-AOP

AOP(面向切面编程)是一种编程范式,通过分离横切关注点来提升代码模块化,其核心机制基于动态代理,在运行时将切面逻辑织入目标方法的连接点,实现非侵入式的功能增强


动态代理

静态代理的实现方式先前已有涉及且较容易理解,此处我会补充一些动态代理的内容 。静态代理的目标对象在程序运行前就已经确定,而动态代理的目标对象在程序运行时创建且目标对象不固定


JDK动态代理

newProxyInstance()方法

newProxyInstance()方法是Proxy类下的方法,该类是完成代理的操作类,可以通过此方法为一个或多个接口动态生成实现类,该方法接受三个参数

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

loader:一个ClassLoader对象,用于定义和加载动态生成的代理类,通常使用与目标接口相同的类加载器

interfaces:一个Interface对象的数组,表示代理对象需要实现的接口,从而可以响应接口中定义的方法调用

h:一个InvocationHandler接口的实现类,作为代理对象的调用处理程序。对代理对象调用方法时,将对方法调用进行编码并将其转发到它的调用处理程序的invoke()方法

invoke()方法

invoke()方法存在于InvocationHandler接口的实现类,其接受三个参数

public Object invoke(Object proxy, Method method, Object[] args)

proxy:调用该方法的代理对象

method:要调用的目标对象的方法

args:要调用的方法的参数

invoke()方法中可以对目标对象的行为进行增强,但一般都会回到method.invoke(target,args),容易看出动态代理和静态代理的很大不同在于动态代理的代理对象在运行时被需要时才被创建,而且代理对象不直接调用目标对象,而是将调用请求转发给处理程序进行处理,此时处理程序可以对目标对象的行为进行增强

底层实现

执行newProxyInstance()方法时,程序会在运行时创建一个缓存类$Proxy0,该类继承于Proxy,也是客户端实际上操作的对象,当对$Proxy0对象调用接口声明的方法时,会回调父类的h属性的invoke()方法

protected InvocationHandler h;

该属性在执行newProxyInstance()方法时被赋为传入的InvocationHandler实现类也就是handler,所以调用了handlerinvoke()方法,实现了转发

$Proxy0大概长这样,test()是接口定义的方法,this是代理对象本身,呼应了重写invoke()时第一个参数proxy

public final void test() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

重写的invoke()的返回值承接了原始对象调用方法后的返回值


CGLIB动态代理

JDK动态代理要求目标类需实现某些接口,CGLIB代理不依赖接口进行代理而依赖继承机制实现,通过生成一个目标类的子类并覆盖需要代理的方法实现增强,final修饰的类不能通过CBLIB代理

方法拦截器

CGLIB动态代理中的方法拦截器类似于JDK动态代理中的InvocationHandler处理器,其作用是在调用目标类方法时对目标类的行为进行增强,方法拦截器需要实现MethodInterceptor该接口定义了一个intercept()方法

Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;

该方法接受四个参数,obj指向要代理的目标对象,method指向被拦截的方法,args指向被拦截方法的参数,proxy为方法代理,用于调用父类方法

该方法使用invokeSupera()达到对目标对象的调用

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//前置增强方法
Object result = proxy.invokeSuper(obj, args);
//后置增强方法
return result;
}

Enhancer ( 增强器 )

增强器用来动态生成子类代理对象,类似JDK代理创建缓存对象的过程,在创建代理对象前要先对Enhancer对象调用setSupoerclass()方法设置父类(目标类),调用setCallback()方法设置拦截器后才能使用create()方法生成代理对象

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setCallback(new NewInterceptor());
Test proxy = (Test) enhancer.create();
proxy.test();

底层实现

我们测试一个无参test()方法,执行 Enhancer.create() 方法时,程序会在运行时生成一个继承于目标类的缓存类 TestBean$$EnhancerByCGLIB$$5a33a704,该类是被代理类的子类,也是客户端实际上操作的对象。当对代理对象调用方法时,会回调设置的方法拦截器 MethodInterceptorintercept() 方法,要调用的方法和参数都被转发给了intercept() 方法,生成的缓存子类对应的方法类似如下

public final void test() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

if (var10000 != null) {
var10000.intercept(this,CGLIB$methodProxy$0.getMethod(),new Object[0],CGLIB$methodProxy$0);
} else {
super.methodName();
}
}

回调intercept()时传递的参数

image-20251226205329508

进入invokeSuper()

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
this.init();
FastClassInfo fci = this.fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}

this.fastClassInfo就是传入的MethodPeoxy形参字段,它的两个字段f1f2分别储存了目标类对象和代理缓存子类,它们都是FastClass类型,其存在是CGLIB为了避免反射调用,进行缓存直接调用以提高性能

image-20251226212839853

所以我的环境下所以最后执行的实际是

Bean.TestBean$$EnhancerByCGLIB$$5a33a704.invoke(16, Bean.TestBean$$EnhancerByCGLIB$$5a33a704, null)

FastClass类的invoke()方法与正常的不一样,这里调用了自己的一个编号为16的方法,对应的可能是CGLIB$test$0()这样格式的方法,该方法就是直接调用父类也就是目标对象的方法,完成了闭环

总的来说,代理对象调用方法时,会先回调拦截器,进行行为增强,然后再回到缓存子类直接调用父类的方法去调用目标方法


AOP基本概念

Joinpoint(连接点)

被拦截到的每个点,spring中指被拦截到的每一个方法,spring aop一个连接点即代表一个方法的执行

Pointcut(切入点)

对连接点进行拦截的定义(匹配规则定义 规定拦截哪些方法,对哪些方法进行处理),spring有专门的表达式语言定义

Advice(通知)

通知类型 描述
前置通知(前置增强) 执行方法前通知
返回通知(返回增强) 方法正常结束返回后的通知
异常抛出通知(异常抛出增强) 出现异常时的通知
最终通知 无论方法是否发生异常,均会执行该通知
环绕通知 包围一个连接点(join point)的通知,如方法调用。最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常

Aspect(切面)

切入点与通知的结合,决定了切面的定义,切入点定义了要拦截哪些类的哪些方法,通知则定义了拦截过方法后要做什么,切面则是横切关注点的抽象,与类相似,类是对物体特征的抽象,切面则是横切关注点抽象

Target(目标对象)

被代理的目标对象

Weave(织入)

将切面应用到目标对象并生成代理对象的这个过程即为织入

Introduction(引入)

在不修改原有应用程序代码的情况下,在程序运行期为类动态添加方法或者字段的过程称为引入


AOP实现

AOP需要引入依赖并在配置文件中添加命名空间

xmlns:aop="http://www.springframework.org/schema/aop"

http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

开启AOP注解支持

<aop:aspectj-autoproxy/>

注解实现

AOP切入点表达式

AOP切入点表达式用于匹配@Pointcut注解要拦截的方法,其语法主要由*.组成。表达式的第部分由指定的访问修饰符组成,有publicprivateprotected*四种类型;第二部分可以指定包位置,表示只拦截在该包下的方法,若最后为..代表也包括子包的方法,该部分若为空代表拦截范围为全局;第三部分是要拦截的类名;第四部分为该类的方法名;第五部分为传入该方法的参数

描述 示例
拦截所有公共方法 public *(..)
拦截任意setter方法 * set*(..)
指定某个包下的任意方法 * com.xxx.*.*(..)
指定某个包及其子包下的任意方法 * com.xxx..*.*(..)

具体注解

@Aspect注解标记在一个类上,表示这个类是一个切面

@Aspect
@Component
public class LogAspect {

}

@Pointcut注解标记在一个方法上,该方法体必须为空。该方法具有value属性,表示要拦截的方法,该属性使用AOP切入点表达式进行指定

@Pointcut("execution(* com.example.service.*.*(..))")
public void allServiceMethods() {}

下面的注解都声明了一个通知类类型,它们都具有value属性,一般指向@Pointcut注解记在的方法

@Before注解声明前置通知,目标类的方法执行前执行该通知

@Before("loginMethod()")
public void beforeLogin() {
System.out.println("用户尝试登录...");
}

@AfterReturning注解声明返回通知,目标类的方法无异常执行后执该通知

@AfterReturning(pointcut = "createOrderMethod()", returning = "orderId")
public void afterCreateOrder(Long orderId) {
System.out.println("订单创建成功,订单号:" + orderId);
}

@After注解声明最终通知,目标类的方法执行后不论有无异常都会执行该通知

@After("uploadMethod()")
public void afterUpload() {
System.out.println("文件上传操作结束,清理临时资源");
}

@AfterThrowing注解声明异常通知,目标类的方法执行异常时执行该通知

@AfterThrowing(pointcut = "payMethod()", throwing = "ex")
public void afterPaymentFailed(Exception ex) {
System.out.println("支付失败,异常信息:" + ex.getMessage());
}

@Around注解环绕通知,目标类的方法执行前后都可通过环绕通知定义响应内容,该通知需要通过显示调用传入的ProceedingJoinPoint对象的proceed(),这相当于明确告诉程序何处为何种通知,proceed()本身不执行任何功能

@Around("controllerMethods()")
public Object aroundController(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("请求开始处理");

Object result = pjp.proceed();

System.out.println("请求处理完成");
return result;
}

@AfterReturning@AfterThrowing搭配别的属性时一般使用pointcut指定要拦截的方法

XML实现

通过配置文件实现AOP不需要修改原先的代码结构,只需要把注解对应修改到配置文件上即可,以先前的例子说,配置文件修改为如下,再删除原切面的注解即可

<aop:config>
<!--aop切面-->
<aop:aspect ref="LogAspect">
<!--定义aop 切入点 -->
<aop:pointcut id="allServiceMethods" expression="execution(* com.example.service.*.*(..))"/>
<!-- 配置前置通知 指定前置通知方法名 并引用切入点定义 -->
<aop:before method="beforeLogin" pointcut-ref="allServiceMethods"/>
<!-- 配置返回通知 指定返回通知方法名 并引用切入点定义 -->
<aop:after-returning method="afterCreateOrder" pointcut-ref="allServiceMethods"/>
<!-- 配置异常通知 指定异常通知方法名 并引用切入点定义 -->
<aop:after-throwing method="afterPaymentFailed" throwing="ex" pointcut-ref="allServiceMethods"/>
<!--配置最终通知 指定最终通知方法名 并引用切入点定义 -->
<aop:after method="afterUpload" pointcut-ref="allServiceMethods"/>
<!--配置环绕通知 指定环绕通知方法名,并引用切入点定义-->
<aop:around method="aroundController" pointcut-ref="allServiceMethods"/>
</aop:aspect>
</aop:config>

以上,容易发现AOP就是更加灵活且多样的动态代理模式,目的还是在于目标行为的增强


SpringFramework框架-Task

Spring Task 是 Spring 框架提供的轻量级定时任务调度组件,用于在 Spring 应用中简单、便捷地实现定时任务。

XML配置实现

配置文件添加命名空间

xmlns:task="http://www.springframework.org/schema/task"

http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd

设置定时任务方法

@Component
public class ShortTask {
public void run() {
System.out.println("定时任务执行:" + System.currentTimeMillis());
}
}

设置配置文件

<task:scheduled-tasks>
<!-- 定时任务1 -->
<task:scheduled ref="shortTask" method="run" cron="0/2 * * * * ?"/>
</task:scheduled-tasks>

测试类中,只要获得了bean对象就会自动执行方法


注解实现

@Scheduled注解标记于需要设置定时任务的方法上,该注解具有cron属性,用于写入cron表达式设置相关定时规则

@Component
public class ShortTask {
@Scheduled(cron = "0/2 * * * * ?")
public void run() {
System.out.println("定时任务执行:" + System.currentTimeMillis());
}
}

配置文件设置定时任务驱动,用于识别@Scheduled注解

<task:annotation-driven/>

Cron表达式

cron表达式的格式为秒 分 时 日 月 周 年(可选)

各字段取值范围和特殊字符

字段 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W
1-12 或 JAN-DEC , - * /
0-7 或 SUN-SAT(0和7都是周日) , - * ? / L #
年(可选) 1970-2099 , - * /

特殊字符含义

字符 含义 示例 说明
* 所有值 * * * * * ? 每秒执行
? 不指定值 0 0 10 * * ? 每天10点,不关心周几
- 区间 0 0 10-12 * * ? 10点、11点、12点
, 多个值 0 0 10,14,18 * * ? 10点、14点、18点
/ 步长 0 0/5 * * * ? 每5分钟
L 最后 0 0 L * * ? 每月最后一天
W 工作日 0 0 0 15W * ? 离15号最近的工作日
# 第几个 0 0 0 ? * 2#3 每月第3个周二