1.什么是AOP
AOP是Aspect Oriented Programming(面向切片编程),切面就是指某一类特定的问题,所以AOP也可以理解为面向特定方法编程。可以实现登入校验拦截器,统一数据返回格式,统一异常处理。
简单来说:AOP是一种思想,是对某一类事情的集中处理。
2.Spring AOP详解
2.1 切点(Pointcut)
切点,也称为切入点。Pointcut的作用就是提供一组规则,告诉程序对哪些方法进行功能增强。
@Around("execution(* com.adviser.springaop1.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {long begin = System.currentTimeMillis();Object result = pjp.proceed();long end = System.currentTimeMillis();log.info("{}执⾏耗时: {}ms", pjp.getSignature(), end - begin);return result;}
上面的表达式execution(* com.adviser.springaop1.controller.*.*(..))就是切点表达式。
2.2 连接点(Join Point)
满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法,以上述的程序为例,所有com.adviser.springaop1.controller路径下的所有方法都是连接点。
package com.adviser.springaop1.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {@RequestMapping("/t1")public String t1() {log.info("执行t1方法......");return "t1";}@RequestMapping("/t2")public String t2() {log.info("执行t2方法......");return "t2";}
}
上述TestController中所有的方法都是连接点
切点和连接点的关系,连接点是满足切点表达式的元素。切点可以看作是保存了众多连接点的一个集合。说白了切点就是表示一个集合,连接点就是集合中的单独的个体
比如:
切点表达式:学校全体老师
连接点:张三,李四等各个老师
2.3 通知(Advice)
通知就是具体要做的工作,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
比如上述程序中记录业务方法的耗时时间,就是通知。
就是红色框住的这些。在AOP面向切片编程当中,我们把这部分重复的代码逻辑抽取起来单独定义,这部分代码就是通知的内容。
2.4 切面(Aspect)
切面(Aspect)= 切点(Pointcut)+ 通知(Advice)
通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。
切面既包含了通知的逻辑定义,也包含了连接点的定义。
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
3. 通知类型
上面我们讲述了什么是通知,接下来学习通知的类型。@Around就是其中一种通知类型,标识环绕通知。
Spring中AOP的通知类型有以下几种:
@Around:环绕通知,此注解标注的通知方法在目标方法前后都被执行。
@Before:前置通知,此注解标注的通知方法在目标方法前被执行。
@After:后置通知,此注解标注的通知方法在目标方法后被执行。无论是否有异常都会执行。
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。
我们通过代码来加深对这几个通知的理解
package com.adviser.springaop1.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Slf4j
@Component
@Aspect
public class AspectDemo {//前置通知@Before("execution(* com.adviser.springaop1.controller.*.*(..))")public void doBefore() {log.info("执行 Before 方法");}//后置通知@After("execution(* com.adviser.springaop1.controller.*.*(..))")public void doAfter() {log.info("执行 doAfter 方法");}//返回后通知@AfterReturning("execution(* com.adviser.springaop1.controller.*.*(..))")public void doAfterReturn() {log.info("执行 doAfterReturn 方法");}//抛出异常后通知@AfterThrowing("execution(* com.adviser.springaop1.controller.*.*(..))")public void doAfterThrowing() {log.info("执行 doAfterThrowing 方法");}//添加环绕通知@Around("execution(* com.adviser.springaop1.controller.*.*(..))")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around 方法开始执行");Object result = joinPoint.proceed();log.info("Around 方法结束执行");return result;}
}
接下来是测试程序
package com.adviser.springaop1.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {@RequestMapping("/t1")public String t1() {log.info("执行t1方法......");return "t1";}@RequestMapping("/t2")public Boolean t2() {log.info("执行t2方法......");int a = 10 / 0;return true;}
}
用postman测试一下
程序正常运行的时候@AfterThrowing标识的通知方法不会执行
从上图也可以看出,@Around标识的通知方法包含两部分,一个前置逻辑,一个后置逻辑。其中前置逻辑会先于@Before标识的通知方法执行,获知逻辑会晚于@After标识的通知方法执行。
异常时的情况
程序发生异常的情况下:
@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了。
@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)
注意事项:
@Around环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。
@Around环绕通知方法的返回值,必须是Object,来接收原始方法的返回值,否则原始方法执行完毕后是获取不到返回值的。
一个切面类可以有多个切点
4.@Pointcut
上面代码存在一个问题,就是存在大量重复的切点表达式
execution(* com.adviser.springaop1.controller.*.*(..)),spring提供了@Pointcut注解,把公共的切点表达式提取出来,需要用到时引入切入点表达式即可。
@Slf4j
@Component
@Aspect
public class AspectDemo {//定义切点(公共切点表达式)@Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")private void pt(){}//前置通知@Before("pt()")public void doBefore() {log.info("执行 Before 方法");}//后置通知@After("pt()")public void doAfter() {log.info("执行 doAfter 方法");}//返回后通知@AfterReturning("pt()")public void doAfterReturn() {log.info("执行 doAfterReturn 方法");}//抛出异常后通知@AfterThrowing("pt()")public void doAfterThrowing() {log.info("执行 doAfterThrowing 方法");}//添加环绕通知@Around("pt()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around 方法开始执行");Object result = joinPoint.proceed();log.info("Around 方法结束执行");return result;}
}
当切点用private修饰时,仅能在当前切面类中中使用,当其他切面类也要使用当前切点定义时,就需要把private改为public。引用方式为:全限定类名.方法名()
@Aspect
@Slf4j
@Component
public class AspectDemo1 {@Before("com.adviser.springaop1.aspect.AspectDemo.pt()")public void doBefore() {log.info("执行 AspectDemo1 -> Before方法");}}
5.切面优先级@Order
当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当这些目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的?
定义多个切面类:
@Aspect
@Slf4j
@Component
public class AspectDemo1 {@Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")private void pt(){}@Before("pt()")public void doBefore() {log.info("执行 AspectDemo1 -> Before方法");}@After("pt()")public void doAfter() {log.info("执行 AspectDemo1 -> doAfter方法");}}
@Aspect
@Slf4j
@Component
public class AspectDemo2 {@Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")private void pt(){}@Before("pt()")public void doBefore() {log.info("执行 AspectDemo2 -> Before方法");}@After("pt()")public void doAfter() {log.info("执行 AspectDemo2 -> doAfter方法");}}
@Aspect
@Slf4j
@Component
public class AspectDemo2 {@Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")private void pt(){}@Before("pt()")public void doBefore() {log.info("执行 AspectDemo2 -> Before方法");}@After("pt()")public void doAfter() {log.info("执行 AspectDemo2 -> doAfter方法");}}
运行程序使用postman测试一下
通过上述日志可以看出:
存在多个切面类时,默认按照切面类的类名字母排序:
@Before通知:字母排名靠前的先执行
@After通知:字母排名靠后的先执行
但这种方式不方便管理,我们的类名更多的还是具备一定含义的。
Spring给我们提供一个新的注解,来控制这些切面通知的执行顺序:@Order
使用方式如下
@Aspect
@Slf4j
@Order(3)
@Component
public class AspectDemo1 {}@Aspect
@Slf4j
@Order(1)
@Component
public class AspectDemo2 {}@Aspect
@Slf4j
@Order(2)
@Component
public class AspectDemo3 {}
再次访问http://127.0.0.1:8080/test/t1
通过上述程序的运行结果,得出结论:
@Order注解标识的切面类,执行顺序如下:
@Before通知:数字越小先执行
@After通知:数字越大先执行
@Order控制切面的优先级,先执行优先级较高的切面,在执行优先级较低的切面,最终执行目标方法。
6.切点表达式
上面的代码中,我们一直在使用切点表达式来描述切点。下面我们来介绍一下切点表达式的语法。
切点表达式常见有两种表达方式
1.execution(........):根据方法的签名来匹配
2.@annotation(........):根据注解匹配
6.1execution表达式
execution()是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰限定符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中访问修饰限定符和异常可以省略
切点表达式支持通配符表达:
1.*:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
a. 包名使用 * 标识任意包(一层包使用一个 * )
b. 类名使用 * 标识任意类
c. 返回值是用 * 表示任意返回值类型
d.方法名使用 * 表示任意方法
e. 参数使用 * 表示一个任意类型的参数
2. . . :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
a.使用 .. 配置包名,表示此包以及此包下的所有子包
b. 可以使用 . . 配置参数,任意个任意类型的参数
切边表达式示例:
TestController下的public修饰,返回类型为String方法名为t1,无参方法
execution(public String com.example.demo.controller.TestController.t1())
省略访问修饰符
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回类型
execution(* com.example.demo.controller.TestController.t1())
匹配TestController 下的所有无参⽅法
execution(* com.example.demo.controller.TestController.*())
匹配TestController 下的所有⽅法
execution(* com.example.demo.controller.TestController.*(..))
匹配controller包下所有的类的所有⽅法
execution(* com.example.demo.controller.*.*(..))
匹配所有包下⾯的TestController
execution(* com..TestController.*(..))
匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法
execution(* com.example.demo..*(..))
6.2@annotation
execution表达式更适合有规则的,如果我们要匹配多个无规则的方法呢,比如TestController中t1()和UserController中的u1()这两个方法。
这个时候我们使用execution这种切点表达式来描述就不是很方便了
我们可以借助自定义注解的方式以及另一种切点表达式@annotation来描述这一类的切点
实现步骤:
1.编写自定义注解
2.使用@annotation表达式来描述切点
3.在连接点的方法上添加自定义注解
测试代码
@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {@RequestMapping("/t1")public String t1() {log.info("执行t1方法......");return "t1";}@RequestMapping("/t2")public Boolean t2() {log.info("执行t2方法......");return true;}
}@RequestMapping("/user")
@RestController
public class UserController {@RequestMapping("/u1")public String u1() {return "u1";}@RequestMapping("/u2")public String u2() {return "u2";}
}
6.2.1自定义注解@MyAspect
package com.adviser.springaop1.config;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 {
}
1. @Target标识了Annotation所修饰的对象范围,即该注解可以用在什么地方。
ElementType.TYPE:用于描述类,接口(包括注解类型)或enum声明
ElementType.METHOD:修饰方法
ElementType.PARAMETER:描述参数
ElementType.TYPE_USE:可以标注任意类型
2. @Retention 指Annotation被保留的时间长短,标明注解的生命周期
1.RetentionPolicy.SOURCE:表示注解仅存在于源码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如@SuppressWarnings,以及lombok提供的注解@Data,@slf4j
2.RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行过程中无法获取。通常用于一些框架和工具的注解。
3.RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时。这意味着在编译时,字节码中和实际运行中都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如Spring的@Controller,@ResponseBody
6.2.2切面类
使用@annotation切点表达式定义切点,只对@MyAspect生效
@Aspect
@Component
@Slf4j
public class MyAspectDemo {@Before("@annotation(com.adviser.springaop1.config.MyAspect)")public void before() {log.info("MyAspect -> before ...");}@After("@annotation(com.adviser.springaop1.config.MyAspect)")public void after() {log.info("MyAspect -> after ...");}}
6.2.3添加自定义注解
@MyAspect
@RequestMapping("/t1")
public String t1() {log.info("执行t1方法......");return "t1";
}@MyAspect
@RequestMapping("/u1")
public String u1() {return "u1";
}
测试运行一下
可以看到切面通知执行了。
7. Spring AOP的实现方式
1.基于注解Aspect
2.基于自定义注解da
3.基于Spring API(通过xml配置的方式,自从SpringBoot广泛使用之后,着用方式几乎看不到了)
4.基于打完
8. Spring AOP原理
Spring AOP是基于动态代理来实现AOP的
JDK动态代理
1.定义一个接口及其实现类
2.自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑。
3.通过Proxy.newProxyInstance(ClassLoader loader, Class<?>[ ] Interfaces, InvocationHandler 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 retVal = method.invoke(target, args);//代理增强内容System.out.println("我是中介代理结束");return retVal;}
}
创建一个代理对象并使用
public interface House {void rent();
}public class RealHouse implements House{@Overridepublic void rent() {System.out.println("我是房东,出租房子");}
}
public class Main {public static void main(String[] args) {House target = new RealHouse();//创建一个代理类,通过被代理类,被代理实现的接口,方法调用处理器来创建House proxy = (House) Proxy.newProxyInstance(target.getClass().getClassLoader(),new Class[]{House.class},new JDKInvocationHandler(target));proxy.rent();}
}
1.InvocationHandler
InvocationHandler接口是Java动态代理的关键接口之一,它定义了一个单一方法invoke(), 用于处理被代理对象的方法调用。
public interface InvocationHandler {/*** 参数说明* proxy:代理对象* method:代理对象需要实现的⽅法,即其中需要重写的⽅法* args:method所对应⽅法的参数*/public Object invoke(Object proxy, Method method, Object[] args)throws Throwable;
}
通过实现InnovationHandler接口,可以对被代理对象的方法进行功能增强。
2.Proxy
Proxy类中使用频率最高的方法是:newProxyInstance() 这个方法主要用来生成一个代理对象
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException{//...代码省略}
这个方法一共三个参数:
Loader:类加载器,用于加载代理对象(确保代理类和被代理类使用的类加载器一致)
interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)
h:实现了invocationHandler接口对象
CGLIB动态代理
JDK动态代理有一个致命的缺点,就是只能代理实现了接口的类。
有些场景下,我们业务代码是直接实现的。并没有接口定义,为了解决这个问题我们可以用CGLIB动态代理机制来解决。
CGLIB动态代理类实现步骤
1.定义一个类(被代理类)
2.自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似。
3.通过Enhancer类的create()创建代理类
public class CGLIBMethodInterceptor implements MethodInterceptor {private Object target;public CGLIBMethodInterceptor(Object target) {this.target = target;}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("我是中介,开始代理");Object result = method.invoke(target, args);System.out.println("我是中介,代理结束");return result;}
}
创建代理类,并使用
public class Main {public static void main(String[] args) {RealHouse target = new RealHouse();RealHouse proxy = (RealHouse) Enhancer.create(target.getClass(), new CGLIBMethodInterceptor(target));proxy.rent();}
}
1.MethodInterceptor
MethodInterceptor和JDK动态代理中的InvocationHandler类似,他只是定义了一个方法intercept(),用于增强目标方法。
public interface MethodInterceptor extends Callback {/*** 参数说明:* o: 被代理的对象* method: ⽬标⽅法(被拦截的⽅法, 也就是需要增强的⽅法)* objects: ⽅法⼊参* methodProxy: ⽤于调⽤原始⽅法*/Object intercept(Object o, Method method, Object[] objects, MethodProxy
methodProxy) throws Throwable;
}
2. Enhancer.create()
Enhancer.create()用来生成一个代理对象
public static Object create(Class type, Callback callback) {//...代码省略
}
参数说明:
type:被代理类的类型(类或接口)
callback:自定义方法拦截器MethodInterceptor
JDK和CGLIB代理的区别
JDK代理:只能为实现了接口的类实现代理,需要提供接口。
CGLIB代理可以为任何类生成代理,不要求目标实现接口。
而Spring AOP 会根据被代理类的情况自适应选择代理方式。如果被代理类实现了一个或多个接口,Spring 将使用 JDK 动态代理;如果被代理类没有实现任何接口,Spring 则会使用 CGLIB 代理。这种机制确保了无论被代理类的结构如何,Spring AOP 都能够有效地为其添加切面逻辑。
9. 总结
1.AOP是一种思想,是对某一类事情的集中处理。Spring框架实现了AOP,称之为Spring AOP
2.Spring AOP常见实现方式有两种:
1. 基于注解@Aspect来实现
2.基于自定义注解来实现,还有一些更原始的方式,比如基于代理,基于xml配置,但目标比较少见
3. Spring AOP是基于动态代理实现的,有两种方式:
1.基本JDK动态代理实现
2.基于CGLIB动态代理实现
运行时使用哪种方式与项目配置和代理的对象有关。