一、什么是AOP
全称Aspect Oriented Programming,即面向切面编程,AOP是Spring框架的第二大核心,第一大为IOC。什么是面向切面编程?切面就是指某一类特定的问题,所以AOP也可以称为面向特定方法编程。例如对异常的统一处理,简单来说,AOP是一种思想,是对某一类问题的集中处理。AOP的优势在于程序运行期间在不修改源码的基础上对已有的方法进行增强(无侵入性)
二、什么是Spring AOP
AOP是一种思想,它的实现方法有很多,有Spring AOP,也有AspectJ,CGLIB等,Spring AOP只是其中的一种实现方式。
三、Spring AOP入门
首先在pom文件中引入依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写AOP程序
@Slf4j
@Aspect
@Component
public class TimeAspect {@Around("execution(* com.example.bookManagement.Controller.*.*(..))")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//记录开始时间long startTime = System.currentTimeMillis();//执行原始方法Object result = joinPoint.proceed();//记录方法执行时间long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗时: {} ms", elapsedTime);return result;}
}
运行程序,观察Controller方法运行时间:
对程序进行简单的讲解:
@Aspect:标识这是一个切面类。
@Around:环绕通知,在目标方法的前后都会被执行,后面的表达式表示对哪些方法进行增强。
ProceedingJoinPoint.proceed()让原方法执行。
四、Spring AOP详解
1.Spring AOP核心
1>切点(Pointcut)
切点的作用是提供一组规则,告诉程序对哪些方法进行功能增强
@Around("execution(* com.example.bookManagement.Controller.*.*(..))")
什么表达式中的"execution(* com.example.bookManagement.Controller.*.*(..))"就是切点表达式
2>.连接点(Join Point)
满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法 。
例如:"execution(* com.example.bookManagement.Controller路径下的方法都是连接点。
3>.通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能。
比如例子中的记录业务方法的耗时时间,就是通知。
long startTime = System.currentTimeMillis();//执行原始方法Object result = joinPoint.proceed();//记录方法执行时间long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗时: {} ms", elapsedTime);
4>.切面(Aspect)
切面=切点+通知
通过切面能够描述当前AOP程序需要针对哪些方法,在什么时候执行什么样的操作,切面即包含了通知逻辑的定义,也包含了连接点的定义。即例子中的如下代码:
@Around("execution(* com.example.bookManagement.Controller.*.*(..))")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//记录开始时间long startTime = System.currentTimeMillis();//执行原始方法Object result = joinPoint.proceed();//记录方法执行时间long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗时: {} ms", elapsedTime);return result;}
切面所在的类,我们一般称为切面类(被@Aspect标注的类)
2.通知类型
上述的@Around就是其中一类的通知类型。Spring中的AOP通知类型有以下几类:
@Around:环绕通知,此注解标注的通知方法在目标方法前后都被执行
@Before:前置通知,此注解标注的方法在目标方法前被执行。
@After:后置通知,此注解标注的方法在目标方法后被执行,无论是否有异常都会执行。
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
@AfterThrowing:异常后通知,此注解的通知方法发生异常后执行。
下面是代码案例:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Slf4j
@Aspect
@Component
public class AspectDemo {//前置通知@Before("execution(* com.example.aop.Controller.*.*(..))")public void before() {log.info("执行Before方法");}//后置通知@After("execution(* com.example.aop.Controller.*.*(..))")public void after() {log.info("执行After方法");}//返回后通知@AfterReturning("execution(* com.example.aop.Controller.*.*(..))")public void afterReturning() {log.info("执行AfterReturn 方法");}//抛出异常后通知@AfterThrowing("execution(* com.example.aop.Controller.*.*(..))")public void afterThrowing() {log.info("执行AfterThrowing方法");}//环绕通知@Around("execution(* com.example.aop.Controller.*.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around方法开始执行");Object result = joinPoint.proceed();log.info("Around方法执行");return result;}
}
添加以下测试程序:
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String test1() {return "test1";}@RequestMapping("/t2")public String test2() {int a = 10/0;return "test2";}
}
运行t1:
可以看出执行顺序如下:
运行t2,观察异常情况:
程序发生异常情况下:
@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
@Around第一个方法执行了,第二个没有执行。
3.@PointCut
什么代码存在一个问题,就是存在大量的切点表达式execution(* com.example.aop.Controller.*.*(..)),Spring提供了@PointCut注解,把公共的切点表达式提取出来,需要时引入该切点表达式即可。
上述代码可以改为:
@Slf4j
@Aspect
@Component
public class AspectDemo {@Pointcut("execution(* com.example.aop.Controller.*.*(..))")public void pointcut() {}//前置通知@Before("pointcut()")public void before() {log.info("执行Before方法");}//后置通知@After("pointcut()")public void after() {log.info("执行After方法");}//返回后通知@AfterReturning("pointcut()")public void afterReturning() {log.info("执行AfterReturn 方法");}//抛出异常后通知@AfterThrowing("pointcut()")public void afterThrowing() {log.info("执行AfterThrowing方法");}//环绕通知@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around方法开始执行");Object result = joinPoint.proceed();log.info("Around方法执行");return result;}
}
但是这种方式只适用于当前切面,如果当其他切面要使用时,就需要将private改为public,而且引用方式改为:全限定类名.方法名()
例如:
@Slf4j
@Aspect
@Component
public class AspectDemo2 {@Before("com.example.aop.AspectDemo.AspectDemo.pointcut()")public void before() {log.info("执行before方法:Aspect Demo2");}
}
4.切面优先级@Order
当我们在一个项目中,定义了多个切面类,而且这些切面类的多个切入点都匹配了同一个目标方法,当目标方法运行时,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样子的呢。
这里直接给出结果:
存在多个切面类时,默认按照切面类的类名字母排序:
@Before:字母排名靠前的先执行。
@After:字母排名靠前的后执行。
这种方式并不方便管理,因为类名往往是不考虑首字母的。
Spring提供了一个全新的注解,来控制这些切面通知的执行顺序:@Order
@Order(数字):
@Before:数字小的先执行;
@After:数字大的先执行。
5.切点表达式
切点表达式常见的有两种表达式
1.execution(......):根据方法的签名来匹配。
2.@annotation(.....):根据注解匹配。
1.execution表达式
最常见的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中访问修饰符和异常可以省略。
其中:*表示任意一层/一个包/参数,..表示任意多个包或者参数。
2.@annotation注解
相较于execution表达式,@annotation注解适用于多个无规则的方法,比如TestController中的t1和User Controller中的t2.。
实现步骤:
1.编写自定义注解。
2.使用@annotation表达式来描述切点。
3.在连接点的方法上添加自定义注解。
例子:
先定义Controller:
@RequestMapping("/user")
public class UserController {@RequestMapping("/u1")public String u1() {return "u1";}@RequestMapping("/u2")public String u2() {return "u2";}
}
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String test1() {return "test1";}@RequestMapping("/t2")public String test2() {int a = 10/0;return "test2";}
}
创建自定义注解类@MyAspect
package com.example.aop.MyAspect;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
解释:@Target标识了Annotation所修饰的对象范围,即注解可以用在什么地方
@Retention标明注解的生命周期。
定义切面类
使用@annotation切点表达式定义切点,只对@MyAspect生效
@Slf4j
@Component
@Aspect
public class MyAspectDemo {@Before("@annotation(com.example.aop.MyAspect.MyAspect)")public void before() {log.info("MyAspect->before method");}@After("@annotation(com.example.aop.MyAspect.MyAspect)")public void after() {log.info("MyAspect->after method");}
}
添加自定义注解
在TestController中的t1和UserController中的u1这两个方法上添加自定义注解@MyAspect,其他的不添加。
@RequestMapping("/user")
public class UserController {@MyAspect@RequestMapping("/u1")public String u1() {return "u1";}@MyAspect@RequestMapping("/t1")public String test1() {return "test1";}
运行程序进行测试:
可以看出切面通知被执行了
运行其他的方法:
可以看出并没有执行切面通知
经典面试题:
Spring AOP的实现方式:
1.基于注解@Aspect
2.基于自定义注解(参考@annotation部分的内容)
3.基于Spring API(提供xml配置的方式,但是自从SpringBoot广泛使用后,这种方式几乎就看不见了)
4.基于代理来实现(更加久远的方式,写法繁琐)
五、Spring AOP原理
Spring AOP是基于动态代理来实现AOP的,首先先了解代理模式:
1.代理模式
也叫委托模式,为其他对象提供一种代理以控制对这个对象的访问,它的作用就是提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,就是提供代理类简介访问。
根据代理的创建时期,代理模式可以分为静态代理和动态代理
静态代理:由程序员创建代理类或特定工具自动生成源代码再对其进行编译,在程序运行前代理类的.class文件就已经存在了。
动态代理:在程序运行时,运用反射机制动态创建而成。
静态代理
静态代理由于代码冗余度高,灵活性低,无法在没有接口的情况下使用,所以使用的频率较低。
下面是一个简单的例子:
就用户注册的例子:
// 公共接口:用户服务
public interface UserService {void register(String username, String password); // 注册方法
}
// 真实目标对象:具体用户服务实现
public class RealUserService implements UserService {@Overridepublic void register(String username, String password) {System.out.println("真实注册逻辑:用户 " + username + " 注册成功(密码已加密)");// 这里可以是复杂的数据库操作、校验等真实逻辑}
}
/ 静态代理类:增强用户服务
public class UserServiceProxy implements UserService {private final UserService realUserService; // 持有真实对象的引用// 构造方法传入真实对象public UserServiceProxy(UserService realUserService) {this.realUserService = realUserService;}// 重写接口方法,织入增强逻辑@Overridepublic void register(String username, String password) {// 前置增强:注册前的日志记录System.out.println("【代理前置】开始处理注册请求:用户 " + username);// 调用真实对象的方法(核心业务)realUserService.register(username, password);// 后置增强:注册后的日志记录System.out.println("【代理后置】注册流程结束,已记录操作日志");}
}
运行一个例子:
public class Client {public static void main(String[] args) {// 1. 创建真实对象UserService realService = new RealUserService();// 2. 创建代理对象,传入真实对象UserService proxyService = new UserServiceProxy(realService);// 3. 通过代理对象调用方法(触发代理逻辑)proxyService.register("张三", "123456");}
}
结果:
【代理前置】开始处理注册请求:用户 张三
真实注册逻辑:用户 张三 注册成功(密码已加密)
【代理后置】注册流程结束,已记录操作日志
动态代理
相比于静态代理,动态代理就更加灵活,我们不需要针对每一个目标对象都单独创建一个代理
对象,而是把这个创建对象的工作推迟到程序运行时由JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建生成。
常见的实现方式有两种:
1.JDK动态代理
2.CGLIB动态代理
JDK动态代理
实现步骤:
1.定义一个接口及其实现类
2.自定义InvocationHandler并重写invoke方法,在invoke方法中调用目标方法并自定义一些处理逻辑。
3.通过Proxy.newProxyInstance(ClassLoder loder,Class<?>[] interfaces, InvaocationHandler h)方法创建代理对象。
定义JDK动态代理类
实现InvocationHandler接口
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class JDKInvocationHandler implements InvocationHandler {//目标对象private Object target;public JDKInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//增强代理内容System.out.println("我是代理者");Object reVal = method.invoke(target, args);//代理增强内容System.out.println("代理结束");return reVal;}
}
创建一个代理对象并使用
import com.example.aop.HouseSubject.HouseSubject;
import com.example.aop.HouseSubject.RealHouseSubject;
import com.example.aop.JDK.JDKInvocationHandler;
import org.springframework.cglib.proxy.Proxy;public class Main {public static void main(String[] args) {HouseSubject target = new RealHouseSubject();//创建一个代理类HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),new Class[]{HouseSubject.class},new JDKInvocationHandler(target));proxy.rentHouse();}
}
HouseSubject和RealHouseSubject的代码:
public interface HouseSubject {void rentHouse();void saleHouse();}
public class RealHouseSubject implements HouseSubject {@Overridepublic void rentHouse() {System.out.println("房屋拥有者出租房子");}@Overridepublic void saleHouse() {System.out.println("房屋拥有者出售房子");}
}
运行结果:
代码讲解:
1.InvocationHandler
InvocationHandler接口是java动态代理的关键接口之一,它定义了一个单一方法invoke(),用于处理被代理对象的方法调用。
public interface InvocationHandler
extends Callback
{/*** @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])*/public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}
参数说明:proxy:代理对象;method:代理对象需要实现的方法,即其中需要重写的方法;
args:mothed所对应方法的参数。
2.Proxy
Proxy类中使用频率最高的方法是:newProxyInstance(),这个方法主要用来生成一个代理对象。
public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) {try {Class clazz = getProxyClass(loader, interfaces);return clazz.getConstructor(new Class[]{ InvocationHandler.class }).newInstance(new Object[]{ h });} catch (RuntimeException e) {throw e;} catch (Exception e) {throw new CodeGenerationException(e);}}
参数含义:
loder:类加载器,用于加载代理对象。
interfaces:被代理类实现的一些接口。
h:实现了InvocationHandler接口的对象。
CGLIB动态代理
JDK有一个很致命的缺点就是只能代理实现了接口的类,在没有实现接口的情况下,我们就可以使用CGLIB来解决问题。
实现步骤:
1.定义一个被代理类;
2.自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK中的invoke方法类似。
3.通过Enhancer类中的create()创建代理类。
添加依赖:
CGLIB是一个开源项目,使用的话需要引入依赖
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version></dependency>
自定义MethodInterceptor(方法拦截器)
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;public class CGLIBInterceptor implements MethodInterceptor {private Object target;public CGLIBInterceptor(Object target) {this.target = target;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("代理者开始代理");Object result = methodProxy.invoke(target, objects);System.out.println("代理者结束代理");return result;}
}
注意导入的包为import org.springframework.cglib.proxy.MethodProxy;
创建代理类并使用:
public class Main {public static void main(String[] args) {HouseSubject target = new RealHouseSubject();HouseSubject proxy = (HouseSubject) Enhancer.create(target.getClass(), new CGLIBInterceptor(target));proxy.rentHouse();}
}
运行结果:
代码讲解:
1.MethodInterceptor
和JDK方式中的InvocationHandler类似,它只是定义了一个方式Intercept,用于增强目标方法。
2.Enhancer.create()
用来生成一个代理对象。
面试题:
Spring使用的哪种方式:
默认proxyTargetClass (源码中的一个重要参数) false,如果实现了接口,使用JDK;普通类,使用CGLIB
Spring Boot使用哪种方式:
在SpringBoot2.0以后,proxyTargetClass默认为true,默认使用CGLIB。