Spring-AOP
参考链接
Spring AOP介绍(切面编程)
AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。AOP的目的是实现关注点的分离;
术语、概念
- 通知(advice)
AOP框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理
- 连接点(join point)
连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用,异常的抛出。在Spring AOP中,连接点总是方法的调用。
- 切点(PointCut)
可以插入增强处理的连接点。
- 切面(Aspect)
切面是通知和切点的结合。
- 引入(Introduction)
引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving)
将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入
9种切点表达式(@Pointcut)
引用其他命名切入点,只有@ApectJ风格(注解)支持,Schema风格(XML配置)不支持,本文展示均为@ApectJ风格。
切点表达式简单解析
合并切入点表达式:
切入点表达式可以使用
&&
,||
和!
来合并.还可以通过名字来指向切入点表达式。【也可以用于类型和参数中】类型匹配模式:
1:
*
:匹配任意类型的单个字符;比如模式(*,String)
匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型在字符串中表示任意长度的字符,比如
bean(demo*)
匹配demo开头的bean(可匹配DemoServiceImpl、Demo2...)2:
..
:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数,可以是零到多个。3:
+
:匹配指定类型及子类型(包含当前类型);仅能作为后缀放在类型后边。
万能:
execution
是最灵活最常用的切点表达式。所有的表达式都可以用execution
表达式表示
指定类
within
=>this
=>target
相同点:都是指定至类的。
不同点:
within
,路径可使用通配符,可指定接口,可指定类。
this
,路径不可使用通配符!可指定接口,不可指定类!
target
,路径不可使用通配符!不可指定接口!可指定类。
指定参数类型
args
,匹配当前执行的方法传入的参数为指定类型的执行方法(是方法传入的参数类型,不是方法声明的参数类型)。参数类型的路径不可使用通配符。
args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用
跟注解有关的
@annotation
=>@target
=>@within
=>@args
@args
匹配方法有持有某注解参数。依然是动态切入点不建议使用
@annotation
匹配持有某注解的方法。
@within
和@target
相同点:都是匹配所有持有指定注解类型内的方法。
不同点:
@within
,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
@target
,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 (测试中,用@target
会报错,大概意思是aop太宽泛不可以使用)
bean
匹配bean名。
execution
匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指定者。
@注解只能使用全限定名,不能模糊使用..
,例如:@java.lang.Deprecated * *(..)
可以,@java..Deprecated * *(..)
不行
// 匹配该包下的所有类的所有方法(不包含子包)
@Pointcut("execution(* com.example.demo.util.*.*(..))")
// 匹配该包及子包下所有类的所有方法
@Pointcut("execution(* com.example.demo.util..*.*(..))")
// 匹配该包下public修饰的所有参数为(Integer,Integer)的方法
@Pointcut("execution(public * com.example.demo.util.*.*(Integer,Integer))")
// 匹配该包及子包下所有类的所有a开头的方法
@Pointcut("execution(* com.example.demo.util..a*(..))")
// 匹配该包下IDataServer接口中的任何方法
@Pointcut("execution(* com.example.demo.service.IDataServer.*(..))")
// 匹配该包及子包下IDataServer接口中的任何方法(可用于接口,所有调用接口的方法都可以被织入)
@Pointcut("execution(* com.example.demo..IDataServer.*(..))")
// 与或非示例,可用于各种位置
@Pointcut("execution(void||Integer com.example.demo..IDataServer.*())")
// 组合条件示例:IDataServer里的所有void方法 并且 com及其子包下IDataServer的add方法
@Pointcut("execution(void com.example.demo.service.IDataServer.*(..)) && execution(* com..IDataServer.add(..))")
// 匹配该包及子包下IDataServer接口及子类型的的任何方法
@Pointcut("execution(* com.example.demo..DataImpl+.*(..))")// + 包含当前类型
@Pointcut("execution(* com.example.demo..IDataServer+.*(..))")
/*支持注解*/
// 任何持有AopAnno注解的方法
@Pointcut("execution(@com.example.demo.aop.AopAnno * *(..))")
// 任何持有AopAnno注解 和 Deprecated注解的方法(并且的关系)
@Pointcut("execution(@com.example.demo.aop.AopAnno @java.lang.Deprecated * *(..))")
// 任何持有AopAnno注解 或 Deprecated注解的方法(或者的关系)
@Pointcut("execution(@(com.example.demo.aop.AopAnno || java.lang.Deprecated) * *(..))")
// 匹配返回值是Integer的带有@Deprecated注解的所有方法
@Pointcut("execution(@java.lang.Deprecated Integer *(..))")
// 匹配所有返回值持有@Data的方法 (@lombok.Data *)是带有@Data的对象作为返回值
@Pointcut("execution((@lombok.Data *) *(..))")
// 匹配任何带有一个参数的方法,且该参数类型持有@Data的方法 (@lombok.Data *)是带有@Data的对象作为返回值
@Pointcut("execution(* *(@lombok.Data *))")
// 匹配任何参数带有两个参数的方法,且这个两个参数都被@Data标记了(这里展示了参数的两种写法,带括号和不带括号)
@Pointcut("execution(* *(@lombok.Data *,@lombok.Data (*)))")
// 演示示例:多参数多注解写法:这里展示了参数的两种写法,带括号和不带括号.这里展示了多个注解关系的写法,或者和并且
@Pointcut("execution(* *(@(lombok.Getter || lombok.Setter) *,@lombok.Getter @lombok.Setter (*),@(lombok.Getter&&lombok.Setter) (*)))")
/*支持泛型,泛型支持注解、+号*/
// 匹配任何参数带有List<DataModel>的方法
@Pointcut("execution(* *(java.util.List<com..DataModel>))")
// 匹配任何参数带有List<DataModel及子类型>的方法
@Pointcut("execution(* *(java.util.List<com..DataModel+>))")
// 匹配任何参数带有List<持有@Data注解的DataModel及子类型>的方法
@Pointcut("execution(* *(java.util.List<@lombok.Data com..DataModel+>))")
// throws 不管用,这依然会匹配throws RunTimeException异常的方法,反之一样。
@Pointcut("execution(* com.example.demo.util.DemoBean.*(..)) throws java.io.IOException")
within
限定匹配特定类型的连接点(在使用SpringAOP的时候,在匹配的类型中定义的方法的执行)。
支持通配符
/*针对的类和接口*/
// 匹配该包下所有类的所有方法的执行
@Pointcut("within(com.example.demo..*)")
// 匹配该包及子包下的IDataServer内所有方法的执行
@Pointcut("within(com.example.demo..IDataServer+)")
// 匹配所有持有@AopAnno的类的所有方法
@Pointcut("within(@com.example.demo.aop.annotates.AopAnno *)")
this
匹配代理对象类型是指定类型的实例(仅支持全限定类名)。
(和
target
基本一样效果,建议不使用this
,使用target
)
// 支持controller和service(接口)
@Pointcut("this(com.example.demo.controller.TestController)")// 生效
@Pointcut("this(com.example.demo.service.IDataServer)")// 实现该接口的 所有类的方法:包含抽象方法和default方法均支持,只是写的时候IDEA提示不出来
@Pointcut("this(com.example.demo.service.impl.DataImpl2)")// 生效
和target的区别要注意
使用方法,代理的切点的效果是基本一致的。有个特例需要注意一下(平常不会这么写,所以了解即可):
一个接口,一个实现了接口的实现类,在接口中default的方法不会被this用实现类的形式代理。
如下代码:
public interface DemoService{
void add(int a,int b);
default void minus(int a,int b) {
Console.log(getClass().getSimpleName()+",算减法{}",a-b);
}
}
@Service
public class DemoServiceImpl implements DemoService {
@Override
public void add(int a, int b) {
Console.log(getClass().getSimpleName()+",算加法{}",a+b);
}
}
结论:@Pointcut("this(com.example.demo.service.DemoServiceImpl)")
不会作用在DemoService.minus
方法上。
支持的
@Pointcut("this(com.example.demo.service.IDataServer)")// ✔️支持。实现了IDataServer的所有实现类的所有方法。
不支持的
@Pointcut("this(com.example.demo.service.*.DataImpl)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
@Pointcut("this(com.example.demo.service..DataImpl)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
@Pointcut("this(com.example.demo.service.impl.DataI*)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
target
匹配目标对象类型是指定类型的实例(仅支持全限定类名)。
// 支持controller,不支持接口
@Pointcut("target(com.example.demo.controller.TestController)")
@Pointcut("target(com.example.demo.service.IDataServer)")// 支持接口(实现该接口的所有类的方法:包含抽象方法和default方法均支持,只是写的时候IDEA提示不出来)
@Pointcut("target(com.example.demo.service.impl.DataImpl)")// 支持Bean类
支持的
@Pointcut("target(com.example.demo.service.IDataServer)")// ✔️支持。实现了IDataServer的所有实现类的所有方法。
不支持的
@Pointcut("target(com.example.demo.service.*.DataImpl)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
@Pointcut("target(com.example.demo.service..DataImpl)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
@Pointcut("target(com.example.demo.service.impl.DataI*)")// ❌错误。启动报错:error wildcard type pattern not allowed, must use type name
args
匹配参数是指定类型的方法。
// 匹配Integer、Integer组合的参数方法
@Pointcut("args(Integer,Integer)")
// 匹配第一个参数是Serializable子类的参数,其余参数0到多个
@Pointcut("args(java.io.Serializable+,..)")
@within
匹配持有给定的注解的所有类的所有方法。(支持通配符)
注解要保证运行时能在类上获取到,例如某注解写到Service上,那么实现类的方法可能无法匹配(持有
@Inherited
的注解可以)
// 匹配持有该注解的类的所有方法
@Pointcut("@within(com.example.demo.aop.AopAnno)")
@Pointcut("@within(org.springframework.stereotype.Service)")
@Pointcut("@within(com.example.demo.*.AopAnno)") // ❌错误,仅支持全限定名
@target
启动报错:Caused by: java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "cause" is null
限定匹配特定的连接点(使用SpringAOP的时候方法的执行),其中执行的对象的类已经有指定类型的注解。
表述和
@within
一样,但是@within
能运行起来,@target
运行不起来,报错,所以尽量不考虑使用该表达式了。
@args
限定匹配实际传入参数的运行时有指定类型的注解。
// 匹配方法有一个参数且参数持有@Data注解
@Pointcut("@args(lombok.Data)")
// 匹配方法有多个参数且第一个参数持有@Data注解
@Pointcut("@args(lombok.Data,..)")
@annotation
匹配持有给定的注解的所有方法
// 匹配持有@Deprecated注解的所有方法
@Pointcut("@annotation(java.lang.Deprecated)")
支持将注解直接写入参数
注意:简写只支持确定会绑定该注解的写法。如果不一定绑定该注解,则会报错。
@After("@annotation(methodAnnotate)")// ⭕ 正常注解写法,
@After("@annotation(methodAnnotate) && bean(demoBean)")// ⭕ 限定为demoBean而已,注解一定会存在
@After("@annotation(methodAnnotate) || bean(demoBean)")// ❌ 报错!编译能过,启动失败。注解可能不存在,spring无法绑定该注解。
public void aopMethod(JoinPoint joinPoint, MethodAnnotate methodAnnotate) {
....
}
bean
根据beanNam来匹配。支持
*
通配符
// 匹配所有以Controller结尾的Bean的所有方法(*可匹配任意长度字符)
@Pointcut("bean(*Controller)")
// 匹配名为 demoBean 或 demoBean* 的Bean的所有方法
@Pointcut("bean(demoBean) || bean(*Controller)")
5种通知类型
-
@Before、@After、@AfterReturning、@AfterThrowing,可选择声明
JoinPoint
参数。 -
@Around需要声明
ProceedingJoinPoint
参数。
前置通知(@Before)
在方法执行前通知
@Before("execOrder()")
public void doBefore(JoinPoint joinPoint) {
Console.log("doBefore...");
}
方法执行后通知(@After)
在目标方法执行后无论是否发生异常,执行通知,不能访问目标方法的执行的结果。
@After("execOrder()")
public void doAfter(JoinPoint joinPoint) {
Console.log("doAfter...");
}
环绕通知(@Around)
可以将要执行的方法(point.proceed())进行包裹执行,可以在前后添加需要执行的操作
/**
* 环绕
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("execOrder()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Console.log("doAround-start...");
Object proceed = proceedingJoinPoint.proceed();
Console.log("doAround-end...");
return proceed;
}
后置通知(@AfterReturning)
在方法正常执行完成进行通知,可以访问到方法的返回值的。
@AfterReturning(value = "execOrder()",returning = "a")
public void doAfterReturning(Object a) {
Console.log("doAfterReturning...");
System.out.println("返回值:"+a);
}
异常通知(@AfterThrowing)
在方法出现异常时进行通知,可以访问到异常对象,且可以指定在出现特定异常时在执行通知。
@AfterThrowing(pointcut="execOrder()",throwing="a")
public void doaction(Throwable a) {
Console.log("doAfterThrowing...");
System.out.println("目标方法中抛出的异常:"+a);
}
代码实现
介绍两种不同的写法
两种写法效果相同,建议用第二种,好读好理解。
1 通过@Pointcut
定义切点,由具体切面引用。
// 定义切点:
@Pointcut("execution(XXX)")
public void webLog(){}// 此处不写任何代码
// 定义切面(引用切点)
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
}
2 直接在切面上定义切点表达式
@After("execution(XXX)")
public void doAfter() throws Throwable {
}
1.maven中引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.编辑切面类(简单代码示例)
@Aspect
@Component
@Slf4j
public class AspectAop {
/** 换行符 */
private static final String LINE_SEPARATOR = System.lineSeparator();
// 定义切点:
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void webLog(){}
/**
* 在切点之前织入
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
}
/**
* 环绕
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 环绕前 doing...
Object result = proceedingJoinPoint.proceed();
// 环绕后 doing...
return result;
}
}
执行顺序问题:
程序正常:
doAround-start...
→doBefore...
→RunningMethod......
→doAfterReturning...
→doAfter...
→doAround-end...
程序异常:
doAround-start...
→doBefore...
→RunningMethod......
→doAfterThrowing...
→doAfter...
图示:
对于同一个aop切点,@Order越小越先执行。
当方法异常,不会执行@Around-end
拓展
JoinPoint获取方法(Method)相关数据
例如:类、方法、方法的参数、参数值...
代码
// 传入参数:`joinPoint`,类型 是 `JoinPoint.class`类型(包含`ProceedingJoinPoint.class`)
Console.log("\n============= start AOP =============");
// 可直接获取的信息:类、方法、参数值:
// 类名
Class<?> aClass = joinPoint.getTarget().getClass();
// 方法名
String name = joinPoint.getSignature().getName();
// 参数值
Object[] args = joinPoint.getArgs();
// 通过 Method 方法获取方法的签名信息:
// 方法签名(类似Method)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取 方法
Method method = signature.getMethod();
// 获取参数名称
String[] parameterNames = signature.getParameterNames();
// 参数类型列表
Class<?>[] parameterTypes = signature.getParameterTypes();
String[] argsTypes = Arrays.stream(parameterTypes).map(Class::getName).toArray(String[]::new);
// 抛出的异常 类
Class<?>[] exceptionTypes = signature.getExceptionTypes();
String[] exTypes = Arrays.stream(exceptionTypes).map(Class::getName).toArray(String[]::new);
log.info("类:{}",aClass.getName());
log.info("方法名:{}",name);
log.info("方法注解:{}", Arrays.toString(method.getAnnotations()));
log.info("参数类型列表:{}", Arrays.toString(argsTypes));
log.info("参数名列表:{}", Arrays.toString(parameterNames));
log.info("参数值列表:{}", Arrays.toString(args));
log.info("抛出的异常类列表:{}", Arrays.toString(exTypes));
Console.log("\n============= end AOP =============");
问题
AOP有两种代理模式:
jdk动态代理和CGLIB代理。
通过接口继承的方法使用jdk动态代理(仅支持实现了接口的方法)。
非接口的动态代理为CGLIB代理。
本质都是生成一个代理类,进行代理。性能CGLIB高,通用性高,JDK动态代理的优势是实现简单。