日志切面
@MyLog 注解 属性 desc 使用了SpEl表达式,主要是用来获取形参值,编写动态日志
- 定义枚举类
@Getter
public enum LogCodeEnum {
SELECT("查询"),
INSERT("添加"),
UPDATE("修改"),
DELETE("删除"),
OTHER("其他");
String Code;
LogCodeEnum(String code) {
Code = code;
}
}
- 定义注解
@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();
System.out.println(methodSignature);
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 PermCodeEnum {
NO_NEED_PERM("no:need:auth", "不需要任何权限"),
EMP_SELECT("emp:select", "emp表的查询权限"),
EMP_DELETE("emp:delete", "emp表的删除权限"),
EMP_UPDATE("emp:update", "emp表的修改权限");
private String code;
private String desc;
PermCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
- 编写注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAuth {
/**
* 是否需要认证
*
* @return
*/
boolean requireAuth() default true;
/**
* 需要的权限,默认不需要任何权限就能访问(只要登陆过)
*
* @return
*/
PermCodeEnum hasPerm() default PermCodeEnum.NO_NEED_PERM;
}
- 编写切面
@Aspect
@Component
@Order(-1)
public class AuthAspect {
/**
* 将常量放到 常量类中管理,因为在其他地方也需要用到
*/
private static final String SESSION_LOGIN_USER_KEY = "session_login_user_key";
@Pointcut("@annotation(com.shi.annotation.MyAuth)")
public void pointCut() {
}
@Before("pointCut()")
public void handle(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
MyAuth annotation = method.getAnnotation(MyAuth.class);
// 不需要认证
if (!annotation.requireAuth()) {
return;
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
// 检查是否登录
checkLogin(session);
// 不需要任何权限
if (annotation.hasPerm() == PermCodeEnum.NO_NEED_PERM) {
return;
}
checkPerm(session, annotation);
}
/**
* 检查是否登录
* 扩展:如果使用的token,请修改代码逻辑
*
* @param session
*/
private void checkLogin(HttpSession session) {
Object attribute = session.getAttribute(SESSION_LOGIN_USER_KEY);
if (attribute == null) {
throw new BusinessException(ResultCodeEnum.NO_LOGIN);
}
}
private void checkPerm(HttpSession session, MyAuth annotation) {
Object attribute = session.getAttribute(SESSION_LOGIN_USER_KEY);
String code = annotation.hasPerm().getCode();
// 将 attribute 强转为 session中的用户对象
// 判断session对象中的 权限码中是否包含 要求的权限,不包含直接抛出异常
}
}
- 实战测试
没有加该注解的就不需要登录验证
@MyAuth(hasPerm = PermCodeEnum.EMP_DELETE)
public Result removeById(@PathVariable("id") Integer id) {
return Result.ok(this.emp1Service.removeById(id));
}
切面限流
滑动窗口限流
- 定义注解
/**
* 限流规则
*/
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");
}