目录
什么是Spring AOP
Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架提供的一种面向切面的编程方式。它通过在程序运行期间动态地将代码切入到类的指定方法、指定位置上,实现了对程序功能的增强。AOP的编程思想就是把很多类对象中的横切问题从业务中分离出来,从而达到解耦的目的增加代码的重用性,提高开发效率。
Spring AOP中的主要概念
术语
- aspect:切面,切面是一组横切关注点的集合,它定义了在哪里以及何时执行这些横切关注点。在 Spring AOP 中,切面通常由切点和通知组成
- pointcut:切点,切点是指那些需要被拦截并添加增强的方法。通过切点表达式来匹配要拦截的方法
- joinpoint:连接点,连接点是程序执行过程中能够插入切面的点,比如方法调用、异常处理等
- advice:通知是切面的具体实现,它定义了在切点处执行的操作,如在方法执行前、执行后、抛出异常时等
- weaving:增强是将切面与目标对象织入在一起创建新的代理对象的过程。增强可以是编译时的、类加载时的、或者运行时的
- taeget:目标对象,通知织入的目标类
- aop proxy:代理对象,即增强后产生的对象
通知的种类
- Before advice:前置通知,即在目标方法调用之前执行。注意:无论方法是否遇到异常都会执行。
- After return advice:后置通知,在目标方法执行之后执行,前提是目标方法没有遇到异常,如果有异常则不执行。
- After throwing advice:异常通知,在目标方法抛出异常是执行,可以获取异常信息
- After finally advice:最终通知,在目标方法执行后执行,无论是否是异常执行
- Around advice:环绕通知,最前大的通知类,可以控制目标方法的执行(通过调用ProceedingJointPoint.proceed()),可以在目标执行全过程中进行执行
举例
1.定义一个切面类Aspect
即在声明的类中添加@Component、@Aspect两个注解,Springboot中要引入spring-boot-stater-aop依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Component
@Aspect
public class MyAdvice {
}
2.定义切点PointCut
定义切点,并定义切点在哪些地方执行,采用@Pointcut注解完成,如Pointcut(execution(public * com.xxx.xxx.*.*(..))。
规则:修饰符(可以不写,但不能用*)+返回类型+那些包下的类需要增强+哪些方法+方法参数
“*”表示不限,“..”两个点代表参数不限
@Component
@Aspect
public class MyAdvice {
private Logger logger = Logger.getLogger(MyAdvice.class.getName());
//定义pointcut,增强controller包下的所有类
@Pointcut(value = "execution( * com.itheima.controller.*.*(..))")
public void mypointcut(){}
}
3.定义Advice通知
利用通知的5种类型的注解@Before、@After、@AfterReturning、@AfterThrowing、@Around来完成在某些切点的增强动作,@Before("mypointcut()"),mypointcut为第二步定义的切点。
@Component
@Aspect
public class MyAdvice {
private Logger logger = Logger.getLogger(MyAdvice.class.getName());
//定义pointcut
@Pointcut(value = "execution( * com.itheima.controller.*.*(..))")
public void mypointcut(){}
//定义advice通知,ProceedingJoinPoint 只能用于环绕通知,因为ProceedingJoinPoint 暴露了proceed方法
@Around(value = "mypointcut()")
public Object myLogger(ProceedingJoinPoint pjp) throws Throwable {
//获取增强的类名
String className=pjp.getTarget().getClass().getName();
//获取增强方法名
String methodName = pjp.getSignature().getName();
ObjectMapper mapper=new ObjectMapper();
//获取参数
Object[] array = pjp.getArgs();
logger.info("调用前,类名:"+className+" 方法名:"+methodName+" 参数:"+ mapper.writeValueAsString(array));
//执行增强的方法
Object obj = pjp.proceed();
logger.info("调用后,类名:"+className+" 方法名:"+methodName+" 返回值:"+ mapper.writeValueAsString(obj));
return obj;
}
}
controller包下的类的功能
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello1")
public String hello1(){
System.out.println("hello");
return "hello";
}
@GetMapping("/hello2")
public String hello2(@RequestParam("name") String name,@RequestParam("age") Integer age){
System.out.println("name="+name+" age"+age);
return "name="+name+" age"+age;
}
}
在浏览器分别请求这两个接口,查看控制台
2024-04-26 14:09:06.627 INFO 52460 --- [nio-8080-exec-4] com.itheima.AOP.MyAdvice : 调用前,类名:com.itheima.controller.TestController 方法名:hello1 参数:[]
hello
2024-04-26 14:09:06.636 INFO 52460 --- [nio-8080-exec-4] com.itheima.AOP.MyAdvice : 调用后,类名:com.itheima.controller.TestController 方法名:hello1 返回值:"hello"
2024-04-26 14:09:22.246 INFO 52460 --- [nio-8080-exec-7] com.itheima.AOP.MyAdvice : 调用前,类名:com.itheima.controller.TestController 方法名:hello2 参数:["zhangsan",19]
name=zhangsan age19
2024-04-26 14:09:22.246 INFO 52460 --- [nio-8080-exec-7] com.itheima.AOP.MyAdvice : 调用后,类名:com.itheima.controller.TestController 方法名:hello2 返回值:"name=zhangsan age19"
Spring AOP是如何实现的
Spring的AOP实现是通过动态代理实现的。如果我们为Spring的一个bean配置了AOP切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。
而Spring的AOP使用了两种动态代理,分别是
- JDK的动态代理;
- CGLib的动态代理。
JDK动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。
JDK实现动态代理需要两个组件
- 第一个是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。
- 第二个组件是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类对象实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。
原理
JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类
优点
- JDK动态代理是JDK原生的,不需要任何依赖即可使用
- 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快
缺点
- 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理
- JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入
- JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低
CGLib动态代理
JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。
若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。
CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。
原理
CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(切面)织入到方法中,对方法进行了增强。
通过字节码操作生成的代理类,和我们自己编写并编译后的类没有什么区别。
优点
- 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类
- CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中所有能够被子类重写的方法进行代理
- CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理
缺点
- 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理
- 由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法
- CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢
举例
首先,我们需要一个接口和一个实现类来演示代理
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println(".............新增用户信息.........");
}
}
JDK动态代理示例
/**
* JDK 动态代理示例
*/
public class JDKDynamicProxyExample {
/**
* 主函数入口
* @param args 命令行参数
*/
public static void main(String[] args) {
// 创建实现UserService接口的具体对象
UserServiceImpl userService = new UserServiceImpl();
// 通过Proxy生成一个动态代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
UserServiceImpl.class.getClassLoader(), // 使用目标对象的类加载器
UserServiceImpl.class.getInterfaces(), // 获取目标对象实现的接口列表
new MyInvocationHandler(userService) // 设置InvocationHandler处理代理对象的方法调用
);
// 调用代理对象的方法,此时会触发MyInvocationHandler的invoke方法
proxy.save();
}
/**
* 自定义的InvocationHandler,实现对方法调用的拦截
*/
static class MyInvocationHandler implements InvocationHandler {
private final Object target;
/**
* 构造函数
* @param target 被代理的对象
*/
public MyInvocationHandler(Object target) {
this.target = target;
}
/**
* 处理代理对象的方法调用
* @param proxy 代理对象
* @param method 被调用的方法
* @param args 方法调用的参数
* @return 方法的返回值
* @throws Throwable 方法调用中产生的异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法调用前的逻辑
System.out.println("Before method call...");
// 调用目标对象的相应方法
Object result = method.invoke(target, args);
// 方法调用后的逻辑
System.out.println("After method call...");
return result;
}
}
}
CGLIB动态代理示例
使用前先导入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
/**
* CGLIB代理示例类
*/
public class CGLIBProxyExample {
/**
* 主函数,演示如何使用CGLIB创建一个动态代理。
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 创建Enhancer对象,并设置基础信息
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理对象
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
// 调用代理对象的方法
proxy.save();
}
/**
* 自定义方法拦截器,实现MethodInterceptor接口。
* 用于在方法执行前后添加日志或其他逻辑。
*/
static class MyMethodInterceptor implements MethodInterceptor {
/**
* 当方法被调用时,此方法会拦截方法的执行,并允许在执行前后添加自定义逻辑。
*
* @param obj 被代理的对象
* @param method 被调用的方法
* @param args 方法调用时传入的参数
* @param proxy 方法调用的代理对象
* @return 方法执行的结果
* @throws Throwable 如果方法执行中出现异常,则抛出
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 方法执行前的逻辑
System.out.println("CGLIB Before method call...");
// 调用真实对象的方法
Object result = proxy.invokeSuper(obj, args);
// 方法执行后的逻辑
System.out.println("CGLIB After method call...");
return result;
}
}
}
注意:Java 9 引入了模块化系统,其中模块默认是封闭的,即模块之间不能访问对方的内部。在这种情况下,cglib
库无法访问 java.lang.reflect.AccessibleObject
中的 setAccessible
方法,导致无法生成代理类而抛出异常。
要解决这个问题,可以在运行时通过添加 --add-opens
参数来打开 java.base
模块中 java.lang.reflect
包的访问权限。
在VMOption中添加以下内容:
--add-opens java.base/java.lang=ALL-UNNAMED
运行