一、什么是AOP
Spring AOP(面向切面编程)是Spring框架的一个重要模块,用于通过在运行时动态地织入到现有的类中,实现横切关注点的功能。横切关注点是那些会影响多个模块的功能,如日志、事务管理、安全性等。
Spring AOP提供了一种方便的方式来实现横切关注点的功能,而无需修改目标对象的源代码。他通过代理机制,在目标对象的方法执行前、执行后或发生异常时插入额外的逻辑,从而实现注入性能监控、事务管理、日志记录等功能。这样可以避免在每个需要这些功能的方法中都编写重复的代码,提高了代码的重用性和可维护性。
二、核心概念
切点(Pointcut):定义了在那些连接点(Join Point)上应用通知(Advice)。切点是一个表达式,用于匹配连接点。
通知(Advice):定义了在切点处执行的逻辑,比如在方法执行前后执行的代码。
连接点(Join Point):在应用程序执行过程中的某个特定点,必须方法执行时、异常抛出等,连接点是代码的具体执行位置,它代表了横切关注点可以插入的地方。
切面(Aspect):切面是切点和通知的组合,他定义了在哪里以及何时应用通知。
三、Spring AOP支持的通知类型
前置通知使用@Before: 通知方法会在方法调用之前执行
后置通知使用@After: 通知方法会在目标方法返回或者抛出异常后调用
返回通知使用@AfterReturning: 通知方法在目标方法返回后调用
异常通知使用 @AfterThrowing: 通知方法在目标方法抛出异常后调用
环绕通知使用 @Around: 通知包裹目标方法,在目标方法执行之前、执行之后添加自定义的行为。
四、切入点表达式
execution:用于匹配方法执行的连接点 within:用于匹配指定类型内的方法执行 this:用于匹配当前AOP代理对象类型的执行方法的切入点表达式;在spring AOP中,AOP代理对象是Spring框架动态生成的对象,他包含了原始对象的引用,并且可以执行通知逻辑。使用this来匹配的话,会通过AOP代理对象的类型进行匹配,这也就意味着可能会包括引入的接口类型(举个例子:假设有个接口UserService,并且有一个实现类UserServiceImpl,如果你在配置AOP时使用this(UserService)那么无论是代理对象还是目标对象,只要类型是UserService或其子类,都会被匹配到) target:用于匹配当前目标对象类型的执行方法的切入点表达式,目标对象是指被代理的原始对象,而不是代理对象本身。当使用target时,匹配的是目标对象的类型,不包括引入的接口类型(举个例子:如果使用target(UserService),那么只有目标对象的类型为UserService的方法会被匹配到,而不包括代理对象的类型)) 针对this、target来说,this匹配的是AOP代理对象的类型(包括引入的接口类型),而target匹配的是目标对象的类型(不包括引入的接口接口类型) args:用于匹配当前执行方法传入参数为指定类型的执行方法 @within: 用于匹配所有持有执行注解类型内的方法,当在切面配置中使用@within注解时,会匹配所有类级别被指定注解标记的方法 示例:@within(com.example.annotation.MyAnnotation)匹配所有被MyAnnotation注解标记的方法 @target: 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解。在切面配置中使用@target注解时,会匹配当前目标对象(而不是代理对象)持有指定注解的方法。 @args:用于匹配当前执行方法传入的参数持有指定注解的执行。在切面配置中使用@args注解时,会匹配当前方法参数中包含指定注解的方法 @annotation:用于匹配带有特定注解的连接点,即匹配被某个特定注解标记的方法。
类型匹配语法
* :匹配任何数量的字符
.. :匹配任何数量的字符,如在类型模式中匹配任何数量的子包;而在方法参数模式中匹配任何数量参数。
+ :匹配指定类型及其所有子类型,仅能作为后缀放在类型模式后边
java.lang.String 匹配String类型
java.*.String 匹配java包下的任何“一级子包”下的String类型;如匹配java.lang.String而不匹配 java.lang.ss.String
java..* 匹配java包及任何子包下的任何类型,如匹配java.lang.String
java.lang.*ing 匹配任何java.lang包下的以ing结尾的类型
java.lang.Number+ 匹配java.lang包下类型为Number及Number的子类型数据
1.execution() 用于描述方法【掌握】
语法:execution(修饰符 返回值 包.类.方法名(参数) throws异常)
修饰符,一般省略
public 公共方法
* 任意
返回值,不能省略
void 返回没有值
String 返回值字符串
* 任意
包,[可以省略]
com.itheima.crm 固定的包
com.itheima.crm.*.service crm包下面的任意子包,固定目录service(例如:com.itheima.crm.staff.service)
com.itheima.crm.. crm包下面的所有子包(含自己)
com.itheima.crm.*.service.. crm包下面的任意子包,固定目录service,service目录任意包(含自己)
类,[可以省略]
UserServiceImpl 指定的类
*Impl 以Impl结尾的类
User* 以User开头的类
* 任意的类
方法名,不能省略
addUser 固定的方法名
add* 以add开头的方法名
*Do 以Do结尾的方法名
* 任意的方法名
(参数)
() 无参
(int) 一个整型
(int, int) 两个整型
(..) 参数任意
throws,[可以省略],一般省略。
综合案例1:
execution(* com.itheima.crm.*.service..*.*(..))
综合案例2:
<aop:pointcut expression="execution(* com.itheima.*WithCommit.*(..)) ||
execution(* com.itheima.*Service.*(..))" id="myPointCut"/>
2.within:匹配包或子包中的方法(了解)
within(com.itheima.aop..*)
3.this:匹配实现了接口的代理对象中的方法(了解)
this(com.itheima.aop.user.UserDAO)
4.target:匹配实现了接口的目标对象中的方法(了解)
target(com.itheima.aop.user.UserDAO)
5.args:匹配参数格式符合标准的方法(了解)
args(int, int)
6.bean(id):对指定的bean所有的方法(了解)
bean('userServiceId')
五、自定义注解
描述:
AOP+分布式锁:请求到来时,根据redisKey先从缓存中取数据,如果命中缓存,则将结果返回,如果缓存中没有该条数据,则执行业务逻辑代码查询该条数据,如果未查到该条数据,则根据redisKey缓存redis中一个空值,时间为6分钟,如果该条数据不为空,则缓存该条数据24小时
package com.project.common.annotation.cache;
import java.lang.annotation.*;
/**
* 从缓存中获取数据
* @Author:yjj
* @Package:com.project.common.annotation.cache
* @Date:2024/3/3 10:39
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheData {
/**
* 缓存数据前缀
*/
String prefix() default "cache:";
/**
* 缓存数据后缀
*/
String suffix() default ":info";
}
package com.project.common.annotation.cache;
import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.project.model.base.CommonMessage;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* AOP + 分布式锁
* @Author:yjj
* @Package:com.project.common.annotation.cache
* @Date:2024/3/3 10:43
*/
@Component
@Aspect
@Slf4j
public class CacheDataAspect {
@Autowired
private RedisTemplate redisTemplate;
@Resource
private RedissonClient redissonClient;
int count = 0;
@SneakyThrows
@Around("@annotation(com.project.common.annotation.cache.CacheData)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
try {
Object resultObject = null;
log.info("前置通知");
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
// 1、获取缓存注解---从方法签名中获取到注解的相关信息
CacheData cacheData = methodSignature.getMethod().getAnnotation(CacheData.class);
// 1.1 获取方法中的参数,并将方法参数转为String类型,采用 | 分割
Object[] args = joinPoint.getArgs();
String argStr = Arrays.asList(args).stream().map(item -> {
return item.toString();
}).collect(Collectors.joining("|"));
// 1.2 拼接redis的key
String dataKey = cacheData.prefix() + argStr + cacheData.suffix();
// 2、 从redis中查询数据--如果数据为空则需要去数据库中查询,如果不为空则直接返回结果
resultObject = getRedisData(dataKey,methodSignature);
// 如果缓存数据为空,则需要查询数据库,并将查询结果缓存到redis中
if (resultObject != null){
// 2.1 缓存数据不为空,则直接返回
return resultObject;
}
// 2.2 代码执行到此时说明缓存为空,则需要从数据库中查询数据
// 2.2.1 设置lockKey
String lockKey = cacheData.prefix() + argStr + CommonMessage.CACHE_DATA_LOCK_SUFFIX;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean flag = lock.tryLock(CommonMessage.CACHE_DATA_EXPIRE_PX1, CommonMessage.CACHE_DATA_EXPIRE_PX2, TimeUnit.SECONDS);
// 如果获取锁失败,则进行自旋
if (!flag){
count++;
if (count<=3){
Thread.sleep(100);
return this.cacheAroundAdvice(joinPoint);
}else {
return new RuntimeException("重试3次后仍未获取到锁!!!");
}
}
// 如果获取锁成功,则执行目标方法
resultObject = joinPoint.proceed(args);
if (resultObject == null){
redisTemplate.opsForValue().set(dataKey,JSONObject.toJSONString(resultObject),CommonMessage.CACHE_DATA_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return resultObject;
}else{
redisTemplate.opsForValue().set(dataKey,JSONObject.toJSONString(resultObject),CommonMessage.CACHE_DATA_TIMEOUT,TimeUnit.SECONDS);
return resultObject;
}
} finally {
// 释放锁资源
lock.unlock();
}
} catch (Throwable e) {
e.printStackTrace();
}
log.info("后置通知");
return joinPoint.proceed(joinPoint.getArgs());
}
/**
* 从缓存中获取数据
* @param dataKey
* @param methodSignature
* @return
*/
public Object getRedisData(String dataKey, MethodSignature methodSignature) {
String resultObject = (String) redisTemplate.opsForValue().get(dataKey);
if (StringUtils.isNotBlank(resultObject)){
return JSONObject.parseObject(resultObject,methodSignature.getReturnType());
}
return null;
}
}