日志切面
@MyLog 注解 属性 desc 使用了SpEl表达式,主要是用来获取形参值,编写动态日志
- 定义枚举类
@Getter
public enum LogCodeEnum {
INSERT(2,"添加"),
UPDATE(3,"修改"),
DELETE(4,"删除"),
OTHER(5,"其他")
;
private Integer code;
private String message;
LogType(Integer code, String message) {
this.code = code;
this.message = message;
}
}
- 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
LogCodeEnum type() default LogCodeEnum.OTHER;
/**
* 日志的描述信息,支持SpEL表达式
*
* @return
*/
String desc() default "";
}
- 编写切面类
@Component
@Aspect
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.shi.annotation.MyLog)")
public void pointCut() {
}
@Around("pointCut()")
public Object handle(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String requestURI = request.getRequestURI();
Object[] args = joinPoint.getArgs();
MyLog annotation = method.getAnnotation(MyLog.class);
String logDesc = parseSpEL(joinPoint);
long begin = System.currentTimeMillis();
Object res = null;
try {
res = joinPoint.proceed(args);
} catch (Throwable e) {
throw new RuntimeException();
}
long end = System.currentTimeMillis();
// 可以定义entity 持久化到数据库
log.info("日志类型:{}\t日志描述:{}\t请求URI:{}\t方法签名:{}\t请求参数列表:{}\t请求用时(ms):{}", annotation.type().getCode(), logDesc, requestURI, methodSignature, args, end - begin);
return res;
}
/**
* 解析 @MyLog 注解中的 desc属性
*/
private String parseSpEL(ProceedingJoinPoint joinPoint) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
MyLog annotation = method.getAnnotation(MyLog.class);
if (annotation.desc() == null || "".equals(annotation.desc().trim())) {
return "";
}
Object[] args = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
// 向SpEL上下文注入参数信息
for (int i = 0; i < parameters.length; i++) {
context.setVariable(parameters[i].getName(), args[i]);
}
// 使用解析器,在上下文中解析目标表达式
Object value = parser.parseExpression(annotation.desc()).getValue(context);
if (value == null) {
return "";
}
return value.toString();
}
}
- 实战测试
/**
* 通过主键查询单条数据
*
* @return 单条数据
*/
@MyLog(type = LogCodeEnum.SELECT)
@GetMapping("/all")
public Result queryAll() {
return Result.ok(this.emp1Service.queryAll());
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@MyLog(type = LogCodeEnum.DELETE, desc = "'通过【id='+#id+'】删除'")
@DeleteMapping("/{id}")
public Result removeById(@PathVariable("id") Integer id) {
return Result.ok(this.emp1Service.removeById(id));
}
测试结果
日志类型:删除 日志描述:通过【id=5】删除 请求URI:/emp1/5 方法签名:Result com.shi.controller.Emp1Controller.removeById(Integer) 请求参数列表:[5] 请求用时(ms):234
缓存切面
我的这个缓存切面有哪些优势?
实现了解耦合 可以通过配置文件修改属性
缓存空值:解决了缓存穿透
通过互斥锁:解决了缓存击穿
设置随机的过期时间:解决了缓存雪崩
通过使用SpEl表达式,灵活的实现key的删除,redis的模糊表达式。
@MyCacheEvict注解 likeKeys支持数组形式。
为什么使用数组形式呢?
比如:
我们有一个emp表,缓存中目前存的key如下:
emp:all
emp:Id:1
emp:Id:2
emp:Id:3
现在我们想要删除emp:Id:1
那么哪些缓存会受到影响呢?emp:all 和emp:Id:1会受到影响,所以需要删除这些key。
怎么实现呢?下面注解就可以实现
@MyCacheEvict(likeKeys = {“‘emp:all’”, “‘emp:Id:’+#Id”})
话不多说,开始上手
- 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheable {
/**
* 缓存 key
* 支持SpEl表达式
*
* @return
*/
String key();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheEvict {
/**
* 模糊删除
* 支持SpEl表达式
*
* @return
*/
String[] likeKeys();
}
- 编写切面
@Aspect
@Component
public class CacheableAspect {
private static final Random RANDOM = new Random();
/**
* 默认10秒
*/
@Value("${cache.lock-ttl:10000}")
private Integer lock_ttl;
/**
* 默认30分钟
*/
@Value("${cache.object-ttl:1800000}")
private Integer cache_ttl;
/**
* 空值缓存过期时间10秒
*/
@Value("${cache.null-ttl:10000}")
private Integer null_ttl;
@Value("${cache.lock-key-prefix:lock}")
private String lockKey_prefix;
/**
* 全限定类名
* 确保这个类中有 data属性,因为在重建缓存函数中用到了
*/
@Value("${cache.result.type:com.shi.common.Result}")
private String cacheResultType;
/**
* 统一返回结果集中,data的属性名
* 这里默认使用的data,你可以根据需要修改
*/
@Value("${cache.result.data.name:data}")
private String dataName;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.shi.annotation.MyCacheable)")
public void pointCut() {
}
@Around("pointCut()")
public Object handle(ProceedingJoinPoint joinPoint) {
String key = parseSpEl(joinPoint);
String cacheStr = stringRedisTemplate.opsForValue().get(key);
// 检查缓存中是否存在
if (cacheStr != null) {
try {
Class<?> clazz = Class.forName(cacheResultType);
return JSONUtil.toBean(cacheStr, clazz);
} catch (ClassNotFoundException e) {
throw new RuntimeException();
}
}
String lockKey = lockKey_prefix + ":" + key;
// 重建缓存
return buildCache(lockKey, key, joinPoint);
}
/**
* 通过互斥锁重建缓存
*
* @param lockKey
* @param key
* @param joinPoint
* @return
*/
private Object buildCache(String lockKey, String key, ProceedingJoinPoint joinPoint) {
// 获取互斥锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMillis(lock_ttl));
Object res = null;
if (Boolean.TRUE.equals(success)) {
try {
Class<?> clazz = Class.forName(cacheResultType);
Field field = clazz.getDeclaredField(dataName);
field.setAccessible(true);
res = joinPoint.proceed();
if (ObjectUtil.isEmpty(field.get(res))) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(res), Duration.ofMillis(null_ttl));
stringRedisTemplate.delete(lockKey);
return res;
}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(res), Duration.ofMillis(getRandomTTL()));
stringRedisTemplate.delete(lockKey);
return res;
} catch (Throwable e) {
throw new RuntimeException();
}
} else {
try {
Thread.sleep(50);
handle(joinPoint);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
return null;
}
/**
* 获取缓存的随机过期时间
*
* @return
*/
private Integer getRandomTTL() {
// 1000*60*10
// 在原来缓存时间的基础上,随机加上 0-10分钟
return cache_ttl + RANDOM.nextInt(600000);
}
/**
* 解析注解 @MyCacheable中属性key的SpEl
*
* @param joinPoint
* @return
*/
private String parseSpEl(ProceedingJoinPoint joinPoint) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
MyCacheable annotation = method.getAnnotation(MyCacheable.class);
Object[] args = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
// 向SpEL上下文注入参数信息
for (int i = 0; i < parameters.length; i++) {
context.setVariable(parameters[i].getName(), args[i]);
}
// 使用解析器,在上下文中解析目标表达式
Object value = parser.parseExpression(annotation.key()).getValue(context);
if (value == null) {
return "";
}
return value.toString();
}
}
@Aspect
@Component
public class CacheEvictAspect {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.shi.annotation.MyCacheEvict)")
public void pointCut() {
}
@AfterReturning("pointCut()")
public void handle(JoinPoint joinPoint) {
Set<String> set = parseSpEl(joinPoint);
Set<String> allKeys = new HashSet<>();
for (String n : set) {
Set<String> keys = stringRedisTemplate.keys(n);
if (keys != null && !keys.isEmpty()) {
allKeys.addAll(keys);
}
}
stringRedisTemplate.delete(allKeys);
}
/**
* 解析注解 @MyCacheEvict中属性keys的SpEl
*
* @param joinPoint
* @return
*/
private Set<String> parseSpEl(JoinPoint joinPoint) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
MyCacheEvict annotation = method.getAnnotation(MyCacheEvict.class);
Object[] args = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
// 向SpEL上下文注入参数信息
for (int i = 0; i < parameters.length; i++) {
context.setVariable(parameters[i].getName(), args[i]);
}
// 使用解析器,在上下文中解析目标表达式
Set<String> set = new HashSet<>();
for (String key : annotation.likeKeys()) {
set.add(JSONUtil.toJsonStr(parser.parseExpression(key).getValue(context)));
}
return set;
}
}
- 实战测试
@MyCacheable(key = "'emp:all'")
@GetMapping("/all")
public Result queryAll() {
return Result.ok(this.emp1Service.queryAll());
}
@GetMapping("/{id}")
@MyCacheable(key = "'emp:id:'+#id")
public Result queryById(@PathVariable("id") Integer id) {
return Result.ok(this.emp1Service.queryById(id));
}
@DeleteMapping("/{id}")
@MyCacheEvict(likeKeys = {"'emp:all'","'emp:id:'+#id"})
public Result removeById(@PathVariable("id") Integer id) {
return Result.ok(this.emp1Service.removeById(id));
}
权限切面
- 编写枚举类
@Getter
public enum AuthCodeEnum {
NO_NEED_PERM("", "不需要权限"),
PERM_MENU_DQL("perm:menu:dql", "查询菜单"),
PERM_MENU_DML("perm:menu:dml", "新增/删除/修改菜单"),
PERM_ROLE_DQL("perm:role:dql", "查询角色"),
PERM_ROLE_DML("perm:role:dml", "新增/删除/修改角色"),
PERM_ROLE_GIVE_MENUS("perm:role:giveMenus", "为角色分配权限"),
PERM_ACCOUNT_DQL("perm:account:dql", "查询用户"),
PERM_ACCOUNT_DML("perm:account:dml", "新增/删除/修改用户"),
PERM_ACCOUNT_GIVE_ROLES("perm:account:giveRoles", "为用户分配角色");
private String code;
private String desc;
AuthCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
- 编写注解
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAuth {
boolean requireAuth() default true;
AuthCodeEnum[] hasPerm() default AuthCodeEnum.NO_NEED_PERM;
LogicalEnum logical() default LogicalEnum.OR;
}
- 编写切面
@Aspect
@Component
public class AuthAspect {
@Pointcut("@annotation(com.shi.annotation.MyAuth)")
public void pointCut() {
}
@Before(value = "pointCut()")
public void handleBefore(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Auth annotation = method.getAnnotation(Auth.class);
if (annotation == null || !annotation.requireAuth()) {
return;
}
// 1. 校验是否登录
checkLogin();
// 2. 校验权限
checkPerm(annotation);
}
private void checkLogin() {
// 1. 获取登录用户
SessionAccountVO sessionAccountVO = UserContext.get();
if (sessionAccountVO == null) {
throw new BusinessException(ResultCodeEnum.NO_LOGIN, "未登录或登录过期");
}
}
private void checkPerm(Auth annotation) {
// 1. 获取登录用户的权限
List<String> accountPerms = UserContext.getAuthority();
AuthCodeEnum[] authCodeEnums = annotation.hasPerm();
// hasPerm空数组的情况
if (authCodeEnums == null || authCodeEnums.length == 0) {
return;
}
LogicalEnum logical = annotation.logical();
ArrayList<AuthCodeEnum> codeList = new ArrayList<>(Arrays.asList(authCodeEnums));
// 逻辑或,逻辑与处理
if (logical == LogicalEnum.OR) {
if (codeList.contains(AuthCodeEnum.NO_NEED_PERM)) {
return;
}
if (accountPerms == null || accountPerms.isEmpty()) {
throw new BusinessException(ResultCodeEnum.NO_PERM);
}
// 是否存在一个权限码在用户的权限列表中
for (AuthCodeEnum codeEnum : codeList) {
if (accountPerms.contains(codeEnum.getCode())) {
return;
}
}
} else {
for (AuthCodeEnum codeEnum : codeList) {
if (codeEnum == AuthCodeEnum.NO_NEED_PERM) {
continue;
}
if (!accountPerms.contains(codeEnum.getCode())) {
throw new BusinessException(ResultCodeEnum.NO_PERM);
}
}
}
}
}
- 实战测试
没有加该注解的就不需要登录验证
@MyAuth(hasPerm = PermCodeEnum.EMP_DELETE)
public Result removeById(@PathVariable("id") Integer id) {
return Result.ok(this.emp1Service.removeById(id));
}
切面限流
滑动窗口限流
- 定义枚举
/**
* 限流类型
**/
public enum LimitType {
/**
* 默认策略全局限流, 根据接口限流
*/
DEFAULT,
/**
* 根据请求者IP进行限流
*/
IP,
/**
* 根据用户限流
*/
USER,
}
- 定义注解
/**
* 限流规则
*/
public @interface RateLimitRule {
/**
* 时间窗口, 单位秒
*
* @return
*/
int time() default 60;
/**
* 允许请求数
*
* @return
*/
int count() default 100;
}
/**
* 限流器
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 支持重复注解
@Repeatable(value = RateLimiters.class)
public @interface RateLimiter {
/**
* 限流键前缀
*
* @return
*/
String key() default "rate_limit:";
/**
* 限流规则
*
* @return
*/
RateLimitRule[] rules() default {};
/**
* 限流类型
*
* @return
*/
LimitType type() default LimitType.DEFAULT;
}
/**
* 限流器容器
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiters {
RateLimiter[] value();
}
- 定义切面
@Component
@Aspect
@Order(2)
public class RateLimiterAspect {
@Resource
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> limitScript;
{
limitScript = new DefaultRedisScript<>("""
local flag = 1
for i = 1, #KEYS do
local window_start = tonumber(ARGV[1]) - tonumber(ARGV[(i - 1) * 2 + 2])
print(tonumber(ARGV[(i - 1) * 2 + 2]))
redis.call('ZREMRANGEBYSCORE', KEYS[i], 0, window_start)
local current_requests = tonumber(redis.call('ZCARD', KEYS[i]))
if current_requests < tonumber(ARGV[(i - 1) * 2 + 3]) then
else
flag = 0
end
end
if flag == 1 then
for i = 1, #KEYS do
print('add')
print(KEYS[i], tonumber(ARGV[1]), ARGV[1])
redis.call('ZADD', KEYS[i], tonumber(ARGV[1]), ARGV[1])
redis.call('pexpire', KEYS[i], tonumber(ARGV[(i - 1) * 2 + 2]))
end
end
return flag
""", Long.class);
}
private static String getIpAddr(HttpServletRequest request) {
String ipAddress;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
try {
ipAddress = InetAddress.getLocalHost().getHostAddress();
} catch (BusinessException e) {
throw new RuntimeException();
}
}
}
// 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null) {
if (ipAddress.contains(",")) {
return ipAddress.split(",")[0];
} else {
return ipAddress;
}
} else {
return "";
}
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
// 定义切点,需要把RateLimiter和RateLimiters同时加进来,否则多重注解不生效
@Pointcut("@annotation(com.shi.annotation.limit.RateLimiter)")
public void rateLimiter() {
}
@Pointcut("@annotation(com.shi.annotation.limit.RateLimiters)")
public void rateLimiters() {
}
// 定义切点之前的操作
@Before("rateLimiter() || rateLimiters()")
public void doBefore(JoinPoint point) {
try {
// 从切点获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
// 获取方法
Method method = signature.getMethod();
String name = point.getTarget().getClass().getName() + "." + signature.getName();
// 获取日志注解
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
RateLimiters rateLimiters = method.getAnnotation(RateLimiters.class);
List<RateLimiter> limiters = new ArrayList<>();
if (rateLimiter != null) {
limiters.add(rateLimiter);
}
if (rateLimiters != null) {
limiters.addAll(Arrays.asList(rateLimiters.value()));
}
if (!allowRequest(limiters, name)) {
throw new RuntimeException("访问过于频繁,请稍候再试");
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("服务器限流异常,请稍候再试");
}
}
/**
* 是否允许请求
*
* @param rateLimiters 限流注解
* @param name 方法全名
* @return 是否放行
*/
private boolean allowRequest(List<RateLimiter> rateLimiters, String name) {
List<String> keys = getKeys(rateLimiters, name);
String[] args = getArgs(rateLimiters);
Long res = stringRedisTemplate.execute(limitScript, keys, args);
System.out.println(res);
return res != null && res == 1L;
}
/**
* 获取限流的键
*
* @param rateLimiters 限流注解
* @param name 方法全名
* @return
*/
private List<String> getKeys(List<RateLimiter> rateLimiters, String name) {
List<String> keys = new ArrayList<>();
for (RateLimiter rateLimiter : rateLimiters) {
String key = rateLimiter.key();
RateLimitRule[] rules = rateLimiter.rules();
LimitType type = rateLimiter.type();
StringBuilder sb = new StringBuilder();
sb.append(key).append(name);
if (LimitType.IP == type) {
String ipAddr = getIpAddr(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
sb.append("_").append(ipAddr);
} else if (LimitType.USER == type) {
// 获取用户id,自己实现
Long userId = 1L;
sb.append("_").append(userId);
}
for (RateLimitRule rule : rules) {
int time = rule.time() * 1000;
int count = rule.count();
StringBuilder builder = new StringBuilder(sb);
builder.append("_").append(time).append("_").append(count);
keys.add(builder.toString());
}
}
return keys;
}
/**
* 获取需要的参数
*
* @param rateLimiters 限流注解
* @return
*/
private String[] getArgs(List<RateLimiter> rateLimiters) {
List<String> args = new ArrayList<>();
args.add(String.valueOf(System.currentTimeMillis()));
for (RateLimiter rateLimiter : rateLimiters) {
RateLimitRule[] rules = rateLimiter.rules();
for (RateLimitRule rule : rules) {
int time = rule.time() * 1000;
int count = rule.count();
args.add(String.valueOf(time));
args.add(String.valueOf(count));
}
}
return args.toArray(new String[]{});
}
}
- 限流测试
/**
* 对于接口,1分钟可以接收1000个请求
* 对于一个ip,1分钟可以接收10个请求
*
* @return
*/
@RateLimiter(rules = {@RateLimitRule(time = 60, count = 1000)})
@RateLimiter(rules = {@RateLimitRule(time = 60, count = 10)}, type = LimitType.IP)
@GetMapping("/test")
public Result test() {
return Result.ok("hello");
}