文章目录
- Spring 面向切面编程
- AOP 概念
- AOP 代理
- @AspectJ 支持
- 启用@AspectJ 支持
- 声明一个切面
- 声明一个切入点
- 表达式标签(10种)
- 1. execution标签
- 2. within标签
- 3. this
- 4. target
- within、this、target对比
- 5. args
- 6. @within
- 7. @target
- 8. @args
- 9. @annotation
- 10. bean
- 切点表达式的组合
- 声明通知
- @Before:前置通知
- JoinPoint:连接点信息
- @After:后置通知
- 介绍
- 特点
- @AfterReturning:返回通知
- 介绍
- 特点
- @AfterThrowing:异常通知
- 介绍
- 特点
- 案例
- @Around:环绕通知
- 介绍
- 特点
- 案例
- ProceedingJoinPoint:环绕通知连接点信息
- AOP示例
- 重试调用
- 异常处理
- 性能监测
- 缓存
- 参考资料
Spring 面向切面编程
面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。OOP 中模块化的关键单位是类,而 AOP 中模块化的单位是切面。切面能够实现跨越多种类型和对象的关注点(例如事务管理)的模块化。
Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器不依赖于 AOP(意味着如果您不想使用AOP,则不需要使用 AOP),但 AOP 补充了 Spring IoC 以提供一个非常强大的中间件解决方案 。
Spring 通过使用基于xml配置文件方法或**@AspectJ 注释样式**提供了编写自定义切面的简单而强大的方法。这两种风格都提供了完全类型化的通知和 AspectJ 切入点语言的使用,同时仍然使用 SpringAOP 进行编织 。
AOP 在 Spring 框架中用于 :
- 提供声明式企业服务。最重要的此类服务是**声明式事务管理 **
- 让用户实现自定义切面,用 AOP 补充他们对 OOP 的使用。(有很多应用)
AOP 概念
让我们从定义一些核心 AOP 概念和术语开始。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用自己的术语会更加混乱。
- Join point(连接点):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
连接点由两个信息确定:
- 方法(表示程序执行点,即在哪个目标方法)
- 相对点(表示方位,即目标方法的什么位置,比如调用前,后等)
简单来说,连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。
- Advice(通知):切面在特定连接点采取的操作。不同类型的通知包括“环绕”、“前置”和“后置”通知。(通知类型将在后面讨论。)许多AOP框架,包括Spring,将通知建模为拦截器并在连接点周围维护一个拦截器链。
- Pointcut(切入点):匹配连接点的谓词。Advice与切入点表达式相关联,并在与切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。说白了就是:用来指定需要将通知使用到哪些地方,比如需要用在哪些类的哪些方法上,切入点就是做这个配置的。
- Aspect(切面):通知(Advice)和切入点(Pointcut)的组合。切面来定义在哪些地方(Pointcut)执行什么操作(Advice)。
- Advisor(顾问、通知器):Advisor 其实它就是 Pointcut 与 Advice 的组合,Advice 是要增强的逻辑,而增强的逻辑要在什么地方执行是通过Pointcut来指定的,所以 Advice 必需与 Pointcut 组合在一起,这就诞生了 Advisor 这个类,spring Aop中提供了一个Advisor接口将Pointcut 与 Advice 的组合起来。
- Introduction(引介):是指在不更改源代码的情况,给一个现有类增加属性、方法,以及让现有类实现其它接口或指定其它父类等,从而改变类的静态结构。Spring AOP通过采代理加拦截器的方式来实现的,可以通过拦截器机制使一个实有类实现指定的接口。
- Target object(目标对象):被一个或多个切面通知的对象。也称为“通知对象”。由于Spring AOP是使用运行时代理实现的,所以这个对象始终是一个被代理的对象。
- AOP proxy(AOP代理):由AOP框架创建的对象,用于实现切面契约(建议方法执行等)。在SpringFramework中,AOP代理是JDK动态代理或CGLIB代理。
- Weaving(编织):将切面与其他应用程序类型或对象联系起来以创建通知对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP与其他纯Java AOP框架一样,在运行时执行编织。
Spring AOP包括以下类型的通知:
- 前置通知(Before advice):在连接点之前运行的通知,但没有能力阻止执行流继续到连接点(除非它抛出异常)。
- 返回通知(After returning advice):在连接点正常完成后运行的通知(例如,如果方法返回而没有抛出异常)。
- 异常通知(After throwing advice):如果方法通过抛出异常退出,则运行通知。
- 后置通知(After (finally) advice):不管连接点退出的方式(正常或异常返回)都将运行的通知。
- 环绕通知(Around advice):环绕连接点的通知,例如方法调用。这是最有力的通知。环绕通知可以在方法调用前后执行自定义行为。它还负责选择是继续连接点还是通过返回自己的返回值或抛出异常来缩短通知的方法执行。
AOP 代理
Spring AOP 默认为 AOP 代理使用标准的JDK 动态代理。这允许代理任何接口(或接口集)。
Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于编程接口而不是类是一种很好的做法,因此业务类通常实现一个或多个业务接口。在需要建议未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下(希望很少见),可以强制使用 CGLIB。
@AspectJ 支持
@AspectJ指的是一种将切面声明为带有注释的常规java类的风格。
启用@AspectJ 支持
要使用Java 启用@AspectJ 支持 @Configuration ,请添加 @EnableAspectJAutoProxy 注释,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is {@code false}.
*/
boolean proxyTargetClass() default false;
/**
* Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
* for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
* Off by default, i.e. no guarantees that {@code AopContext} access will work.
* @since 4.3.1
*/
boolean exposeProxy() default false;
}
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
声明一个切面
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
声明一个切入点
切入点确定感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP 仅支持 Spring bean 的方法执行连接点,因此您可以将切入点视为匹配 Spring bean 上方法的执行。一个切入点声明有两个部分:一个切入点签名以及一个切入点表达式,它确定我们对哪些方法执行感兴趣。在@AspectJ 注释样式中,一个切入点签名由常规方法定义提供,切入点表达式通过 @Pointcut 注解
(作为切入点签名的方法必须有 void 返回类型)。
一个例子可能有助于明确切入点签名和切入点表达式之间的区别。下面的示例定义了一个名为anyOldTransfer的切入点,该切入点与名为 transfer 的任何方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
表达式标签(10种)
- execution:用于匹配方法执行的连接点
- within:用于匹配指定类型内的方法执行
- this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
- target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
- args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
- @within:用于匹配所以持有指定注解类型内的方法
- @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
- @args:用于匹配当前执行的方法传入的参数持有指定注解的执行
- @annotation:用于匹配当前执行方法持有指定注解的方法
- bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法
1. execution标签
用于匹配方法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- 其中带 ?号的 modifiers-pattern?,declaring-type-pattern?,hrows-pattern?是可选项
- ret-type-pattern,name-pattern, parameters-pattern是必选项
- modifier-pattern? 修饰符匹配,如public 表示匹配公有方法
- ret-type-pattern 返回值匹配,* 表示任何返回值,全路径的类名等
- declaring-type-pattern? 类路径匹配
- name-pattern 方法名匹配,* 代表所有,set*,代表以set开头的所有方法
- (param-pattern) 参数匹配,指定方法参数(声明的类型),(…)代表所有参数,(*,String)代表第一个参数为任何值,第二个为String类型,(…,String)代表最后一个参数是String类型
- throws-pattern? 异常类型匹配
AspectJ类型匹配的通配符:
- *****:匹配任何数量字符
- …:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数(0个或者多个参数)
- **+:**匹配指定类型及其子类型;仅能作为后缀放在类型模式后边
以下示例显示了一些常见的切入点表达式 :
- 任何公共方法的执行:
execution(public * *(..))
- 任何名称以 set开头的方法的执行 :
execution(* set*(..))
- AccountService 接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
- service 包中定义的任何方法的执行
execution(* com.xyz.service.*.*(..))
- service包及其子包之一中定义的任何方法的执行
execution(* com.xyz.service..*.*(..))
- service包及其子包下IPointcutService接口及子类型的任何无参方法
execution(* com.xyz.service..IPointcutService+.*())
- 配置Service中只有1个参数的且参数类型是String的方法
execution(* Service.*(String))
- 配置Service中最后1个参数类型是String的方法
execution(* Service.*(..,String))
2. within标签
用于匹配指定类型内的方法执行(严格类型匹配)
匹配规则:
target.getClass().equals(within表达式中指定的类型)
- 匹配Service1中定义的所有方法,不包含其子类中的方法
within(com.xyz.service.Service1)
- 匹配service包内的任何连接点(方法只在 Spring AOP 中执行)
within(com.xyz.service.*)
- 匹配service包或其子包之一中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service..*)
- 匹配
com.xyz.service
包下IPointcutService
类型及其子类型的方法
within(com.xyz.service.IPointcutService+)
3. this
this(类型全限定名)
:通过aop创建的代理对象的类型是否和this中指定的类型匹配;注意判断的目标是代理对象;this中使用的表达式必须是类型全限定名,不支持通配符。
匹配规则:
如:this(x),则代理对象proxy满足下面条件时会匹配
x.getClass().isAssignableFrom(proxy.getClass());
- 匹配代理是AccountServiceImpl类型的所有方法
this(com.xyz.service.AccountServiceImpl)
- 匹配代理实现IAccountService接口的所有方法
this(com.xyz.service.IAccountService)
举例说明
package com.example.springlearn.aop;
public interface I1 {
void m1();
}
package com.example.springlearn.aop;
public class Service3 implements I1 {
@Override
public void m1() {
System.out.println("我是m1");
}
}
package com.example.springlearn.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AspectTest3 {
//@1:匹配proxy是Service3类型的所有方法
@Pointcut("this(Service3)")
public void pc() {
}
@Before("pc()")
public void beforeAdvice(JoinPoint joinpoint) {
System.out.println(joinpoint);
}
}
@Test
public void test3() {
Service3 target = new Service3();
AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
proxyFactory.setTarget(target);
//获取目标对象上的接口列表
Class<?>[] allInterfaces = ClassUtils.getAllInterfaces(target);
//设置需要代理的接口
proxyFactory.setInterfaces(allInterfaces);
proxyFactory.addAspect(AspectTest3.class);
//proxyFactory.setProxyTargetClass(true);
//获取代理对象
Object proxy = proxyFactory.getProxy();
//调用代理对象的方法
((I1) proxy).m1();
System.out.println("proxy是否是jdk动态代理对象:" + AopUtils.isJdkDynamicProxy(proxy));
System.out.println("proxy是否是cglib代理对象:" + AopUtils.isCglibProxy(proxy));
System.out.println("代理类" + proxy.getClass());
System.out.println("代理类父类" + proxy.getClass().getSuperclass());
System.out.println("代理类实现的接口" + Arrays.toString(proxy.getClass().getInterfaces()));
//判断代理对象是否是Service3类型的
System.out.println(Service3.class.isAssignableFrom(proxy.getClass()));
}
输出:(没有代理)
我是m1
proxy是否是jdk动态代理对象:true
proxy是否是cglib代理对象:false
代理类class com.sun.proxy.$Proxy76
代理类父类class java.lang.reflect.Proxy
代理类实现的接口[interface com.example.springlearn.aop.I1, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.core.DecoratingProxy]
false
设置使用CGLIB动态代理:proxyFactory.setProxyTargetClass(true);
输出:(有代理)
我是m1
proxy是否是jdk动态代理对象:false
proxy是否是cglib代理对象:true
代理类class com.example.springlearn.aop.Service3$$EnhancerBySpringCGLIB$$3a49cc06
代理类父类class com.example.springlearn.aop.Service3
代理类实现的接口[interface com.example.springlearn.aop.I1, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.cglib.proxy.Factory]
true
4. target
target(类型全限定名)
:判断目标对象的类型是否和指定的类型匹配;注意判断的是目标对象的类型;表达式必须是类型全限定名,不支持通配符。
匹配规则:
如:target(x),则目标对象target满足下面条件时会匹配
x.getClass().isAssignableFrom(target.getClass());
- 匹配目标对象是 AccountServiceImpl类型的任何连接点(仅在 Spring AOP 中执行方法)
target(com.xyz.service.AccountServiceImpl)
- 匹配目标对象实现IAccountService接口的任何连接点(仅在 Spring AOP 中执行方法)
target(com.xyz.service.IAccountService)
within、this、target对比
表达式标签 | 判断的对象 | 判断规则(x:指表达式中指定的类型) |
within | target对象 | target.getClass().equals(x) |
this | proxy对象 | x.getClass().isAssignableFrom(proxy.getClass()) |
target | target对象 | x.getClass().isAssignableFrom(target.getClass()) |
5. args
args(参数类型列表)
匹配当前执行的方法传入的参数是否为args中指定的类型;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,不支持通配符;args属于动态切入点,也就是执行方法的时候进行判断的,这种切入点开销非常大,非特殊情况最好不要使用
- 任何带有单个参数的连接点(仅在 Spring AOP 中执行方法),并且在运行时传递的参数是
Serializable :
args(java.io.Serializable)
- 匹配只有2个参数的且第2个参数类型是String的方法:
args(*,Sring)
- 匹配最后1个参数类型是String的方法:
args(..,String)
6. @within
@within(注解类型)
:匹配指定的注解内定义的方法。
匹配规则:
被调用的目标方法Method对象.getDeclaringClass().getAnnotation(within中指定的注解类型) != null
- 目标对象方法具有 @Transactional 注释的任何连接点(仅在 Spring AOP 中执行方法) :
@target(org.springframework.transaction.annotation.Transactional)
7. @target
@target(注解类型)
:判断目标对象target类型上是否有指定的注解;@target中注解类型也必须是全限定类型名。
匹配规则:
target.class.getAnnotation(指定的注解类型) != null
2种情况可以匹配
- 注解直接标注在目标类上
- 注解标注在父类上,但是注解必须是可以继承的,即定义注解的时候,需要使用
@Inherited
标注
使用方法:
- 目标对象具有 @Transactional 注释的任何连接点(仅在 Spring AOP 中执行方法) :
@target(org.springframework.transaction.annotation.Transactional)
8. @args
@args(注解类型)
:方法参数所属的类上有指定的注解;注意不是参数上有指定的注解,而是参数类型的类上有指定的注解。
使用方法:
- 任何带有单个参数的连接点(仅在 Spring AOP 中执行方法),并且传递的参数的运行时类型具有
@Classified
注释:
@args(com.xyz.security.Classified)
9. @annotation
@annotation(注解类型)
:匹配被调用的方法上有指定的注解
使用方法:
- 任何连接点(方法仅在 Spring AOP 中执行),其中执行的方法有一个 @Transactional 注解:
@annotation(org.springframework.transaction.annotation.Transactional)
10. bean
bean(bean名称)
:这个用在spring环境中,匹配容器中指定名称的bean。
使用方法:
- bean名为
tradeService
的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):
bean(tradeService)
- Spring bean 上具有与通配符表达式
*Service
匹配的名称的任何连接点(仅在 Spring AOP 中执行方法)
bean(*Service)
切点表达式的组合
Pointcut定义时,还可以使用&&、||、!运算符。
- &&:多个匹配都需要满足
- ||:多个匹配中只需满足一个
- !:匹配不满足的情况下
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
声明通知
**@Aspect中有5种通知 **
- @Before:前置通知, 在方法执行之前执行
- @Aroud:环绕通知, 围绕着方法执行
- @After:后置通知, 在方法执行之后执行
- @AfterReturning:返回通知, 在方法返回结果之后执行
- @AfterThrowing:异常通知, 在方法抛出异常之后
这几种通知用起来都比较简单,都是通过注解的方式,将这些注解标注在@Aspect类的方法上,这些方法就会对目标方法进行拦截。
@Before:前置通知
在方法执行之前执行
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck(JoinPoint joinPoint) {
// ...
}
}
- 类上需要使用
@Aspect
标注 - 任意方法上使用
@Before
标注,将这个方法作为前置通知,目标方法被调用之前,会自动回调这个方法 - 被
@Before
标注的方法参数可以为空,或者为JoinPoint
类型,当为JoinPoint
类型时,必须为第一个参数 - 被
@Before
标注的方法名称可以随意命名,符合java规范就可以,其他通知也类似
JoinPoint:连接点信息
提供访问当前被通知方法的目标对象、代理对象、方法参数等数据:
package org.aspectj.lang;
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
Object getThis(); //返回AOP代理对象
Object getTarget(); //返回目标对象
Object[] getArgs(); //返回被通知方法参数列表,也就是目前调用目标方法传入的参数
Signature getSignature(); //返回当前连接点签名,这个可以用来获取目标方法的详细信息,如方法Method对象等
SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
String getKind(); //连接点类型
StaticPart getStaticPart(); //返回连接点静态部分
}
获取连接点的方法信息:
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method curMethod = methodSignature.getMethod();
@After:后置通知
介绍
后置通知,在方法执行之后执行,用法和前置通知类似。它通常用于释放资源和类似目的。
特点
- 不管目标方法是否有异常,后置通知都会执行
- 这种通知无法获取方法返回值
- 可以使用
JoinPoint
作为方法的第一个参数,用来获取连接点的信息
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock(JoinPoint joinPoint) {
// ...
}
}
@AfterReturning:返回通知
介绍
返回通知,在方法返回结果之后执行
特点
- 可以获取到方法的返回值
- 当目标方法返回异常的时候,这个通知不会被调用,这点和@After通知是有区别的
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",returning="retVal")
public void doAccessCheck(JoinPoint joinPoint,Object retVal) {
// ...
}
}
@AfterThrowing:异常通知
介绍
在方法抛出异常之后会回调@AfterThrowing
标注的方法。
@AfterThrowing标注的方法可以指定异常的类型,当被调用的方法触发该异常及其子类型的异常之后,会触发异常方法的回调。也可以不指定异常类型,此时会匹配所有异常。
特点
不论异常是否被异常通知捕获,异常还会继续向外抛出。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
案例
捕获非受检异常,发送邮件通知
@Around:环绕通知
介绍
环绕通知会包裹目标目标方法的执行,可以在通知内部调用ProceedingJoinPoint.process
方法继续执行下一个拦截器。
用起来和@Before类似,但是有2点不一样
- 若需要获取目标方法的信息,需要将ProceedingJoinPoint作为第一个参数
- 通常使用Object类型作为方法的返回值,返回值也可以为void
特点
环绕通知比较特殊,其他4种类型的通知都可以用环绕通知来实现。
案例
通过环绕通知来统计方法的耗时。
package com.xyz.aop.test;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
@Aspect
public class AroundAspect3 {
@Pointcut("execution(* com.xyz.myapp.CommonPointcuts.*(..))")
public void pc() {
}
@Around("com.xyz.aop.test.AroundAspect3.pc()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取连接点签名
Signature signature = joinPoint.getSignature();
//将其转换为方法签名
MethodSignature methodSignature = (MethodSignature) signature;
//通过方法签名获取被调用的目标方法
Method method = methodSignature.getMethod();
long startTime = System.nanoTime();
//调用proceed方法,继续调用下一个通知
Object returnVal = joinPoint.proceed();
long endTime = System.nanoTime();
long costTime = endTime - startTime;
//输出方法信息
System.out.println(String.format("%s,耗时(纳秒):%s", method.toString(), costTime));
//返回方法的返回值
return returnVal;
}
}
ProceedingJoinPoint:环绕通知连接点信息
用于环绕通知,内部主要关注2个方法,一个有参的,一个无参的,用来继续执行拦截器链上的下一个通知。
package org.aspectj.lang;
import org.aspectj.runtime.internal.AroundClosure;
public interface ProceedingJoinPoint extends JoinPoint {
/**
* 继续执行下一个通知或者目标方法的调用
*/
public Object proceed() throws Throwable;
/**
* 继续执行下一个通知或者目标方法的调用
*/
public Object proceed(Object[] args) throws Throwable;
}
AOP示例
重试调用
由于并发问题等其它问题,业务服务的执行有时会失败。如果重试该操作,下一次尝试很可能会成功。对于适合在这种情况下重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到异常.
为了优化切面以使其仅重试幂等操作,我们可以定义以下 Idempotent 注释:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
异常处理
比如说非受检异常数据库连接池不可用异常、网络中断异常我们可以用异常通知处理,我们可以
通过电子邮件的方式将系统中的异常信息转发给相关人员,并记录到日志。
import com.alibaba.druid.pool.DataSourceNotAvailableException;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(pointcut="within(com.xyz.service.*)",throwing="ex")
public void doRecoveryActions(DataSourceNotAvailableException ex) {
// 记录日志。发送邮件等处理
}
}
我们还可以定义一个注解,用于将方法记录到数据库中。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysErrorLog {
/**
* 业务类型
*
* @return :业务类型
*/
public String businessType() default "unknown";
}
@Component
@Aspect
@Slf4j
public class SysErrorLogAop {
@Resource
private ISysErrorLogService sysErrorLogService;
@Resource
private Gson gson;
@Pointcut("@annotation(com.test.aop.annotation.SysErrorLog)")
public void logPointCut(){
}
@AfterThrowing(pointcut = "logPointCut()",throwing = "ex")
public void handThrowing(JoinPoint joinPoint,Throwable ex){
//获取连接点的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
SysErrorLog errorLog = method.getAnnotation(SysErrorLog.class);
String businessType = errorLog.businessType();
//获取参数
Object[] args = joinPoint.getArgs();
String argumentsJson = gson.toJson(args);
sysErrorLogService.addSysErrorLogInfo(methodName,argumentsJson,businessType,ex);
}
}
性能监测
统计方法调用时间并做相应处理。
package com.xyz.aop.test;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
@Aspect
public class AroundAspect3 {
@Pointcut("execution(* com.xyz.myapp.CommonPointcuts.*(..))")
public void pc() {
}
@Around("com.xyz.aop.test.AroundAspect3.pc()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取连接点签名
Signature signature = joinPoint.getSignature();
//将其转换为方法签名
MethodSignature methodSignature = (MethodSignature) signature;
//通过方法签名获取被调用的目标方法
Method method = methodSignature.getMethod();
long startTime = System.nanoTime();
//调用proceed方法,继续调用下一个通知
Object returnVal = joinPoint.proceed();
long endTime = System.nanoTime();
long costTime = endTime - startTime;
//输出方法信息
System.out.println(String.format("%s,耗时(纳秒):%s", method.toString(), costTime));
//返回方法的返回值
return returnVal;
}
}
缓存
AOP应用的另一个主要场景是为系统透明地添加缓存支持。缓存可以在很大程度上提升系统的性能,但它不是业务需求,而是系统需求。在现有方法论的基础上为系统添加缓存支持,就会因为系统中缓存需求的广泛分布,造成实现上的代码散落。
为了避免需要添加的缓存实现逻辑影响业务逻辑的实现,我们可以让缓存的实现独立于业务对象的实现之外,将系统中的缓存需求通过AOP的Aspect进行封装,只在系统中某个点确切需要缓存支持的情况下,才为其织入。
用于缓存的Aspect原型示例
@Aspect
public class CachingAspect{
private static Map cache=new LRUMap(5);
@Around("...")
public Object docache(ProceedingJoinpoint pjp, Object key) {
if(cache.containskey(key)){
return cache.get(key);
}else{
object retValue=pjp.proceed();
cache.put(key,retValue);
return retValue;
}
}
}
在没有使用AOP之前,要为系统某个地方加入缓存的话,恐怕也是以差不多的逻辑实现的。
不过,现在不需要这么做了,原因如下。
- 现在已经有许多现成的Caching产品实现,如EhCache、Redis等。
- Spring项目提供了对现有Caching产品的集成,这样就可以通过外部声明的方式为系统中的Joinpoint添加Caching支持。
- spring中通过简单的配置,就可以使用注解
@Cacheable
、@CachePut
、@CacheEvict
了。
参考资料
- [1] Spring5.3.9框架官方文档
- [2] Spring揭秘.pdf