一、SpringAOP理解
SpringAOP称之为“面向切面编程”,这和我们之前编程思想“面向对象编程”是不一样的,当然我们还听过“面向过程的编程”,这些都是一种编程思想,我们先来看一下他们之间的区别:
面向过程编程(opp):面向过程更接近于计算机底层的执行逻辑,关注的是“怎么去做”的过程,他会将一个任务拆分成各个小的步骤,主要通过那个函数进入,逐步的调用哪些函数去执行,着重于细节的过程性内容。
面向对象编程(oop):面向对象关注的是由谁来做,比较接近我们人类的认识,关注的是做任务的对象,他有什么样的特点,能干什么样的事情,通过对象之间的相关配合工作去完成一个个的任务。
面向切面编程(aop):aop是在oop的基础上,延伸而出的,他关注的是在已有的任务执行过程中,如何能动态的插入新的逻辑代码,上下游的相互配合或者单独执行功能,来玩新业务的需要,使得对原有逻辑的伤害到最小。
值得强调的是,很多项目的编程思想,并不是只有一种,他们之间相互配合达到编程的最优效果。
二、SpringAOP的基本概念
- Aspect(切面):通常是一个类,里面定义切入点和通知;
- JoinPoint(连接点):表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用;
- Advice(通知):通知描述了切面何时执行以及如何执行增强处理;
- Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式;
三、SpringAOP通知的类型
- 前置通知(Before):在目标方法被调用之前的通知功能;
- 后置通知(After):在目标方法执行完成后执行,如果目标方法异常,则后置通知不再执行;
- 异常通知(After-throwing):目标方法抛出异常的时候执行;
- 最终通知(finally);不管目标方法是否有异常都会执行,相当于try。。catch。。finally中的finally;
- 环绕通知(round):可以控制目标方法是否执行;
四、JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。
/*获取参数的值数组*/
Object[] args = point.getArgs();
/*获取目标对象(被加强的对象)*/
Object target = point.getTarget();
/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
MethodSignature signature = (MethodSignature) point.getSignature();
/*方法名*/
String signatureName = signature.getName();
/*参数名称数组(与args参数值意义对应)*/
String[] parameterNames = signature.getParameterNames();
/*获取执行的方法对应Method对象*/
Method method = signature.getMethod();
/*获取返回值类型*/
Class returnType = signature.getReturnType();
/*获取方法上的注解*/
WebAnnotation webAnnotation = method.getDeclaredAnnotation(WebAnnotation.class);
五、ProceedingJoinPoint 对象
Proceedingjoinpoint 继承了 JoinPoint ,是在JoinPoint的基础上暴露出 proceed 这个方法, 暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。
六、代码示例(基于注解的aop使用)
我们创建了一个maven项目,并通过注解的方式配置 spring aop。
1、pom中引入aop的依赖
<!--spring AOP的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
2、定义目标类(切面类)
@Slf4j // 日志框架 slf4j + log4j2
@Component // 声明这是一个组件
@Aspect // 声明这是一个切面 bean
public class LogAspect {
// 日志框架 slf4j + log4j2
public static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Pointcut("execution(* springaop.service..*.*(..))") // 切入点注解,定义程序中需要注入Advice的位置的集合,有十中类型的表达方式:
/*
* execution:一般用于指定方法的执行,用的最多。
* within:指定某些类型的全部方法执行,也可用来指定一个包。
* this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
* target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
* args:当执行的方法的参数是指定类型时生效。
* @target:当代理的目标对象上拥有指定的注解时生效。
* @args:当执行的方法参数类型上拥有指定的注解时生效。
* @within:与@target类似,@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。
* @annotation:当执行的方法上拥有指定的注解时生效。
* bean:当调用的方法是指定的bean的方法时生效。
*/
public void pointcut() {
}
}
3、前置通知 @Before
前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
/**
* 前置通知
**/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
//获取参数
/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
/*方法名*/
String signatureName = signature.getName();
/*参数名称数组(与args参数值意义对应)*/
String[] parameterNames = signature.getParameterNames();
/*获取参数的值数组*/
Object[] args = joinPoint.getArgs();
for (Object ss : args) {
System.out.print("【参数】"+ss);
}
log.info("befor通知>>>【方法名】"+signatureName);
}
4、后置通知 @After
在目标方法执行之后执行的通知。
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。
/**
* 后置通知
*/
@After("pointcut()")
public void after() {
log.info("after通知>>>>>>>>>>>>>>>>>>>>>>>>");
}
5、后置通知——增强处理 @AfterReturning
AfterReturning增强处理将在目标方法正常完成后被织入,
(1) pointcut/value:这两个属性的作用是一样的,它们都属于指定切入点对应的切入表达式。一样既可以是已有的切入点,也可直接定义切入点表达式。当指定了pointcut属性值后,value属性值将会被覆盖。
(2) returning:该属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。
/**
* 后置通知,增强处理
* */
@AfterReturning(pointcut = "pointcut()", returning = "res")
public void afterReturn(Object res) {
log.info("afterReturning通知>>>>>>>>>>>>>>>>>>>>"+res);
}
6、异常通知
在目标方法抛出异常时执行的通知
可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位
另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。
/**
* 异常通知
* */
@AfterThrowing(pointcut = "pointcut()",throwing = "err")
public void afterThrow(Throwable err){
log.info("afterThrowing>>>>>>>>>>>>>【异常:】" +err);
}
7.环绕通知 @Around
在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。
要注意:只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。
/**
* 环绕通知
*/
@Around("pointcut()")
public void aroud(ProceedingJoinPoint proceedingJoinPoint) {
Object res = null;
log.info("aroud通知:begin>>>>>>>>>>>>>");
// 业务参数
Object[] args = proceedingJoinPoint.getArgs();
try {
// 手动执行真正的业务方法,传入业务参数
res = proceedingJoinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】" + res);
}
七、通知执行的先后顺序
LogAspect .class
package com.study.springaop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* @project: javaDemoStudy
* @author: 13965
* @date:2021/3/1 9:48
* @description:
*/
@Slf4j // 日志框架 slf4j + log4j2
@Component // 声明这是一个组件
@Aspect // 声明这是一个切面 bean
public class LogAspect {
@Pointcut("execution(* com.study.springaop.service..*.*(..))") // 切入点注解,定义程序中需要注入Advice的位置的集合,有十中类型的表达方式:
/*
* execution:一般用于指定方法的执行,用的最多。
* within:指定某些类型的全部方法执行,也可用来指定一个包。
* this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
* target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
* args:当执行的方法的参数是指定类型时生效。
* @target:当代理的目标对象上拥有指定的注解时生效。
* @args:当执行的方法参数类型上拥有指定的注解时生效。
* @within:与@target类似,@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。
* @annotation:当执行的方法上拥有指定的注解时生效。
* bean:当调用的方法是指定的bean的方法时生效。
*/
public void pointcut() {
}
/**
* 前置通知
**/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
//获取参数
/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
/*方法名*/
String signatureName = signature.getName();
/*参数名称数组(与args参数值意义对应)*/
String[] parameterNames = signature.getParameterNames();
/*获取参数的值数组*/
Object[] args = joinPoint.getArgs();
for (Object ss : args) {
log.info("befor通知>>>>>>>>>>>>>>>>【方法名】" + signatureName+"【参数】" + ss);
}
}
/**
* 后置通知
*/
@After("pointcut()")
public void after() {
log.info("after通知>>>>>>>>>>>>>>>>>>>>>>>>");
}
/**
* 后置通知,增强处理
*/
@AfterReturning(pointcut = "pointcut()", returning = "res")
public void afterReturn(Object res) {
log.info("afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】" + res);
}
/**
* 异常通知
*/
@AfterThrowing(pointcut = "pointcut()", throwing = "err")
public void afterThrow(Throwable err) {
log.info("afterThrowing通知>>>>>>>>>>>>>【异常:】" + err);
}
/**
* 环绕通知
*/
@Around("pointcut()")
public void aroud(ProceedingJoinPoint proceedingJoinPoint) {
Object res = null;
log.info("aroud通知:begin>>>>>>>>>>>>>");
// 业务参数
Object[] args = proceedingJoinPoint.getArgs();
try {
// 手动执行真正的业务方法,传入业务参数
res = proceedingJoinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】" + res);
}
}
AopDemoService.calss service层业务逻辑
package com.study.springaop.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @project: javaDemoStudy
* @author: 13965
* @date:2021/3/1 10:29
* @description:
*/
@Slf4j
@Service
public class AopDemoService {
public String insert(String name) {
log.info("方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】"+name);
// int num = 10 / 0;
return "这是【insert】的返回内容呢";
}
}
AopDemoServiceTest.class 单元测试
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AopDemoServiceTest {
@Autowired
private AopDemoService aopDemoService;
@Test
public void insert() {
aopDemoService.insert("demo");
}
}
通过执行单元测试,我们可以看到,日志中的输出顺序;
2021-03-04 16:11:43.616 INFO 17964 --- [ main] c.s.s.service.AopDemoServiceTest : Starting AopDemoServiceTest on LAPTOP-QEAM9EMC with PID 17964 (started by 13965 in C:\gitproject\studyProjectWorkSpace\java_syudy_moduls\springaop)
2021-03-04 16:11:43.619 INFO 17964 --- [ main] c.s.s.service.AopDemoServiceTest : No active profile set, falling back to default profiles: default
2021-03-04 16:11:48.421 INFO 17964 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-03-04 16:11:49.583 INFO 17964 --- [ main] c.s.s.service.AopDemoServiceTest : Started AopDemoServiceTest in 6.619 seconds (JVM running for 8.024)
2021-03-04 16:11:50.063 INFO 17964 --- [ main] com.study.springaop.LogAspect : aroud通知:begin>>>>>>>>>>>>>
2021-03-04 16:11:50.068 INFO 17964 --- [ main] com.study.springaop.LogAspect : befor通知>>>>>>>>>>>>>>>>【方法名】insert【参数】demo
2021-03-04 16:11:50.084 INFO 17964 --- [ main] c.s.springaop.service.AopDemoService : 方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】demo
2021-03-04 16:11:50.084 INFO 17964 --- [ main] com.study.springaop.LogAspect : aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】这是【insert】的返回内容呢
2021-03-04 16:11:50.084 INFO 17964 --- [ main] com.study.springaop.LogAspect : after通知>>>>>>>>>>>>>>>>>>>>>>>>
2021-03-04 16:11:50.084 INFO 17964 --- [ main] com.study.springaop.LogAspect : afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】null
2021-03-04 16:11:50.109 INFO 17964 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
结论:在单个切面类,正常情况下,五大通知的执行顺序为:
环绕前置>>>>>@Before>>>>>目标方法执行>>>>环绕返回>>>>>环绕最终>>>>>@After>>>>>@AfterReturning;
异常的情况下:环绕前置>>>>@Before>>>>目标方法执行>>>>环绕异常>>>>环绕最终>>>>@After>>>>@AfterThrowing
八、通过注解定义切点
通过注解的方式,可以灵活的控制切入的位置,和切入的颗粒大小。
(1) 首先定义一个新的注解(这时项目中有两个切面类分别为:LogAspect 通过 execution 指定方法作为切入点;SecondAspect 通过@annotation 指定注解作为切入点):
MyselfAnnotationDemo.class
package com.study.springaop.annotation;
import com.study.springaop.enums.KeyCodesEnums;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyselfAnnotationDemo {
// key值
KeyCodesEnums key();
// 名称
String name();
// 数据值
String value() default "default";
}
(2) 修改切面类
SecondAspect.class
package com.study.springaop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @project: java_syudy_moduls
* @author: 13965
* @date:2021/3/9 10:04
* @description:
*/
@Slf4j // 日志框架 slf4j + log4j2
@Component // 声明这是一个组件
@Aspect // 声明这是一个切面 bean
public class SecondAspect {
@Pointcut("@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")
public void pointcut() {
}
}
说明:这里我们使用@annotation 注解来指定 切入点的注入方式是通过 注解声明
(3) 业务service 定义一个方法,添加自定义的注解 MyselfAnnotationDemo
package com.study.springaop.service;
import com.study.springaop.annotation.MyselfAnnotationDemo;
import com.study.springaop.enums.KeyCodesEnums;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @project: javaDemoStudy
* @author: 13965
* @date:2021/3/1 10:29
* @description:
*/
@Slf4j
@Service
public class AopDemoService {
public String insert(String name) {
log.info("方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】"+name);
// int num = 10 / 0;
return "这是【insert】的返回内容呢";
}
// 添加自己定义的注解
@MyselfAnnotationDemo(key = KeyCodesEnums.FRIDAY,name = "方法annotationMethod")
public String annotationMethod(String params) {
log.info("该方法添加注解++++++params" + params);
return "添加注解的 annotationMethod 方法";
}
}
说明:这里要注意,我们两个切面类目前都包含了 annotationMethod() 方法的切入点。
(4) 单元测试
@Test
public void annotationMethodTest() {
log.info("TEST_BEGIN======annotationMethodTest");
aopDemoService.annotationMethod("GOOD");
log.info("TEST_END======annotationMethodTest");
}
输出结果:
TEST_BEGIN======annotationMethodTest
aroud通知:begin>>>>>>>>>>>>>
befor通知>>>>>>>>>>>>>>>>【方法名】annotationMethod【参数】GOOD
该方法添加注解++++++paramsGOOD
SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】添加注解的 annotationMethod 方法
aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】添加注解的 annotationMethod 方法
after通知>>>>>>>>>>>>>>>>>>>>>>>>
afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】null
TEST_END======annotationMethodTest
说明:首先我们应该看到,通过注解的方式SecondAspect 切面的后置通知正常运行,除此之外,还可以看到,对于一个方法,通过execution指定方法,和通过annotation注解的形式,他们是可以同时运行的。我们也可以在 @Pointcut()表达式中指定切入点的时候 排除一些切入位置:
@Pointcut("execution(* com.study.springaop.service..*.*(..)) && !@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")
这样我们的annotationMethod() 方法,就只会被 SecondAspect 切面进行切入,而在execution表达式时被排除,可以看到单元测试的运行结果:
TEST_BEGIN======annotationMethodTest
该方法添加注解++++++paramsGOOD
注解对象的解析:key=FRIDAY;name=方法annotationMethod;valuedefault
SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】添加注解的 annotationMethod 方法
TEST_END======annotationMethodTest
九、在切面类中解析注解切入点
package com.study.springaop;
import com.study.springaop.annotation.MyselfAnnotationDemo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @project: java_syudy_moduls
* @author: 13965
* @date:2021/3/9 10:04
* @description:
*/
@Slf4j // 日志框架 slf4j + log4j2
@Component // 声明这是一个组件
@Aspect // 声明这是一个切面 bean
public class SecondAspect {
@Pointcut("@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")
public void pointcut() {
}
/**
* 后置通知,增强处理
*/
@AfterReturning(pointcut = "pointcut()", returning = "res")
public void afterReturn(JoinPoint joinPoint,Object res) {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取执行的方法对应Method对象
Method method = signature.getMethod();
// 获取方法上的注解
MyselfAnnotationDemo myselfAnnotationDemo = method.getDeclaredAnnotation(MyselfAnnotationDemo.class);
log.info("注解对象的解析:key=" + myselfAnnotationDemo.key() + ";name=" + myselfAnnotationDemo.name() + ";value" + myselfAnnotationDemo.value());
log.info("SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】" + res);
}
}