Spring AOP
使用动态代理作为其主要机制来实现面向切面的编程。这种机制允许Spring
在运行时动态地创建代理对象,这些代理对象包装了目标对象(即业务组件),以便在调用目标对象的方法前后插入额外的行为(如安全检查、事务管理、日志记录等)。
Spring AOP
默认使用JDK
的动态代理。JDK
动态代理通过反射机制,为接口创建一个代理对象,这个代理对象会拦截对目标接口方法的所有调用。Spring AOP
会退回到使用CGLIB
库生成目标类的子类。CGLIB
(Code Generation Library
)是一个强大的高性能代码生成库,它在运行时扩展了Java
类,并在子类中覆盖了方法来实现方法拦截。无论使用哪种代理方式,目的都是在不改变原有业务逻辑代码的基础上,通过切面定义的通知在方法执行的不同阶段插入附加行为。
Advice
)和一个或多个切点(Pointcut
),用于定义在何处以及何时执行这些通知。Spring AOP
限定这些位置为方法的调用。简而言之,连接点就是能够插入切面通知的点。Before advice
):在方法执行之前执行。After advice
):在方法执行后执行,无论其结果如何。After-returning advice
):在方法成功执行之后执行。After-throwing advice
):在方法抛出异常后执行。Around advice
):在方法执行之前和之后执行,提供对方法调用的全面控制。AOP
框架创建的对象,用于实现切面契约(由通知和切点定义)。在Spring AOP
中,AOP
代理可以是JDK
动态代理或CGLIB
代理。Introduction interfaces
)实现的,AOP
框架会为目标对象创建一个代理,该代理实现这些接口。如果还是觉得抽象,我们再举一个电影制作的例子来类比
AOP
中,这些“特效”就是切面,它们可以被应用到程序的多个部分,而不需要改变实际的场景(或代码)。AOP
中,这些“指令”就是通知,指定了切面(特效)应该在连接点(特定的代码执行时刻)之前、之后或周围执行。AOP
代理就像是特效团队提供的一个虚拟的、可控制特效的场景副本。这个副本在观众看来与原场景无异,但实际上它能在导演需要的时刻自动添加特效。在编程中,代理是一个被AOP
框架自动创建的对象,它包装了目标对象,确保了通知(特效指令)在正确的时间被执行。AOP
中,引入允许我们向现有的类添加新的方法或属性,这就像是在不改变原始脚本的情况下扩展电影的内容。 Spring
提供了丰富的AOP
支持,可以通过XML
配置来定义切面、通知(advice
)和切点(pointcuts
)。这样可以在不修改源代码的情况下增加额外的行为(如日志、事务管理等)
实现步骤:
pom.xml
中添加Spring
框架和AOP
相关的依赖。applicationContext.xml
中配置切面和业务bean
,以及AOP
相关的标签。在pom.xml
文件中,添加以下依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
首先,我们定义一个业务逻辑接口MyService
和它的实现MyServiceImpl
。
MyService.java:
package com.example.demo.aop;
public interface MyService {
String performAction(String input) throws Exception;
}
MyServiceImpl.java:
package com.example.demo.aop;
public class MyServiceImpl implements MyService {
@Override
public String performAction(String action) throws Exception {
System.out.println("Performing action in MyService: " + action);
if ("throw".equals(action)) {
throw new Exception("Exception from MyService");
}
return "Action performed: " + action;
}
}
接下来,我们定义一个切面类MyAspect
,这个类将包含一个前置通知(advice
),它在MyService
的performAction
方法执行之前执行。
MyAspect.java:
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
// 前置通知
public void beforeAdvice() {
System.out.println("Before advice is running!");
}
// 后置通知
public void afterAdvice() {
System.out.println("After advice is running!");
}
// 返回后通知
public void afterReturningAdvice(Object retVal) {
System.out.println("After returning advice is running! Return value: " + retVal);
}
// 异常后通知
public void afterThrowingAdvice(Throwable ex) {
System.out.println("After throwing advice is running! Exception: " + ex.getMessage());
}
// 环绕通知
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around advice: Before method execution");
Object result = null;
try {
result = joinPoint.proceed();
} finally {
System.out.println("Around advice: After method execution");
}
return result;
}
}
最后,我们需要在Spring
的配置文件applicationContext.xml
中配置上述bean
以及AOP
的相关内容。
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
上面这是XML文件的头部声明,它定义了文件的版本和编码类型,同时引入了Spring beans 和 AOP 的命名空间。
通过这些命名空间,我们可以在XML中使用<bean>和<aop:*>标签。
-->
<!-- Bean definitions -->
<bean id="myService" class="com.example.demo.aop.MyServiceImpl"/>
<bean id="myAspect" class="com.example.demo.aop.MyAspect"/>
<!-- AOP配置 -->
<aop:config>
<!-- 定义切面及其通知 -->
<aop:aspect id="myAspectRef" ref="myAspect">
<!-- 定义切点,指定通知应该在哪些方法执行时触发 -->
<aop:pointcut id="serviceOperation" expression="execution(* com.example.demo.aop.MyService.performAction(..))"/>
<!-- 应用前置通知,指定方法执行前的操作 -->
<aop:before method="beforeAdvice" pointcut-ref="serviceOperation"/>
<!-- 应用后置通知,指定方法执行后的操作,不论方法执行成功还是抛出异常 -->
<aop:after method="afterAdvice" pointcut-ref="serviceOperation"/>
<!-- 应用返回后通知,指定方法成功执行并返回后的操作 -->
<aop:after-returning method="afterReturningAdvice" pointcut-ref="serviceOperation" returning="retVal"/>
<!-- 应用异常后通知,指定方法抛出异常后的操作 -->
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="serviceOperation" throwing="ex"/>
<!-- 应用环绕通知,提供方法执行前后的完全控制 -->
<aop:around method="aroundAdvice" pointcut-ref="serviceOperation"/>
</aop:aspect>
</aop:config>
</beans>
bean
,指向MyServiceImpl
类的实例。bean
,指向MyAspect
类的实例。<aop:config>
:这是AOP
配置的根元素,所有的AOP
配置,包括切面定义、切点和通知方法等,都需要在此元素内部定义。<aop:aspect>
元素定义,它包含了一系列通知(advice
)和一个或多个切点(pointcut
)。这个元素将切面类(包含通知逻辑的类)与具体的操作(如何、何时对目标对象进行增强)关联起来。<aop:pointcut>
元素定义,切点通过表达式来指定,当需要精确控制哪些方法执行时会触发通知时,就需要定义切点。切点表达式可以非常精确地指定方法,例如通过方法名称、参数类型、注解等。expression
定义了切点的表达式,指明了切点的匹配规则。这里的表达式execution(* com.example.demo.aop.MyService.performAction(..))
意味着切点匹配MyService
接口中performAction
方法的执行,切点用于指定在哪些连接点(Join Point
,例如方法调用)上应用通知。关于解析表达式execution(* com.example.demo.aop.MyService.performAction(..))
Pointcut
)的表达式来识别和匹配的,execution(* com.example.demo.aop.MyService.performAction(..))
表达式定义了一个切点,它指定了一个明确的连接点集合——即MyService
接口的performAction
方法的所有调用。这个例子中,MyService
接口的performAction
方法的调用就是潜在的连接点。每次performAction
方法被调用时,就达到了一个连接点。这个连接点就是这里通知应用的时机。AOP
通过在特定时机执行的操作来增强方法的执行。method
属性指明当切点匹配时应该执行的切面的方法名,pointcut-ref
引用了上面定义的切点。比如这里的beforeAdvice
是在目标方法performAction
执行之前被调用的方法。这意味着每当MyService.performAction(..)
方法被调用时,beforeAdvice
方法将首先被执行。 总结为一句话:Spring AOP
通过在切面中定义规则(切点)来指定何时(连接点)以及如何(通知)增强特定方法,实现代码的模块化和关注点分离,无需修改原有业务逻辑。
通过这种方式,Spring AOP
允许定义在特定方法执行前、执行后、环绕执行等时机插入自定义逻辑,而无需修改原有业务逻辑代码。这是实现关注点分离的一种强大机制,特别是对于跨越应用程序多个部分的横切关注点(如日志、事务管理等)。
注意,如果<aop:config>
设置为
<aop:config proxy-target-class="true">
<!-- 其他配置不变 -->
</aop:config>
设置proxy-target-class="true"
会使Spring AOP
优先使用CGLIB
代理,即使目标对象实现了接口。默认情况下,不需要设置proxy-target-class
属性,或者将其设置为false
,则是使用JDK
动态代理。
主程序:
DemoApplication.java:
package com.example.demo;
import com.example.demo.aop.MyService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService myService = (MyService) context.getBean("myService");
try {
System.out.println(myService.performAction("normal"));
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("=======================");
try {
System.out.println(myService.performAction("throw"));
} catch (Exception e) {
System.out.println("Exception caught in main: " + e.getMessage());
}
context.close();
}
}
运行结果:
通过结合动态代理技术和这些AOP
概念,Spring AOP
能够以非侵入式的方式为应用程序提供横切关注点的支持,这样开发者就可以将这些关注点模块化,并保持业务逻辑组件的聚焦和简洁。
如果对动态代理感兴趣可以再调试看看,这里是JDK
动态代理是因为public class MyServiceImpl implements MyService
实现了接口,调试如下:
简单说一下这里能看到的关键类和接口
Spring AOP
用来创建代理对象的工厂类。它可以根据目标对象是否实现接口来决定使用JDK
动态代理还是CGLIB
代理。JdkDynamicAopProxy
(用于JDK
动态代理)和CglibAopProxy
(用于CGLIB
代理)。AopProxy
接口,使用JDK
动态代理技术创建代理。它实现了InvocationHandler
接口,拦截对代理对象的所有方法调用。AopProxy
接口,但使用CGLIB
库来创建代理对象。对于没有实现接口的类,Spring
会选择这种方式来创建代理。如果大家想深入了解Spring AOP
的源码,可以直接查看JdkDynamicAopProxy
和CglibAopProxy
这两个类的实现。这里不是本篇重点,简单提一下:
比如在JdkDynamicAopProxy
中看到动态代理的实现:
JdkDynamicAopProxy
类实现了InvocationHandler
接口,这是JDK
动态代理的核心。在其invoke
方法中,会有逻辑判断是否需要对调用进行拦截,并在调用前后应用相应的通知。ProxyFactory
通过调用createAopProxy()
方法时完成的,这个方法会根据配置返回JdkDynamicAopProxy
或CglibAopProxy
的实例。ProxyFactory
获取代理对象,并通过这个代理对象调用目标方法。代理对象在内部使用JdkDynamicAopProxy
或CglibAopProxy
来拦截这些调用,并根据AOP
配置执行通知。通过ProxyFactory
获取代理对象的过程,通常在Spring
的配置和使用中是隐式完成的,特别是在使用Spring
容器管理AOP
时。这一过程不需要开发者直接调用ProxyFactory
类。当Spring
配置中定义了一个bean
,并对其应用了切面,Spring
容器会自动处理代理的创建和应用通知的过程。这是通过Spring
的后处理器和AOP
命名空间的支持实现的,开发者通常只需声明式地配置切面和通知即可。如果想看到CGLIB
代理,这里有2
种方法
第1
种方法是去掉MyServiceImpl
实现的MyService
接口,然后把主程序和expression
表达式对应的地方改成MyServiceImpl
。第2
种方法就是Spring
配置文件中显式设置<aop:config>
标签的proxy-target-class="true"
属性来实现这一点。如下:
<aop:config proxy-target-class="true">
<!-- 其他配置保持不变 -->
</aop:config>
调试如下:
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。