Spring AOP 学习大纲
一.AOP概述
AOP是Spring框架的第二大核心(第一大核心是IoC)
什么是AOP?
Aspect Oriented Programming(面向切面编程)
什么是面向切面编程呢?切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程
拦截器就是AOP的一种应用,AOP是一种思想,拦截器是AOP思想的一种实现,spring框架实现了这种思想,提供了拦截器技术的相关接口
同样的,统一数据返回格式和统一异常处理,也是AOP思想的一种实现
简单来说:AOP是一种思想,是对某一类事情的集中处理
什么是Spring AOP?
AOP是一种思想,他的实现方法有很多,有springAOP,也有Aspect J,CGLIB等。SpringAOP是其中的一种实现方式
学会了统一功能之后,是不是就学会了SpringAOP呢?当然不是
拦截器作用的维度是URL(一次请求和响应),@ControllerAdvice应用场景主要是全局异常处理(配合自定义异常效果更佳),数据绑定,数据预处理。AOP作用的维度更加细致(可以根据包,类,方法名,参数等进行拦截),能够实现更加复杂的业务逻辑
AOP可以在不改动原有程序的基础上,针对特定的方法进行功能的增强(AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性:解耦)
二.SpringAOP快速入门
1.引入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.编写AOP程序
记录Controller中每个方法的执行时间
java
@Slf4j
@Aspect
@Component
public class TimeAspect {
/**
* 记录方法耗时
*/
@Around("execution(*com.july.aop.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info(pjp.getSignature() + "执行耗时:{}ms",end - begin);
return result;
}
}
对程序进行简单的讲解:
1.@Aspect:标识这是一个切面类
2.@Around:环绕通知,在目标方法的前后都会被执行,后面的表达式表示对哪些方法进行增强
3.ProceedingJoinPoint.proceed()让原始方法执行
通过上述程序,我们可以感受到AOP的一些优势:
1.代码无侵入
2.减少了重复代码
3.提高了开发效率
4.维护方便
三.SpringAOP详解
1.Spring AOP核心概念
1.切点(PointCut)
切点(PointCut),也叫做"切入点",PointCut的作用就是提供一组规则,告诉程序员对哪些方法来进行功能增强
上述代码中@Around("execution(*com.july.aop.controller.*.*(..))")就是切点表达式
2.连接点(Join Point)
满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法
以入门AOP程序举例,所有com.july.aop.controller路径下的方法就是连接点
切点和连接点的关系:
连接点是满足切点表达式的元素,切点可以看作是保存了众多连接点的一个集合
3.通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
在AOP面向切面编程中,我们把这部分重复的代码逻辑抽取出来单独定义 ,这部分代码就是通知的内容
4.切面(Aspect)
切面(Aspect) = 切点(CutPoint)+ 通知(Advice)
切面既包含了通知逻辑的定义,也包含了连接点的定义
2.通知类型
上述我们了解了什么是通知,接下来我们学习通知的类型
@Around就是一种通知类型,表示环绕通知
Spring中AOP的通知类型有以下几种:
@Around:环绕通知,此注解标注的通知方法在目标方法前后都执行
@Before:前置通知,此注解标注的通知方法在目标方法前执行
@After:后置通知,此注解标注的通知方法在目标方法后执行,无论是否有异常都会被执行
@AfterReturing:返回后通知,此注解标注的通知方法在目标方法后执行,有异常不执行
@AfterThrowing:异常后通知,此注解标注的通知方法发送异常后执行
程序正常运行情况下:
@Around前->@Before->@AfterReturning->@After->@Around后
程序发生异常的情况下:
@AfterReturning 标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出现异常了)
@Around前--->@Before->@After Throwing->@After
注意事项:
1.@Around环绕通知需要调用@ProceedingJoinPoint.proceed()来让原方法执行,其他通知不需要考虑目标方法执行
2.@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的
3.一个切面类可以有多个切点
3.@PointCut
Spring提供了@PointCut注解,把公共的切点表达式提取出来,需要时引用该切入点表达式即可
java
package com.july.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(com.july.aop.controller..*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore(){
}
//后置通知
@After("pt()")
public void deAfter(){
}
//返回后通知
@After("pt()")
public void deAfterReturning(){
}
//抛出异常后通知
@After("pt()")
public void deAfterThrowing(){
}
//环绕通知
@After("pt()")
public void deAround(){
}
}
当切点用private修饰的时候,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义的时候,就需要把private改为public,引用方式为:全限定名.方法名()
java
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
@Before("execution(* com.bit.aop.controller.*.*(..))")
public void doBefore() {
log.info("AspectDemo2 doBefore....");
}
}
4.切面优先级@Order
当存在多个切面类的时候,默认按照切面类的类名字母排序:
1.@Before通知:字母排名靠前的先执行
2.@After通知:字母排名靠后的先执行
但是这种方式并不便于管理,Spring提供了一个新的注解,来控制切面通知的顺序:@Order
@Order注解标识的切面类,执行顺序如下:
1.@Before通知:数字越小先执行
2.@After通知:数字越大先执行
5.切点表达式
切点表达式常见有两种表达方式:
1.execution(...):根据方法的签名来匹配
2.@Annotation(...):根据注解匹配
1.execution表达式
execution()是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符><返回类型><包名.类名.方法(方法参数)><异常>
切点表达式支持通配符表达:
1.*:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
- ..:匹配多个连续的任意符号,可以统配任意层级的包,或任意类型,任意个数的参数
2.@annotation
execution表达式更适合有规则的,如果要匹配多个无规则的方法呢,这个时候我们可以借助自定义注解的方式以及另一种切点表达式@annotaion来描述这一类的切点
实现步骤:
1.编写自定义注解
2.使用@annotion表达式来描述切点
3.在连接点的方法上添加自定义注解
java
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
//前置通知
@Before("@annotation(com.july.aop.aspect.MyAspect")
public void doBefore(){
log.info("MyAspect ->before");
}
//后置通知
@After("@annotation(com.july.aop.aspect.MyAspect")
public void deAfter(){
log.info("MyAspect ->after");
}
}
总结:SpringAOP的实现方式(常见的面试题)
1.基于注解@Aspect
2.基于自定义注解@annotation
3.基于SpringAPI(通过xml配置的方式,在springboot广泛使用后,这种方法几乎看不到了)
4.基于代理的方式实现(更加古老笨重,不建议使用)
四.SpringAOP原理
Spring是如何实现AOP的呢?
SpringAOP是基于动态代理来实现AOP的,下面我们先了解代理模式,再对SpringAOP源码进行分析
1.代理模式
代理模式也叫做委托模式
定义:为其他对象提供一种代理以控制这个对象的访问。他的作用就是通过一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用
代理模式的主要角色:
1.Subject:业务接口类,可以是抽象类或者接口(不一定有)
2.ReadSubject:业务实现类,具体的业务执行,也就是被代理对象
3.Proxy:代理类,RealSubject的代理
代理模式可以在不修改被代理对象的基础上,通过拓展代理类,进行一些功能的附加与增强
根据代理的创建时期,代理模式分为静态代理和动态代理
静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了
动态代理:在程序运行时,运用反射机制动态创建而成
1.静态代理
静态代理在程序运行前就把代码写好了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,在日常开发中,几乎看不到静态代理的场景
我们通过代码来加深对静态代理模式的理解,以租房为例
1.定义接口(定义房东要做的时,也是中介要做的事情)
java
public interface HouseSubject {
void rentHouse();
}
2.实现接口(房东出租房子)
java
public class RealHouseSubject implements HouseSubject{
@Override
public void rentHouse() {
System.out.println("我是房东,我出租房子");
}
}
3.代理(中介,帮忙出租房子)
java
public class HouseProxy implements HouseSubject{
//将被代理对象声明成成员变量
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject){
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
//开始代理
System.out.println("我是中介,开始代理");
houseSubject.rentHouse();
//结束代理
System.out.println("我是中介,代理结束");
}
}
4.使用
java
public class StaticMain {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
//创建代理类
HouseProxy houseProxy = new HouseProxy(subject);
//通过代理访问目标方法
houseProxy.rentHouse();
}
}

上面这个代理实现方式就是静态模式,从上述程序可以看出,虽然静态代理也完成了目标对象的代理,但代码写死了,对目标对象的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景
接下来又新增需求,出售房子
我们需要对上述代码进行修改
1.接口定义修改
java
public interface HouseSubject {
void rentHouse();
void saleHouse();
}
2.接口实现修改
java
public class RealHouseSubject implements HouseSubject{
@Override
public void rentHouse() {
System.out.println("我是房东,我出租房子");
}
@Override
public void saleHouse() {
System.out.println("我是房东,我出售房子");
}
}
3.代理类修改
java
public class HouseProxy implements HouseSubject{
//将被代理对象声明成成员变量
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject){
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
//开始代理
System.out.println("我是中介,开始代理");
houseSubject.rentHouse();
//结束代理
System.out.println("我是中介,代理结束");
}
@Override
public void saleHouse() {
//开始代理
System.out.println("我是中介,开始代理");
houseSubject.rentHouse();
//结束代理
System.out.println("我是中介,代理结束");
}
}
从上述代码可以看出,我们修改接口(subject)和业务实现类(realSubject)时还需要修改代理类
同样的,如果有新增接口(subject)和业务实现类(realSubject),也需要对每一个业务实现类新增代理类(proxy)
2.动态代理
相比于静态代理来说,动态代理更加灵活
我们不需要针对每个目标对象都单独创建一个 代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM实现,也就是说动态代理在程序运行时,根据需要动态创建生成
Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:
1.JDK动态代理
JDK动态代理类实现步骤
1.自定义一个接口及其实现类
2.自定义InvocationHandler并 重写invoke方法,在invoke方法中我们会调用目标方法(被代理的方法)并自定义一些处理逻辑
3.通过Proxy.newProxyInstance(ClassLoader loader,Class<?>\[\] interfacces,InvocationHandler h)方法创建代理对象
实现JDK动态代理类
1,实现InvocationHandler接口
java
public class JDKInvocationHandler implements InvocationHandler {
//目标对象就是被代理对象
private Object target;
public JDKInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理增强内容
System.out.println("我是中介,开始代理");
//通过反射调用被代理类方法
Object retVal = method.invoke(target,args);
//代理增强内容
System.out.println("我是中介,结束代理");
return retVal;
}
}
2.创建一个代理对象并使用
java
public class JDKInvocationHandler implements InvocationHandler {
//目标对象就是被代理对象
private Object target;
public JDKInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理增强内容
System.out.println("我是中介,开始代理");
//通过反射调用被代理类方法
Object retVal = method.invoke(target,args);
//代理增强内容
System.out.println("我是中介,结束代理");
return retVal;
}
}
下面我们了解一下这部分代码
1.InvocationHandler
InvocationHandler是Java动态代理的关键接口之一,他定义了一个单一方法invoke(),用于处理被代理对象的方法调用
通过实现InvocationHandler接口,可以对被代理对象的方法进行功能增强
2.Proxy
Proxy类中使用频率最高的方法是newProxyInstance(),这个方法主要用来生成一个代理对象
这个方法一共三个参数:1.Loader:类加载器,用于加载代理对象 2.interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类) h:实现了InvocationHandler接口的对象
ps:上述代码只是伪代码,我们要知道JDK动态代理只能代理接口
2.CGLIB动态代理
JDK动态代理有一个致命的缺点就是只能代理实现了接口的类,有一些场景下业务代码是直接实现的,JDK动态代理无法解决,我们可以使用CGLIB动态代理机制来解决
CGLIB(Code Generation Libary)是一个基于ASM的字节码生成库,它允许我们在运行时对于字节码进行修改和动态生成.CGLIB通过继承的方式实现代理,很多知名的开源框架都使用到了CGLIB,例如Spring中的AOP模块中:如果目标对象实现了接口,默认使用JDK动态代理,否则采用CGLIB动态代理
CGLIB动态代理类实现步骤:
1.定义一个类(被代理类)
2.自定义MethodInterceptor并重写Interceptor方法,intercept用于增强目标方法,和JDK动态代理中的invoke()方法类似
3.通过enhancer类的create()创建代理类
实现:
1.添加依赖
CGLIB实际上属于一个开源项目,如果要使用,要添加依赖
java
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
2.自定义MethodInterceptor(方法拦截器)
实现MethodInterceptor接口
java
public class CGlibMethodInterceptor implements MethodInterceptor {
private Object target;
public CGlibMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("我开始代理");
Object invoke = method.invoke(target, args);
System.out.println("我结束代理");
return invoke;
}
}
3.创建代理类,并使用
java
HouseSubject houseProxy = (HouseSubject) Enhancer.create(target.getClass(), new CGlibMethodInterceptor(target));
houseProxy.rentHouse();
代码介绍:
1.MethodInterceptor
MethodInterceptor和JDK动态代理中的InvocationHandler类似,他只是定义了一个方法intercept(),用于增强目标方法
2.Enhancer.create()用来创建要给代理对象
2.SpringAOP源码分析(了解)
springAOP主要是基于两种方式实现的:JDK以及CGLIB的方式
Spring 对AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成
生成代理对象的逻辑在父类AbstractAutoProxyCreator中
java
private Class<?> createProxyClass(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
return (Class<?>) buildProxy(beanClass, beanName, specificInterceptors, targetSource, true);
}
private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
}
//创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
/**
*检查proxyTargetClass属性值,spring默认为false
*proxyTargetClass检查接口是否对类代理,而不是接口代理
*如果代理对象为类,设置为true,使用cglib代理
*
*/
if (proxyFactory.isProxyTargetClass()) {
//
// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
}
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
classLoader = smartClassLoader.getOriginalClassLoader();
}
return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}</code></pre>
代理工厂有一个重要的属性:proxyTargetClass,默认值为false,也可以通过程序设置
proxyTargetClass
目标对象
代理方式
false
实现了接口
jdk代理
false
未实现接口(只有实现类)
cglib代理
true
实现了接口
cglib代理
true
未实现接口(只有实现类)
cglib代理
CGLIB和JDK代理的区别
1.JDK只能代理接口
2.CGLIB能代理接口和类
SpringFrameWork proxyTargetClass默认为false
spring2.x默认为false
spring3.x默认为true
大部分情况下springframework和springboot是保持一致的,但是spring aop是不一致的