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;
}
@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;
}
}
创建一个代理对象并使用
public interface House {
void rent();
}
public class RealHouse implements House{
@Override
public 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;
}
@Override
public 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动态代理实现
运行时使用哪种方式与项目配置和代理的对象有关。