Spring AOP
Spring AOP是什么
Spring AOP 是AOP的一种实现方式。AOP 是一种编程思想,旨在通过将跨多个模块或层的横切关注点(如日志记录、事务管理等)与核心业务逻辑分离,来增强代码的重用性、可维护性和可扩展性。
AOP(面向切面编程)是OOP(面向对象编程)的一种补充,简单来说面向切面编程就是拦截指定的方法,不需要侵入业务代码,在不修改源代码的情况下对这个方法添加额外的功能,例如(日志记录,安全检查,缓存管理)
AOP特点
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
AOP实现原理
Spring AOP(面向切面编程)使用动态代理来实现横切关注点的处理。在Spring AOP中,动态代理是通过Java的反射机制来实现的。
Spring AOP提供了两种类型的动态代理:JDK动态代理和CGLIB动态代理。
-
JDK动态代理是基于接口的代理,它要求被代理的目标对象实现一个或多个接口。当我们使用JDK动态代理时,Spring AOP将为目标对象创建一个实现了代理接口的代理对象。这个代理对象可以将方法调用委托给目标对象,并在方法调用前后执行额外的逻辑。
下面是一个简单的示例,演示如何使用JDK动态代理来实现代理:
-
首先,创建一个接口,定义需要被代理的方法:
public interface UserService { void addUser(String username); }
-
接下来,创建一个实现InvocationHandler接口的代理类,用于处理对被代理方法的调用:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class UserServiceProxy implements InvocationHandler { private Object target; public UserServiceProxy(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("开始代理"); Object result = method.invoke(target, args); System.out.println("代理结束"); return result; } }
-
最后,创建一个测试类来演示动态代理的使用:
import java.lang.reflect.Proxy; public class Test { public static void main(String[] args) { // 创建被代理对象 UserService userService = new UserServiceImpl(); // 创建动态代理对象 UserServiceProxy proxy = new UserServiceProxy(userService); UserService userServiceProxy = (UserService) Proxy.newProxyInstance( userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), proxy); // 调用代理对象的方法 userServiceProxy.addUser("Alice"); } }
-
运行上述代码,输出如下:
开始代理
添加用户:Alice
代理结束
-
-
CGLIB动态代理是通过继承目标对象来创建代理对象。如果目标对象没有实现任何接口,Spring AOP将使用CGLIB动态代理。CGLIB动态代理不要求目标对象实现接口,而是通过继承目标对象并重写方法来实现代理。
以下是一个使用CGLIB动态代理实现的示例:
1.首先,确保你在项目的依赖中添加了CGLIB库的引用。
2.接下来,创建一个需要被代理的类:
public class UserService { public void addUser(String username) { System.out.println("添加用户:" + username); } }
3.然后,创建一个实现MethodInterceptor接口的拦截器类,用于处理对被代理方法的调用。
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class UserServiceInterceptor implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("开始代理"); Object result = methodProxy.invokeSuper(object, args); System.out.println("代理结束"); return result; } }
4.最后,创建一个测试类来使用CGLIB动态代理:
import net.sf.cglib.proxy.Enhancer; public class Test { public static void main(String[] args) { // 创建被代理对象 UserService userService = new UserService(); // 创建Enhancer对象 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new UserServiceInterceptor()); // 创建代理对象 UserService userServiceProxy = (UserService) enhancer.create(); // 调用代理对象的方法 userServiceProxy.addUser("Alice"); } }
5.运行上述代码,输出如下:
开始代理 添加用户:Alice 代理结束
可以看到,通过CGLIB动态代理,我们使用
Enhancer
类来创建代理对象,并指定了拦截器UserServiceInterceptor
,在拦截器中对被代理方法进行了额外的操作。需要注意的是,CGLIB动态代理生成的代理类是目标类的子类,因此要求目标类不能是被final修饰的类。同时,由于CGLIB动态代理是通过生成字节码实现的,所以生成的代理类可能会相对JDK动态代理略微复杂。
Spring AOP的动态代理是在运行时动态生成的,它基于切面配置中定义的切点和通知来确定在目标对象的方法执行前、后或环绕时执行哪些额外操作。通过使用动态代理,Spring AOP能够在不修改目标对象代码的情况下,将横切关注点(例如日志记录、性能监控等)与业务逻辑分离开来。
AOP织入时机
织入是一个AOP概念,指的是将切面(Aspect)应用到目标对象(Target Object)上的过程。
AOP织入时期包括如下几个阶段:
- 编译时织入:在源代码编译成字节码文件时,通过特定的编译器或预处理器将切面织入到字节码文件中。这种织入时期需要使用专门的工具或编译器支持,例如AspectJ编译器。编译时织入在目标对象最终生成的字节码中嵌入了切面逻辑。
- 类加载时织入:在目标对象的字节码文件加载到JVM中的过程中,通过字节码增强技术将切面织入到目标对象的字节码中。这种织入时期需要通过字节码增强框架,如ASM或Byte Buddy来实现。类加载时织入可以动态修改目标对象的字节码,实现切面的关联。
- 运行时织入:在目标对象的方法被调用时,通过代理机制将切面的通知织入到方法调用的前后。这种织入时期通过创建代理对象来拦截方法调用,并在适当的时机执行切面通知。
运行时织入是Spring AOP的实现方式,通过动态代理或者字节码生成来实现切面与目标对象的关联。
AOP相关术语
-
AOP核心概念
- Target(目标对象):代理的目标对象
- Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring,,这些点指的是方法,因为spring只支持方法类型的连接点
- Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
- Advice(通知/增强):所谓通知是指拦截Joinpoint之后所要做的事情就是通知
- Aspect(切面):是切入点和通知(引介)的结合
- Weaving (织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
-
AOP通知分类
- 前置通知(Before Advice):在目标方法调用之前执行的通知。前置通知可以用于在方法执行前进行一些准备工作,或者进行权限检查等操作。
- 后置通知(After Advice):在目标方法调用之后执行的通知。后置通知无论目标方法是正常返回还是抛出异常,都会执行。
- 返回通知(After Returning Advice):在目标方法正常返回后执行的通知。返回通知可以获取目标方法的返回值,并进行相应的处理。
- 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。异常通知可以捕获目标方法抛出的异常,并进行相应的处理。
- 环绕通知(Around Advice):在目标方法调用前后都执行的通知。环绕通知可以完全控制目标方法的执行过程,包括是否执行目标方法、在执行前后进行处理等。
AOP怎么实现
-
需要编写的内容
- 编写核心业务代码(目标类的目标方法)
- 编写切面类,切面类中有通知(增强功能方法)
- 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合
-
切点表达式的写法
语法:
execution([修饰符]返回值类型包名.类名.方法名(参数))
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用星号*代表任意
- 包名与类名之间一个点.代表当前包下的类,两个点…表示当前包及其子包下的类
- 参数列表可以使用两个点…表示任意个数,任意类型的参数列表
例如:
execution(public void com.iflytek.aop.Target.method()) execution(void com.iflytek.aop.Target.* ( ..)) execution(* com.iflytek.aop.*.*( ..)) execution(* com.iflytek.aop..*.* (..)) execution(* *..*.*(..))
-
通知的配置语法:
<!--配置文件方式--> <aop:通知类型 method="切面类中方法名" pointcut="切点表达式"></aop:通知类型>
//注解方式 @通知注解("切点表达式")
-
具体步骤
- 基于配置文件方式实现日志打印
(1)导入AOP相关坐标
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.8</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.8</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
(2)创建目标接口和目标类(内部有切点)
-
在com.cqgcxy.service包下创建目标接口PhoneService
package com.cqgcxy.service; public interface PhoneService { void select(); }
-
在com.cqgcxy.service.impl包下创建目标类PhoneServiceImpl
package com.cqgcxy.service.impl; import com.cqgcxy.service.PhoneService; public class PhoneServiceImpl implements PhoneService { public void select() { System.out.println("全世界都是华为手机!"); } }
(3)创建切面类(内部有增强方法)
- 在com.cqgcxy.aop包下创建切面类LogManager
package com.cqgcxy.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import java.time.LocalDateTime; public class LogManager { public Object printLog(ProceedingJoinPoint pjp) throws Throwable { Signature signature = pjp.getSignature(); System.out.println(LocalDateTime.now()+"==============>"+signature+"===============>开始执行!"); Object proceed = pjp.proceed(); System.out.println(LocalDateTime.now()+"==============>"+signature+"===============>结束执行!"); return proceed; } }
(4)将目标类和切面类的对象创建权交给spring
<bean id="logManager" class="com.cqgcxy.aop.LogManager"></bean> <bean id="phoneService" class="com.cqgcxy.service.impl.PhoneServiceImpl"></bean>
(5)在applicationContext.xml中配置织入关系
-
方式一:
<aop:config> <aop:aspect ref="logManager"> <aop:around method="printLog" pointcut="execution(* com.cqgcxy.service..*.*(..))"></aop:around> </aop:aspect> </aop:config>
-
方式二:
<aop:config> <aop:aspect ref="logManager"> <aop:pointcut id="logPointcut" expression="execution(* com.cqgcxy.service..*.*(..))"/> <aop:around method="printLog" pointcut-ref="logPointcut"></aop:around> </aop:aspect> </aop:config>
(6)测试代码
package com.cqgcxy.service.impl; import com.cqgcxy.service.PhoneService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class PhoneServiceImplTest { @Autowired private PhoneService phoneService; @Test public void select() { phoneService.select(); } }
2.基于注解方式实现日志打印
(1)创建目标接口和目标类(内部有切点)
-
在com.cqgcxy.service包下创建目标接口PhoneService
package com.cqgcxy.service; public interface PhoneService { void select(); }
-
在com.cqgcxy.service.impl包下创建目标类PhoneServiceImpl
(2)创建切面类(内部有增强方法),并配置织入关系
-
方式一:
-
方式二:
(3)将目标类和切面类的对象创建权交给spring,并在配置文件中开启组件扫描和AOP的自动代理
(4)测试
package com.cqgcxy.service.impl; import com.cqgcxy.service.PhoneService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class PhoneServiceImplTest { @Autowired private PhoneService phoneService; @Test public void select() { phoneService.select(); } }
3.执行结果