目录
1、通知(Advice)
通知(Advice):在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”,“before” 和“after”等等。通知的类型将在后面详细了解。许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
用来放增强的代码的那个方法
- 环绕通知@Around:可以把代码增强在目标方法的任意地方,更通用
- 前置通知@Before:目标方法之前执行
- 后置通知@After:目标方法之后执行
- 异常通知@AfterThrowing:目标方法出现了异常执行
- 返回通知@AfterReturning:目标方法返回值执行
这是一个自定义类UserService,假设其所有业务功能都已经实现,在后面的示例中可能会修改一些代码
//自定义类UserService
@Service
public class UserService {
// 增加增删改查方法,这里就用一个打印语句来代替具体的方法功能了
public void add(){
System.out.println("增加");
}
public void delete(){
System.out.println("删除");
}
public void update(){
System.out.println("修改");
}
public void query(){
System.out.println("查询");
}
}
这是一个切面类,用于辅助理解各种通知类型
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//后续在这会写各种通知的代码
}
这是一个测试类,在里面获取UserService的bean,然后调用他里面的方法进行测试
@SpringBootTest(classes = AdviceTest.class)
@ComponentScan
@EnableAspectJAutoProxy
public class AdviceTest {
@Test
void contextLoads(@Autowired UserService userService) {
userService.query(); //这里调用的方法不是固定的
}
}
1.1、前置通知
前置通知@Before:目标方法之前执行,即在连接点之前进行执行
示例:
//自定义类UserService,假设其所有业务功能都已经实现
@Service
public class UserService {
//......这里的代码不变
public void delete(long id){
System.out.println("删除");
}
//......这里的代码不变
}
//切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//前置通知
@Before("execution(* com.lt.advice.UserService.*(..))")
public void before(JoinPoint joinPoint){
//记录当前方法的方法名,参数
String methodName = joinPoint.getSignature().getName();
//参数
Object[] args = joinPoint.getArgs();
//目标对象
Object target = joinPoint.getTarget();
System.out.println("当前执行的方法是" + target.getClass() + "." + methodName + ": 参数" + Arrays.toString(args));
}
}
//测试类AdviceTest
@SpringBootTest(classes = AdviceTest.class)
@ComponentScan
@EnableAspectJAutoProxy
public class AdviceTest {
@Test
void contextLoads(@Autowired UserService userService) {
userService.delete(1L);//调用delete方法进行测试
}
}
运行结果:
1.2、后置通知
后置通知@After:目标方法之后执行,就是在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。
示例:
//切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//前置通知
@Before("execution(* com.lt.advice.UserService.*(..))")
public void before(JoinPoint joinPoint){
//......这里的代码不变
}
//后置通知
@After("execution(* com.lt.advice.UserService.*(..))")
public void after(JoinPoint joinPoint){
// 在这里也可以获得目标方法的一些信息,这里就不再演示了,简单的执行一条打印语句就好了
System.out.println("后置通知");
}
}
运行结果:
1.3、返回通知
返回通知@AfterReturning:目标方法返回值执行。当目标方法(连接点方法)成功执行后,返回通知方法才会执行,如果目标方法出现异常,则返回通知方法不执行。返回通知方法在目标方法执行成功后才会执行,所以,返回通知方法可以拿到目标方法执行后的结果。
切面类中定义返回通知方法,示例如下:
//自定义类UserService,假设其所有业务功能都已经实现
@Service
public class UserService {
// 增加增删改查方法,这里就用一个打印语句来代替具体的方法功能了
//......这里的代码不变
public Object query(){
System.out.println("查询");
return null;
}
}
切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//前置通知和后置通知
//......这里的代码不变
// 返回通知:可以获取返回值,在后置通知之前执行
@AfterReturning(value = "execution(* com.lt.advice.UserService.*(..))", returning = "returnValue")
public void afterReturning(JoinPoint joinPoint, Object returnValue){
System.out.println("返回通知:" + returnValue);//输出返回的结果
}
}
@SpringBootTest(classes = AdviceTest.class)
@ComponentScan
@EnableAspectJAutoProxy
public class AdviceTest {
@Test
void contextLoads(@Autowired UserService userService) {
userService.query();//调用query方法进行测试
}
}
1.4、异常通知
异常通知@AfterThrowing:目标方法出现了异常执行。异常通知方法只在连接点方法出现异常后才会执行,否则不执行。在异常通知方法中可以获取连接点方法出现的异常。
在切面类中异常通知方法,示例如下:
//自定义类UserService,假设其所有业务功能都已经实现
@Service
public class UserService {
// 增加增删改查方法,这里就用一个打印语句来代替具体的方法功能了
//......这里的代码不变
public void update(){
System.out.println("修改");
throw new RuntimeException("出错了~~"); //自定义一个异常,用于测试异常通知
}
//......这里的代码不变
}
//切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//前置通知、后置通知和返回通知
//......这里的代码不变
//异常通知
@AfterThrowing(value = "execution(* com.lt.advice.UserService.*(..))", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Exception exception){
System.out.println("异常通知:" + exception.getMessage()); //输出异常信息
}
}
//测试类AdviceTest
@SpringBootTest(classes = AdviceTest.class)
@ComponentScan
@EnableAspectJAutoProxy
public class AdviceTest {
@Test
void contextLoads(@Autowired UserService userService) {
userService.update();//调用update方法进行测试
}
}
1.5、通知的执行顺序
正常情况:前置--->目标方法--->返回通知--->后置通知(finally)
异常情况:前置--->目标方法--->异常通知--->后置通知(finally)
2、切点(Pointcut)
切点(Pointcut):匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用Aspectj切点语义。
增强代码要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法,即要切哪些东西。切点表达式
2.1、切点表达式的抽取
通过抽取切点表达式,可以将关注点分离,提高代码的模块化和可维护性。同时,抽取后的切点表达式具有更高的重用性,可以在不同的切面中共享使用。并且,代码的可读性和可维护性也会得到提升,开发者能更清晰地理解和管理切点,方便后续的修改和调整。最重要的是,抽取切点表达式有助于降低代码之间的耦合度,使系统更加灵活和可扩展。
抽取切点表达式的实现:先声明一个方法,这个方法里面不需要写任何代码,然后在这个方法上利用@Pointcut注解去声明一个切点表达式,之后就可以在各种通知当中去引用这个加了@Pointcut注解的切点方法名了,这就相当于把@Pointcut注解声明的切点表达式给引用过来了
示例:
//切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//声明一个方法,来抽取切点表达式
@Pointcut("execution(* com.lt.pointcut.UserService.*(..))")
public void pointcut(){}
//前置通知
@Before("pointcut()") //引用抽取了的切点表达式
public void before(JoinPoint joinPoint){
//记录当前方法的方法名,参数
String methodName = joinPoint.getSignature().getName();
//获取参数
Object[] args = joinPoint.getArgs();
//获取目标对象
Object target = joinPoint.getTarget();
System.out.println("当前执行的方法是" + target.getClass() + "." + methodName + ": 参数" + Arrays.toString(args));
}
//后置通知
@After("pointcut()") //引用抽取了的切点表达式
public void after(JoinPoint joinPoint){
System.out.println("后置通知");
}
// 返回通知:可以获取返回值,在后置通知之前执行
@AfterReturning(value = "pointcut()", returning = "returnValue")
public void afterReturning(JoinPoint joinPoint, Object returnValue){
System.out.println("返回通知:" + returnValue);//returnValue是返回值
}
//异常通知
@AfterThrowing(value = "pointcut()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Exception exception){
System.out.println("异常通知:" + exception.getMessage()); //输出异常信息
}
}
2.2、切点标识符
2.2.1、execution
execution是切点标识符,用于匹配方法执行连接点。,execution是使用Spring AOP时使用的主要切点标识符,可以匹配到方法级别(就是可以去匹配规定的要具体到哪一个方法),细粒度
(1)访问修饰符:不写代表所有
(2)返回值:*代表所有
(3)完整限定名:
包名:*代表任何包名,而 ..代表子孙包
com.lt.service = service包
com.lt.* = com.lt.service、 com.lt.dao、 com.lt.xxx 等。包名任意不代表层级任意
com.lt.. = com.lt.service.imple、 com.lt.service.depend 等。代表层级任意
类:*代表所有类
com.lt..* = com.lt.service.imple.任意包. 任意类
参数:不写代表没有参数的方法 而 ..代表任意参数
2.2.2、within
within只能匹配类这一级别,只能指定类, 类下面的某个具体的方法无法指定, 粗粒度
示例:如果想让execution和within的切点表达式都生效,可以在引用时使用 && 实现
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//execution 方法级别
@Pointcut("execution(* com.lt.pointcut.UserService.*(..))")
public void pointcut(){}
//within 类级别=UserService所有的方法
@Pointcut("within()* com.lt.pointcut.UserService)")
public void pointcutWithIn(){}
//前置通知
@Before("pointcut() && pointcutWithIn()")//使用 && 让execution和within的切点表达式都生效
public void before(JoinPoint joinPoint){
//记录当前方法的方法名,参数
String methodName = joinPoint.getSignature().getName();
//参数
Object[] args = joinPoint.getArgs();
//目标对象
Object target = joinPoint.getTarget();
System.out.println("当前执行的方法是" + target.getClass() + "." + methodName + ": 参数" + Arrays.toString(args));
}
}
2.2.3、@annotation
@annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)
假设我需要记录每个方法详细作用,并且记录数据库日志表。这时候就可以用@annotation注解来作为切点标识符,因为@annotation匹配到的是方法上的注解
示例:
先自定义一个注解Log
@Retention(RetentionPolicy.RUNTIME) //注解的保留
@Target({ElementType.METHOD}) //可以标记的地方
public @interface Log {
String value();//用于记录方法的作用
}
这是一个自定义类UserService
@Service
public class UserService {
//加上自定义的注解,并写上add()方法的作用
@Log("用户增加方法")
public void add(){
System.out.println("增加");
}
}
这是一个切面类LogAspect
@Aspect //把这个类标记为切面类
@Component //切面类必须声明为Spring的bean
public class LogAspect {
//execution 方法级别
@Pointcut("execution(* com.lt.pointcut.UserService.*(..))")
public void pointcut(){}
//within 类级别=UserService所有的方法
@Pointcut("within(com.lt.pointcut.UserService)")
public void pointcutWithIn(){}
//Pointcut 所有的方法有log注解都会匹配
//如果要在通知的参数中绑定注解就不能单独抽取
//如果要在通知的参数中绑定注解,声明就是参数名了,不是注解类型!
@Pointcut("@annotation(Log)")
public void pointcutAnnotation(){}
//前置通知
//@Before("pointcut() && pointcutWithIn() && @annotation(log)")
@Before("@annotation(log)")//@Before中的参数需要填@annotation(log),而不是pointcutAnnotation()
public void before(JoinPoint joinPoint, Log log){
//记录当前方法的方法名,参数
String methodName = joinPoint.getSignature().getName();
//参数
Object[] args = joinPoint.getArgs();
//目标对象
Object target = joinPoint.getTarget();
System.out.println("当前执行的方法是" + target.getClass() + "." + methodName + ": 参数" + Arrays.toString(args));
//获取注解信息
System.out.println("当前方法的作用是:" + log.value());
}
}
测试类AdviceTest
@SpringBootTest(classes = AdviceTest.class)
@ComponentScan
@EnableAspectJAutoProxy
public class AdviceTest {
@Test
void contextLoads(@Autowired UserService userService) {
userService.add();
}
}
运行结果:
推荐:
【Spring】初识 Spring AOP(面向切面编程)-CSDN博客https://blog.csdn.net/m0_65277261/article/details/138724937?spm=1001.2014.3001.5501【Spring】Bean的生命周期回调函数和Bean的循环依赖-CSDN博客
https://blog.csdn.net/m0_65277261/article/details/138503989?spm=1001.2014.3001.5501【Spring】IOC/DI中常用的注解@Lazy、@Scope与@Conditional-CSDN博客
https://blog.csdn.net/m0_65277261/article/details/138277932?spm=1001.2014.3001.5501