一、定义切面
@Aspect 使用注解定义一个切面
@Aspect
@Component
public class TestAspect {
}
二、Advice 的类型
- @Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;
- @AfterReturning:该注解标注的方法在业务模块代码执行之后执行;
- @AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;
- @After:该注解标注的方法在所有的 Advice 执行完成后执行,无论业务模块是否抛出异常,类似于 finally 的作用;
- @Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,通知的第一个参数必须是 ProceedingJoinPoint 类型。在通知体内,调用 ProceedingJoinPoint 的 proceed () 方法使得连接点方法执行如果不调用 proceed () 方法,连接点方法则不会执行。无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用;
三、切点表达式
1. execution
execution表达式的作用是匹配指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且在 Spring 中,大部分需要使用 AOP 的业务场景也只需要达到方法级别即可,因而 execution 表达式的使用是最为广泛的。如下是 execution 表达式的语法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
这里问号表示当前项可以有也可以没有,其中各项的语义如下:
- modifiers-pattern:方法的可见性,如 public,protected;
- ret-type-pattern:方法的返回值类型,如 int,void 等;
- declaring-type-pattern:方法所在类的全路径名,如 com.spring.Aspect;
- name-pattern:方法名类型,如 buisinessService ();
- param-pattern:方法的参数类型,如 java.lang.String;
- throws-pattern:方法抛出的异常类型,如 java.lang.Exception;
例如:com.yunfang.qjep.test 包下面 Business 类中的任意 doSomeThing 方法
execution(* com.yunfang.qjep.test.Business.doSomeThing(..))
通配符
- *通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词
(上述例子中的 * 表示返回值为任意类型) - ..通配符,该通配符表示 0 个或多个项,主要用于 declaring-type-pattern 和 param-pattern 中,如果用于 declaring-type-pattern 中,则表示匹配当前包及其子包,如果用于 param-pattern 中,则表示匹配 0 个或多个参数。
(上述例子中的 ..表示方法中的参数可以有 0 个或多个参数)
2. @annotation
@annotation 表示式的作用是匹配使用指定注解标注的方法。其使用语法如下:
@annotation(annotation-type)
如下示例表示匹配使用 com.spring.annotation.BusinessAspect 注解标注的方法:
@annotation(com.spring.annotation.BusinessAspect)
3. within
within 表达式式的作用是匹配指定的类(可使用通配符)。
within(declaring-type-pattern)
within 表达式只能指定到类级别,如下示例表示匹配 com.yunfang.qjep.test.Business 中的所有方法:
within(com.yunfang.qjep.test.Business)
4 @within
@within 表达式的作用是匹配指定注解的类,其使用语法如下所示:
@within(annotation-type)
如下所示示例表示匹配使用 com.spring.annotation.BusinessAspect 注解标注的类:
@within(com.spring.annotation.BusinessAspect)
5. args
args 表达式的作用是匹配指定参数类型和指定参数数量的方法,无论其类路径或者是方法名是什么。如下是 args 表达式的语法:
args(param-pattern)
如下示例表示匹配所有只有一个参数,并且参数类型是 java.lang.String 类型的方法:
args(java.lang.String)
也可以使用通配符,但这里通配符只能使用…,而不能使用 *。如下是使用通配符的实例,该切点表达式将匹配第一个参数为 java.lang.String,最后一个参数为 java.lang.Integer,并且中间可以有任意个数和类型参数的方法:
args(java.lang.String,..,java.lang.Integer)
6. @args
@args 表达式的作用是匹配指定注解标注的类作为某个方法的参数时该方法将会被匹配。如下是 @args 注解的语法:
@args(annotation-type)
如下示例表示匹配使用了 com.spring.annotation.FruitAspect 注解标注的类作为参数的方法:
@args(com.spring.annotation.FruitAspect)
7. this 和 target
this 和 target 需要放在一起进行讲解,主要目的是对其进行区别。this 和 target 表达式中都只能指定类或者接口,在面向切面编程规范中,this 表示匹配调用当前切点表达式所指代对象方法的对象,target 表示匹配切点表达式指定类型的对象。比如有两个类 A 和 B,并且 A 调用了 B 的某个方法,如果切点表达式为 this (B),那么 A 的实例将会被匹配,也即其会被使用当前切点表达式的 Advice 环绕;如果这里切点表达式为 target (B),那么 B 的实例也即被匹配,其将会被使用当前切点表达式的 Advice 环绕。
在讲解 Spring 中的 this 和 target 的使用之前,首先需要讲解一个概念:业务对象(目标对象)和代理对象。对于切面编程,有一个目标对象,也有一个代理对象,目标对象是我们声明的业务逻辑对象,而代理对象是使用切面逻辑对业务逻辑进行包裹之后生成的对象。如果使用的是 Jdk 动态代理,那么业务对象和代理对象将是两个对象,在调用代理对象逻辑时,其切面逻辑中会调用目标对象的逻辑;如果使用的是 Cglib 代理,由于是使用的子类进行切面逻辑织入的,那么只有一个对象,即织入了代理逻辑的业务类的子类对象,此时是不会生成业务类的对象的。
在 Spring 中,其对 this 的语义进行了改写,即如果当前对象生成的代理对象符合 this 指定的类型,那么就为其织入切面逻辑。简单的说就是,this 将匹配代理对象为指定类型的类。target 的语义则没有发生变化,即其将匹配业务对象为指定类型的类。
可以看出,this 和 target 的使用区别其实不大,大部分情况下其使用效果是一样的,但其区别也还是有的。Spring 使用的代理方式主要有两种:Jdk 代理和 Cglib 代理。
- 如果目标对象被代理的方法是其实现的某个接口的方法,那么将会使用 Jdk 代理生成代理对象,此时代理对象和目标对象是两个对象,并且都实现了该接口;
- 如果目标对象是一个类,并且其没有实现任意接口,那么将会使用 Cglib 代理生成代理对象,并且只会生成一个对象,即 Cglib 生成的代理类的对象。
结合上述两点说明,这里理解 this 和 target 的异同就相对比较简单了。我们这里分三种情况进行说明:
- this (SomeInterface) 或 target (SomeInterface):这种情况下,无论是对于 Jdk 代理还是 Cglib 代理,其目标对象和代理对象都是实现 SomeInterface 接口的(Cglib 生成的目标对象的子类也是实现了 SomeInterface 接口的),因而 this 和 target 语义都是符合的,此时这两个表达式的效果一样;
- this (SomeObject) 或 target (SomeObject),这里 SomeObject 没实现任何接口:这种情况下,Spring 会使用 Cglib 代理生成 SomeObject 的代理类对象,由于代理类是 SomeObject 的子类,子类的对象也是符合 SomeObject 类型的,因而 this 将会被匹配,而对于 target,由于目标对象本身就是 SomeObject 类型,因而这两个表达式的效果一样;
- this (SomeObject) 或 target (SomeObject),这里 SomeObject 实现了某个接口:对于这种情况,虽然表达式中指定的是一种具体的对象类型,但由于其实现了某个接口,因而 Spring 默认会使用 Jdk 代理为其生成代理对象,Jdk 代理生成的代理对象与目标对象实现的是同一个接口,但代理对象与目标对象还是不同的对象,由于代理对象不是 SomeObject 类型的,因而此时是不符合 this 语义的,而由于目标对象就是 SomeObject 类型,因而 target 语义是符合的,此时 this 和 target 的效果就产生了区别;这里如果强制 Spring 使用 Cglib 代理,因而生成的代理对象都是 SomeObject 子类的对象,其是 SomeObject 类型的,因而 this 和 target 的语义都符合,其效果就是一致的。
8. 定义切点
- 直接在Advice 类型的注解中定义
@Before("execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void before() throws Throwable {
System.out.println("@Before通知执行................................");
}
- 使用@Pointcut
@Pointcut("execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void poincut() {}
@Before("poincut()")
public void before(){
System.out.println("@Before通知执行................................");
}
9.组合表达式
AspectJ 使用 且(&&)、或(||)、非(!)来组合切入点表达式。
在 Schema 风格下,由于在 XML 中使用 “&&” 需要使用转义字符 “&&” 来代替之,很不方便,因此 Spring AOP 提供了 and、or、not 来代替 &&、||、!
三、执行顺序
@Aspect
@Component
public class TestAspect {
@Pointcut("execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void poincut() {}
@Before("poincut()")
public void before(){
System.out.println("@Before通知执行................................");
}
@AfterReturning("poincut()")
public void afterReturning() {
System.out.println("@AfterReturning通知执行................................");
}
@Around("poincut()")
public Object Around(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around通知执行1................................");
Object proceed = point.proceed();//切点方法
System.out.println("@Around通知执行2................................");
return proceed;
}
@AfterThrowing("poincut()")
public void afterThrowing() {
System.out.println("@AfterThrowing通知执行................................");
}
@After("poincut()")
public void after() {
System.out.println("@After通知执行................................");
}
}
- 正常结果:
@Around通知执行1................................
@Before通知执行................................
business doSomeThing.....测试测试
@AfterReturning通知执行................................
@After通知执行................................
@Around通知执行2................................
- 业务抛异常结果:
@Around通知执行1................................
@Before通知执行................................
business doSomeThing.....测试测试
@AfterThrowing通知执行................................
@After通知执行................................
- @Around 使用影响
- 不使用 @Around 的时候执行情况
@Aspect
@Component
public class TestAspect {
@Pointcut("execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void poincut() {}
@Before("poincut()")
public void before(){
System.out.println("@Before通知执行................................");
}
@AfterReturning("poincut()")
public void afterReturning() {
System.out.println("@AfterReturning通知执行................................");
}
@AfterThrowing("poincut()")
public void afterThrowing() {
System.out.println("@AfterThrowing通知执行................................");
}
@After("poincut()")
public void after() {
System.out.println("@After通知执行................................");
}
}
@Before通知执行................................
business doSomeThing.....测试测试
@AfterReturning通知执行................................
@After通知执行................................
- 使用 @Around 时不使用 ProceedingJoinPoint 调用业务模块的代码
@Aspect
@Component
public class TestAspect {
@Pointcut("execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void poincut() {}
@Before("poincut()")
public void before(){
System.out.println("@Before通知执行................................");
}
@Around("poincut()")
public Object Around(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around通知执行1................................");
//Object proceed = point.proceed();//切点方法
System.out.println("@Around通知执行2................................");
return null;
}
@AfterReturning("poincut()")
public void afterReturning() {
System.out.println("@AfterReturning通知执行................................");
}
@AfterThrowing("poincut()")
public void afterThrowing() {
System.out.println("@AfterThrowing通知执行................................");
}
@After("poincut()")
public void after() {
System.out.println("@After通知执行................................");
}
}
@Around通知执行1................................
@Around通知执行2................................
四、通知参数的获取
有很多场景下,我们需要获取连接点的方法参数,传递到 Advice 中以作判断、处理等。
通过切入点表达式可以将相应的参数自动传递给通知方法,例如上文中将返回值和异常传递给通知方法,以及通过切入点标识符的”arg” 属性传递运行时参数。
需要注意的是,在 Spring AOP 中,execution 和 bean 指示符不支持自动传递参数。不过,可以利用组合表达式达到目的:
@Before(value="execution(* com.yunfang.qjep.test.Business.doSomeThing(..)) && args(str)", argNames="str")
public void before1(String str) {
System.out.println("@Before通知执行................................");
System.out.println("param:" + str);
}
首先 execution(* com.yunfang.qjep.test.Business.doSomeThing(…)) 匹配com.yunfang.qjep.test.Business 类中的任何方法名为 doSomeThing的方法,然后 args(str) 将查找通知方法上同名的参数,并在方法执行时(运行时)匹配传入的参数是使用该同名参数类型,即 java.lang.String;如果匹配将把该被通知参数传递给通知方法上同名参数。argNames 用于指定参数名称,避免参数绑定的二义性。
此外,Spring AOP 提供使用 org.aspectj.lang.JoinPoint 类型获取连接点数据,任何通知方法的第一个参数都可以是 JoinPoint (环绕通知是 ProceedingJoinPoint,JoinPoint 子类),当然第一个参数位置也可以是 JoinPoint.StaticPart 类型,这个只返回连接点的静态部分。
- JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
- ProceedingJoinPoint:用于环绕通知,使用 proceed () 方法来执行目标方法
- JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等
@Before(value="execution(* com.yunfang.qjep.test.Business.doSomeThing(..))")
public void before(JoinPoint joinPoint) throws Throwable {
System.out.println("@Before通知执行................................");
//获取目标方法参数信息
Object[] args = joinPoint.getArgs();
Arrays.stream(args).forEach(arg->{
try {
System.out.println(OBJECT_MAPPER.writeValueAsString(arg));//方法参数
} catch (Exception e) {
throw new RuntimeException("获取参数异常");
}
});
//aop代理对象
Object aThis = joinPoint.getThis();
System.out.println(aThis.toString()); //com.yunfang.qjep.test.Business@4e72b013
//被代理对象
Object target = joinPoint.getTarget();
System.out.println(target.toString()); //com.yunfang.qjep.test.Business@4e72b013
//获取连接点的方法签名对象
Signature signature = joinPoint.getSignature();
System.out.println(signature.toLongString()); //public void com.yunfang.qjep.test.Business.doSomeThing(java.lang.String)
System.out.println(signature.toShortString()); //Business.doSomeThing(..)
System.out.println(signature.toString()); //void com.yunfang.qjep.test.Business.doSomeThing(String)
//获取方法名
System.out.println(signature.getName()); //doSomeThing
//获取声明类型名
System.out.println(signature.getDeclaringTypeName()); //com.yunfang.qjep.test.Business
//获取声明类型 方法所在类的class对象
System.out.println(signature.getDeclaringType().toString()); //class com.yunfang.qjep.test.Business
//和getDeclaringTypeName()一样
System.out.println(signature.getDeclaringType().getName());//com.yunfang.qjep.test.Business
//连接点类型
String kind = joinPoint.getKind();
System.out.println(kind);//method-execution
//返回连接点静态部分
JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();
System.out.println(staticPart.toLongString()); //execution(public void com.yunfang.qjep.test.Business.doSomeThing(java.lang.String))
//attributes可以获取request信息 session信息等
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
System.out.println(request.getRequestURL().toString()); //http://localhost:8081/test/test/test
System.out.println(request.getRemoteAddr()); //0:0:0:0:0:0:0:1
System.out.println(request.getMethod()); //GET
System.out.println(request.getParameterMap()); //切点方法参数
//切点类名
String className = joinPoint.getTarget().getClass().getName();
//切点方法名称
String methodName = joinPoint.getSignature().getName();
//获取详细方法签名对象
//MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//Method method = methodSignature.getMethod();//获取到方法对象
//InquireAction action = method.getAnnotation(InquireAction.class); //获取方法上的注解
//String param1 = action.param1();//获取注解参数
//String inquireNo = request.getParameter(param1);//从request中获取对应参数
//attributes可以获取response信息
//HttpServletResponse response = attributes.getResponse();
System.out.println("before通知执行结束.........................");
}
五、使用注解获取切点方法返回的结果
@AfterReturning(
pointcut = "poincut()",
returning = "jsonResult"
)
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
System.out.println("切点方法执行结果:");
System.out.println(jsonResult);
}
六、使用注解获取切点方法抛出的异常
@AfterThrowing(
value = "poincut()",
throwing = "e"
)
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
e.printStackTrace();
}