• 【SpringBoot实战系列】从AOP+自定义注解到redission分布式锁-接口防重提交场景设计实战


    大家好,我是工藤学编程 🦉一个正在努力学习的小博主,期待你的关注
    作业侠系列最新文章😉Java实现聊天程序
    SpringBoot实战系列🐷【【SpringBoot实战系列】AOP+自定义注解-接口防重提交多场景设计实战
    环境搭建大集合环境搭建大集合(持续更新)

    在本栏中,我们之前已经完成了:
    【SpringBoot实战系列】之发送短信验证码
    【SpringBoot实战系列】之从Async组件应用实战到ThreadPoolTaskExecutor⾃定义线程池
    【SpringBoot实战系列】之图形验证码开发并池化Redis6存储
    【SpringBoot实战系列】阿里云OSS接入上传图片实战
    【SpringBoot实战系列】Sharding-Jdbc实现分库分表到分布式ID生成器Snowflake自定义wrokId实战
    【SpringBoot实战系列】RabbitMQ实现消息发送并实现邮箱发送异常监控报警实战

    本片速览:
    1.AOP简介及好处
    2.Spring⾥⾯的AOP常⻅概念
    3.java核心知识-⾃定义注解
    4.防重提交自定义注解实战
    5.分布式锁
    6.切面开发
    7.测试结果

    AOP简介及好处

    Aspect Oriented Program ⾯向切⾯编程, 在不改变原有逻辑上增加额外的功能AOP思想把功能分两个部分,分离系统中的各种关注点
    好处

    • 减少代码侵⼊,解耦
    • 可以统⼀处理横切逻辑
    • ⽅便添加和删除横切逻辑

    Spring⾥⾯的AOP常⻅概念

    • 横切关注点
      对哪些⽅法进⾏拦截,拦截后怎么处理,这些就叫横切关注点
      ⽐如 权限认证、⽇志、事物
    • 通知 Advice
      在特定的切⼊点上执⾏的增强处理
      做啥?
      ⽐如你需要记录⽇志,控制事务 ,提前编写好通⽤的模块,需要的地⽅直接调⽤
      ⽐如重复提交判断逻辑
      类型
    1. @Before前置通知
      在执⾏⽬标⽅法之前运⾏
    2. @After后置通知
      在⽬标⽅法运⾏结束之后
    3. @AfterReturning返回通知
      在⽬标⽅法正常返回值后运⾏
    4. @AfterThrowing异常通知
      在⽬标⽅法出现异常后运⾏
    5. @Around环绕通知
      在⽬标⽅法完成前、后做增强处理 ,环绕通知是最重要的通知类型 ,像事务,⽇志等都是环绕通知,注意编程中核⼼是⼀个ProceedingJoinPoint,需要⼿动执⾏ joinPoint.procced()
    • 连接点 JointPoint
      要⽤通知的地⽅,业务流程在运⾏过程中需要插⼊切⾯的
      具体位置,⼀般是⽅法的调⽤前后,全部⽅法都可以是连接点只是概念,没啥特殊
    • 切⼊点 Pointcut
      不能全部⽅法都是连接点,通过特定的规则来筛选连接点,就是Pointcut,选中那⼏个你想要的⽅法在程序中主要体现为书写切⼊点表达式(通过通配、正则
      表达式)过滤出特定的⼀组 JointPoint连接点过滤出相应的 Advice 将要发⽣的joinpoint地⽅
    • 切⾯ Aspect
      通常是⼀个类,⾥⾯定义 切⼊点+通知 , 定义在什么地⽅;
      什么时间点、做什么事情
      通知 advice指明了时间和做的事情(前置、后置等)切⼊点 pointcut 指定在什么地⽅⼲这个事情web接⼝设计中,web层->⽹关层->服务层->数据层,每⼀层之间也是⼀个切⾯,对象和对象,⽅法和⽅法之间都是⼀个个切⾯
    • ⽬标 target
      ⽬标类,真正的业务逻辑,可以在⽬标类不知情的条件下,增加新的功能到⽬标类的链路上
    • 织⼊ Weaving
      把切⾯(某个类)应⽤到⽬标函数的过程称为织⼊

    java核心知识-⾃定义注解

    • Annotation(注解) 从JDK 1.5开始, Java增加了对元数据(MetaData)的⽀持,也就是 Annotation(注解)。
      注解其实就是代码⾥的特殊标记,它⽤于替代配置⽂件常⻅的很多 @Override、@Deprecated等
    • 什么是元注解
      注解的注解,⽐如当我们需要⾃定义注解时会需要⼀些元注解(meta-annotation),如@Target和@Retention
    • java内置4种元注解
      @Target 表示该注解⽤于什么地⽅
    1. ElementType.CONSTRUCTOR ⽤在构造器
    2. ElementType.FIELD ⽤于描述域-属性上
    3. ElementType.METHOD ⽤在⽅法上
    4. ElementType.TYPE ⽤在类或接⼝上
    5. ElementType.PACKAGE ⽤于描述包
    • @Retention 表示在什么级别保存该注解信息
    1. RetentionPolicy.SOURCE 保留到源码上
    2. RetentionPolicy.CLASS 保留到字节码上
    3. RetentionPolicy.RUNTIME 保留到虚拟机运⾏时(最多,可通过反射获取)
    • @Documented 将此注解包含在 javadoc 中
    • @Inherited 是否允许⼦类继承⽗类中的注解
    • @interface
      ⽤来声明⼀个注解,可以通过default来声明参数的默认值⾃定义注解时,⾃动继承了java.lang.annotation.Annotation接⼝通过反射可以获取⾃定义注解

    防重提交自定义注解实战

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RepeatSubmit {
    
        enum Type {PARAM,TOKEN}
    
        Type limitType() default Type.PARAM;
    
        long lockTime() default 5;
    
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    分布式锁
    redission依赖

    <dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.10.1</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    配置类

    
    
    
    @Configuration
    public class RedissionConfiguration {
        @Value("${spring.redis.host}")
        private String redisHost;
        @Value("${spring.redis.port}")
        private String redisPort;
        @Value("${spring.redis.password}")
        private String redisPwd;
    
        /**
         * 配置分布式锁的redisson
         *
         * @return
         */
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            //单机⽅式
    
            config.useSingleServer().setPassword(redisPwd).setAddress("redis://" + redisHost + ":" + redisPort);
            //集群
    
    
            RedissonClient redissonClient = Redisson.create(config);
            return redissonClient;
        }
        /**
         * 集群模式
         * 备注:可以⽤"rediss://"来启⽤SSL连接
         */
     /*@Bean
     public RedissonClient redissonClusterClient() {
     Config config = new Config();
    
    config.useClusterServers().setScanInterval(2000) //
    集群状态扫描间隔时间,单位是毫秒
    
    .addNodeAddress("redis://127.0.0.1:7000")
    
    .addNodeAddress("redis://127.0.0.1:7002");
     RedissonClient redisson =
    Redisson.create(config);
     return redisson;
     }*/
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    切面开发:

    @Aspect
    @Component
    @Slf4j
    public class RepeatSubmitAspect {
    
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Autowired
        private RedissonClient redissonClient;
    
        /**
         * 定义 @Pointcut注解表达式,
         * ⽅式⼀:@annotation:当执⾏的⽅法上拥有指定的注解时
         ⽣效(我们采⽤这)
         * ⽅式⼆:execution:⼀般⽤于指定⽅法的执⾏
         *
         * @param repeatSubmit
         */
        @Pointcut("@annotation(repeatSubmit)")
        public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
    
        }
    
        /**
         * 环绕通知, 围绕着⽅法执⾏
         * @Around 可以⽤来在调⽤⼀个具体⽅法前和调⽤后来完成⼀些具体的任务。
         *
         * ⽅式⼀:单⽤ @Around("execution(*net.xdclass.controller.*.*(..))")可以
         * ⽅式⼆:⽤@Pointcut和@Around联合注解也可以(我们采⽤这个)
         *
         *
         * 两种⽅式
         * ⽅式⼀:加锁 固定时间内不能᯿复提交
         * 

    * ⽅式⼆:先请求获取token,这边再删除token,删除成功则是第⼀次提交 * * @param joinPoint * @param noRepeatSubmit * @return * @throws Throwable */ @Around("pointCutNoRepeatSubmit(repeatSubmit)") public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo(); boolean res = false; String type = repeatSubmit.limitType().name(); if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) { long lockTime = repeatSubmit.lockTime(); String ippAddr = CommonUtil.getIpAddr(request); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); String className = method.getDeclaringClass().getName(); String key ="order-server-repeat-submit:"+CommonUtil.MD5(String.format("%s-%s-%s-%s", ippAddr, className, method, accountNo)) ; //res=redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.SECONDS); RLock lock = redissonClient.getLock(key); res = lock.tryLock(0, lockTime, TimeUnit.SECONDS); } else { String requestToken = request.getHeader("request-token"); if (StringUtils.isBlank(requestToken)) { throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL); } String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken); res = redisTemplate.delete(key); } if (!res) { log.error("订单重复提交"); return null; } log.info("环绕通知前:{}", CommonUtil.getCurrentTimestamp()); Object obj = joinPoint.proceed(); log.info("环绕通知后:{}", CommonUtil.getCurrentTimestamp()); return obj; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    将自定义的注解加在对应想要防重提交的方法上即可
    
    • 1
     @PostMapping("page")
        @RepeatSubmit
        public JsonData page(@RequestBody OrderPageRequest orderPageRequest){
            Map<String,Object>pageResult = productOrderService.page(orderPageRequest);
    
            return JsonData.buildSuccess(pageResult);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    访问对应接口
    在这里插入图片描述
    本篇完!

  • 相关阅读:
    字符识别数据集构建(1)
    c++day3
    Centos7.9部署snort-2.9.20
    rabbitmq 保姆级安装教程
    什么是三元表达式?
    nodejs+vue+elementui零食食品o2o商城系统
    Python练手算法
    MySQL主从复制读写分离
    spring boot 一个极简单的 demo 示例
    [创业-46]:中小公司的组织架构与公司管理
  • 原文地址:https://blog.csdn.net/chandfy/article/details/126111387