自定义注解实现服务动态开关

shigen日更文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。
🧑‍💻🧑‍💻🧑‍💻Make things different and more efficient

接近凌晨了,今天的稿子还没来得及写,甚是焦虑,于是熬了一个夜也的给它写完。正如我的题目所说:《自定义注解实现服务动态开关》,接下来和shigen一起来揭秘吧。


前言

shigen实习的时候,遇到了业务场景:实现服务的动态开关,避免redis的内存被打爆了。 当时的第一感受就是这个用nacos配置一下不就可以了,nacos不就是有一个注解refreshScope,配置中心的配置文件更新了,服务动态的更新。当时实现是这样的:

在我的nacos上这样配置的:

yaml 复制代码
 service:
   enable: true

那对应的java部分的代码就是这样的:

typescript 复制代码
 class Service {
   @Value("service.enable")
   private boolean serviceEnable;
   
   public void method() {
     if (!serviceEnable) {
       return;
     }
     // 业务逻辑
   }
 }

貌似这样是可以的,因为我们只需要动态的观察数据的各项指标,遇到了快要打挂的情况,直接把布尔值换成false即可。


但是不优雅,我们来看看有什么不优雅的:

  1. 配置的动态刷新是有延迟的。nacos的延迟是依赖于网络的;
  2. 不亲民。万一哪个开发改坏了配置,服务就是彻底的玩坏了;而且,如果业务想做一个动态的配置,任何人都可以在系统上点击开关,类似于下边的操作:

nacos配置的方式直接不可行了!

那给予以上的问题,相信部分的伙伴已经思考到了:那我把配置放在redis中呗,内存数据库,直接用外部接口控制数据。

很好,这种想法打开了今天的设计思路。我们先协一点伪代码:

ini 复制代码
 @getMapping(value="switch") 
 public Integer switch() {
     Integer status = redisTemplate.get("key");
     if (status == 1) {
       status = 0;
     } else {
       status = 1;
     }
     redisTemplate.set("key", status);
     return status;
 }
 ​
 ​
 @getMapping(value= "pay")
 public Result pay() {
   Integer status = redisTemplate.get("key");
   if (status ==0) {
     throw new Bizexception("服务不可用");
   } else {
     doSometing();
   }
 }

貌似超级完美了,但是想过没有,业务的侵入很大呢。而且,万一我的业务拓展了,别的地方也需要这样的配置,岂不是直接复制粘贴?那就到此为止吧。


我觉得任何业务的设计都是需要去思考的,一味的写代码,做着CRUD的各种操作,简直是等着被AI取代吧。

那接下来分享shigen的设计,带着大家从我的视角分析我的思考和设计点、关注点。

代码设计

注解设计

less 复制代码
 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface ServiceSwitch {
 ​
     String switchKey();
 ​
     String message() default "当前业务已关闭,请稍后再试!";
 ​
 }

我在设计的时候,考虑到了不同的业务模块和失败的信息,这些都可以抽取出来,在使用的时候,直接加上注解即可。具体的方法和拦截,我们采用spring的AOP来做。

常量类

arduino 复制代码
 public class Constants {
 ​
     public static final String ON = "1";
     public static final String OFF = "0";
 ​
     public static class Service {
 ​
         public static final String ORDER = "service-order";
         public static final String PAY = "service-pay";
     }
 ​
 }

既然涉及到了业务模块和状态值,那配置一个常量类是再合适不过了。

业务代码

java 复制代码
   @ServiceSwitch(switchKey = Constants.Service.PAY)
   public Result pay() {
       log.info("paying now");
       return Result.success();
   }

业务代码上,我们肯定喜欢这样的设计,直接加上一个注解标注我们想要控制的模块。

请注意,核心点来了,我们注解的AOP怎么设计?

AOP设计

老方式,我们先看一下代码:

less 复制代码
 @Aspect
 @Component
 @Slf4j
 public class ServiceSwitchAOP {
 ​
     @Resource
     private RedisTemplate<String, String> redisTemplate;
 ​
     /**
      * 定义切点,使用了@ServiceSwitch注解的类或方法都拦截 需要用注解的全路径
      */
     @Pointcut("@annotation(main.java.com.shigen.redis.annotation.ServiceSwitch)")
     public void pointcut() {
     }
 ​
     @Around("pointcut()")
     public Object around(ProceedingJoinPoint point) {
 ​
         // 获取被代理的方法的参数
         Object[] args = point.getArgs();
         // 获取被代理的对象
         Object target = point.getTarget();
         // 获取通知签名
         MethodSignature signature = (MethodSignature) point.getSignature();
 ​
         try {
 ​
             // 获取被代理的方法
             Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
             // 获取方法上的注解
             ServiceSwitch annotation = method.getAnnotation(ServiceSwitch.class);
 ​
             // 核心业务逻辑
             if (annotation != null) {
 ​
                 String switchKey = annotation.switchKey();
                 String message = annotation.message();
                 /**
                  * 配置项: 可以存储在mysql、redis 数据字典
                  */
                 String configVal = redisTemplate.opsForValue().get(switchKey);
                 if (Constants.OFF.equals(configVal)) {
                     // 开关关闭,则返回提示。
                     return new Result(HttpStatus.FORBIDDEN.value(), message);
                 }
             }
 ​
             // 放行
             return point.proceed(args);
         } catch (Throwable e) {
             throw new RuntimeException(e.getMessage(), e);
         }
     }
 }

拦截我的注解,实现一个切点,之后通知切面进行操作。在切面的操作上,我们读取注解的配置,然后从redis中拿取对应的服务状态。如果服务的状态是关闭的,直接返回我们自定义的异常类型;服务正常的话,继续进行操作。

接口测试

最后,我写了两个接口实现了服务的调用和服务模块状态值的切换。

less 复制代码
 @RestController
 @RequestMapping(value = "serviceSwitch")
 public class ServiceSwitchTestController {
 ​
     @Resource
     private ServiceSwitchService serviceSwitchService;
 ​
     @GetMapping(value = "pay")
     public Result pay() {
         return serviceSwitchService.pay();
     }
 ​
     @GetMapping(value = "switch")
     public Result serviceSwitch(@RequestParam(value = "status", required = false) String status) {
         serviceSwitchService.switchService(status);
         return Result.success();
     }
 }

代码测试

测试服务正常

此时,redis中服务的状态值是1,服务也可以正常的调用。

测试服务不正常

我们先调用接口,改变服务的状态:

再次调用服务:

发现服务403错误,已经不能调用了。我们改变一下状态,服务又可以用了,这里就不做展示了。


以上就是今天分享的全部内容了,觉得不错的话,记得点赞 在看 关注支持一下哈,您的鼓励和支持将是shigen坚持日更的动力。同时,shigen在多个平台都有文章的同步,也可以同步的浏览和订阅:

平台 账号 链接
CSDN shigen01 shigen的CSDN主页
知乎 gen-2019 shigen的知乎主页
掘金 shigen01 shigen的掘金主页
腾讯云开发者社区 shigen shigen的腾讯云开发者社区主页
微信公众平台 shigen 公众号名:shigen

shigen一起,每天不一样!

相关推荐
喵个咪4 分钟前
go-wind-cms 微服务架构设计:为什么基于 Kratos?
后端·微服务·cms
神奇小汤圆10 分钟前
百度面试官:Redis 内存满了怎么办?你有想过吗?
后端
喵个咪12 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
开心就好202513 分钟前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
小江的记录本15 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
神奇小汤圆16 分钟前
Spring Batch实战
后端
喵个咪18 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
程序员木圭21 分钟前
07-数组入门必看!Java数组的内存分析02
java·后端
永霖光电_UVLED27 分钟前
氧化镓高体积热容的特性,集成高介电常数界面的结侧冷却架构
人工智能·生成对抗网络·架构·汽车·制造