前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自定义注解实现服务的动态开关

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

原创
作者头像
shigen
发布2023-11-15 08:26:37
3020
发布2023-11-15 08:26:37
举报
文章被收录于专栏:shigen的学习笔记

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

🧑‍💻🧑‍💻🧑‍💻Make things different and more efficient

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

前言

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

在我的nacos上这样配置的:

代码语言:yaml
复制
service:
	enable: true

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

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

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

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

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

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

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

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

代码语言:java
复制
@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的设计,带着大家从我的视角分析我的思考和设计点、关注点。

代码设计

注解设计
代码语言:java
复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceSwitch {

    String switchKey();

    String message() default "当前业务已关闭,请稍后再试!";

}

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

常量类
代码语言:java
复制
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设计

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

代码语言:java
复制
@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中拿取对应的服务状态。如果服务的状态是关闭的,直接返回我们自定义的异常类型;服务正常的话,继续进行操作。

接口测试

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

代码语言:java
复制
@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

知乎

gen-2019

掘金

shigen01

腾讯云开发者社区

shigen

微信公众平台

shigen

公众号名:shigen

shigen一起,每天不一样!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 代码设计
    • 注解设计
      • 常量类
        • 业务代码
          • AOP设计
            • 接口测试
            • 代码测试
              • 测试服务正常
                • 测试服务不正常
                相关产品与服务
                云开发 CloudBase
                云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档