SpringAOP
AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。
AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离,比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略。
AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。
主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。
简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。
核心概念
Spring AOP 的核心概念包括切面、连接点、通知、切点以及织入。下面我将对这些概念做一些简要的解释:
- 切面(Aspect):切面是一个模块化的横切关注点实现,它包括了连接点和通知。可以通过配置文件、注解等方式定义切面。
- 连接点(Joinpoint):程序中能够被切面插入的点,典型的连接点包括方法调用、方法执行过程中的某个时点等等。
- 通知(Advice):在连接点处执行的代码。通知分为各种类型,如前置通知、后置通知、环绕通知等。
- 切点(Pointcut):用于定义哪些连接点上应该应用通知。切点通过表达式进行定义,如匹配所有 public 方法或匹配某个包下的所有方法等。
- 织入(Weaving):指将切面应用到目标对象并创建新的代理对象的过程。织入可以在运行时完成,也可以在编译时完成。 Spring AOP 提供了两种织入方式:编译期织入和运行期织入。
除此之外,Spring AOP 还有其他常用的概念,如目标对象(Target)、代理对象(Proxy)等。目标对象是含有连接点的对象,而代理对象是 Spring AOP 创建的一个包含切面代码的对象。
如果AOP的环绕通知没定义,前置通知也不会执行。
而且环绕通知(@Around)的返回类型必须和原方法的返回类型相同。
ProceedingJoinPoint类
Proceedingjoinpoint 继承了JoinPoint,在JoinPoint的基础上暴露出 proceed(), 这个方法是AOP代理链执行的方法。
JoinPoint仅能获取相关参数,无法执行连接点。暴露出proceed()这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关),就能控制走代理链还是走自己拦截的其他逻辑。
:exclamation::注意
ProcedingJoinPoint
只能用在环绕通知里,其他通知只能用JoinPoint
可能拿不到注解的情况
补充:其实这根本不是拿不拿得到方法注解的问题,而是它拿的其实就是
signature.getClass()
的注解,和我们想拿到的方法的注解根本不相关。直接通过
signture
拿到的是代理的方法,而代理的方法是不会加上原方法的注解的。只有拿到原始的方法才能拿到方法上注解的值。(可能取到类上的某些注解)
@Before("cutPoint()")
public void before(JoinPoint joinPoint) throws NoSuchMethodException {
log.info("===== 前置通知开始");
String methodName = joinPoint.getSignature().getName();
log.info("所切方法名:[{}]",methodName);
Signature signature = joinPoint.getSignature();
Annotation[] annotations = signature.getClass().getAnnotations();
boolean hasAn = false;
for(Annotation a:annotations){
if(a instanceof MyAnnotation){
hasAn = true;
}
log.info("直接从Signature取注解:[{}]",a);
}
if(!hasAn){
log.info("直接从Signature取注解,取不到自定义注解");
}
Class[] parameterTypes = ((MethodSignature) signature).getParameterTypes();
Method readMethod = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), parameterTypes);
Annotation[] annotations1 = readMethod.getAnnotations();
for(Annotation a:annotations1){
log.info("从原始方法取注解:[{}]",a);
}
log.info("===== 前置通知结束");
}
使用Demo
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.Demo
@Aspect
@Slf4j
public class AnnotationAspectj {
/*
切入点签名
1.匹配方法
表示匹配所有方法
1)execution(* *(..))
//表示匹配com.fsx.run.UserService中所有的公有方法
2)execution(public * com.fsx.run.UserService.*(..))
//表示匹配com.fsx.run包及其子包下的所有方法
3)execution(* com.fsx.run..*.*(..))
2.拦截指定类型的方法
// AService下面所有外部调用方法,都会拦截。备注:只能是AService的方法,子类不会拦截的
@Pointcut("within(com.fsx.run.service.AService)")
3.匹配方法标有指定注解
// 可以匹配所有方法上标有此注解的方法
@Pointcut("@annotation(com.fsx.run.anno.MyAnno)")
4.切入点引用
// 这个就是一个`reference pointcut` 甚至还可以这样 @Before("point1() && point2()")
@Before("point()")
public void before() {
System.out.println("this is from HelloAspect#before...");
}
*/
@Pointcut("@annotation(com.demo.springboot_demo.Annotation.MyAnnotation)")
void cutPoint(){}
/**
* 前置通知
*/
@Before("cutPoint()")
public void before(){
log.info("===== 前置通知");
}
/**
* 环绕通知
*/
@Around("cutPoint()")
public Object around(ProceedingJoinPoint joinPoint){
log.info("===== 环绕通知开始");
// 所切对象的类
Class<?> aClass = joinPoint.getTarget().getClass();
log.info("目标对象:[{}]--[{}]",aClass,aClass.getName());
// 所切方法名
String methodName = joinPoint.getSignature().getName();
log.info("所切方法名:[{}]",methodName);
StringBuilder sb = new StringBuilder();
// 所切方法参数
Object[] args = joinPoint.getArgs();
for(Object arg:args){
sb.append(arg);
}
log.info("所切方法的传入参数列表:[{}]",sb.toString());
long start = System.currentTimeMillis();
Object result = null;
// 执行目标方法,并获取返回值
try{
result = joinPoint.proceed();
}catch(Exception e){
e.printStackTrace();
} catch (Throwable e) {
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
log.info("===== 方法执行共用时:[{}]",end -start);
log.info("===== 环绕通知结束");
return result;
}
/**
* 后置通知
*/
@After("cutPoint()")
public void after(){
log.info("===== 后置通知开始");
log.info("===== 后置通知结束");
}
/**
* 返回后通知
*/
@AfterReturning(value = "cutPoint()",returning = "r")
public void afterReturin(Object r){
log.info("===== 返回后通知开始");
log.info("返回值:[{}]",r);
log.info("===== 返回后通知结束");
}
/**
* 抛异常后通知
*/
@AfterThrowing(value = "cutPoint()",throwing = "t")
public void afterThrow(Throwable t){
log.info("===== 异常通知开始");
log.error("异常通知:[{}]",t.getMessage());
log.info("===== 异常通知结束");
throw new RuntimeException(t);
}
}
测试
@Component
public class TestAnnotation {
TestAnnotation(){}
@MyAnnotation
public int test(String str,int start,int end){
System.out.println("入参"+str+" - "+String.valueOf(start)+ " - "+String.valueOf(end));
System.out.println("start + end = " + (start - end));
return start-end;
}
}
@Test
void test1() {
String str = "test";
int start = 10;
int end = 5;
testAnnotation.test(str,start,end);
}
测试结果
正常结束
环绕通知@Around -> 前置通知@Before -> 原方法执行 -> 方法执行结束 -> @AfterReturning -> @After
异常结束
@Around -> @Before -> 原方法执行 -> @AfterThrow -> @After