SpringAop实战

日志切面

@MyLog 注解 属性 desc 使用了SpEl表达式,主要是用来获取形参值,编写动态日志

  1. 定义枚举类
@Getter
public enum LogCodeEnum {

    SELECT("查询"),
    INSERT("添加"),
    UPDATE("修改"),
    DELETE("删除"),
    OTHER("其他");
    String Code;

    LogCodeEnum(String code) {
        Code = code;
    }
}
  1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    LogCodeEnum type() default LogCodeEnum.OTHER;

    /**
     * 日志的描述信息,支持SpEL表达式
     *
     * @return
     */
    String desc() default "";
}
  1. 编写切面类
@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();
    }

}
  1. 实战测试
    /**
     * 通过主键查询单条数据
     *
     * @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”})

话不多说,开始上手

  1. 定义注解
@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();
}
  1. 编写切面
@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;
    }
}
  1. 实战测试
    @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));
    }

权限切面

  1. 编写枚举类
@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;
    }
}

  1. 编写注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAuth {

    /**
     * 是否需要认证
     *
     * @return
     */
    boolean requireAuth() default true;

    /**
     * 需要的权限,默认不需要任何权限就能访问(只要登陆过)
     *
     * @return
     */

    PermCodeEnum hasPerm() default PermCodeEnum.NO_NEED_PERM;
}

  1. 编写切面
@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对象中的 权限码中是否包含 要求的权限,不包含直接抛出异常
    }
}

  1. 实战测试
    没有加该注解的就不需要登录验证
	@MyAuth(hasPerm = PermCodeEnum.EMP_DELETE)
    public Result removeById(@PathVariable("id") Integer id) {
        return Result.ok(this.emp1Service.removeById(id));
    }

切面限流

滑动窗口限流

  1. 定义注解
/**
 * 限流规则
 */
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();
}
  1. 定义切面
@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. 限流测试
	/**
     * 对于接口,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");
    }

Gitee源码地址

https://gitee.com/shi-fangqing/aspect-demo

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值