SpringBoot + 自定義註解 + AOP 高階玩法打造通用開關

2023-10-17 12:02:02

前言

最近在工作中遷移程式碼的時候發現了以前自己寫的一個通用開關實現,發現挺不錯,特地拿出來分享給大家。

為了有良好的演示效果,我特地重新建了一個專案,把核心程式碼提煉出來加上了更多註釋說明,希望xdm喜歡。

案例

1、專案結構

2、引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

3、yml設定

連線Redis的設定修改成自己的

server:
  port: 8888

spring:
  redis:
    database: 6
    host: xx.xx.xx.xx
    port: 6379
    password: 123456
    jedis:
      pool:
        max-active: 100
        max-wait: -1ms
        max-idle: 50
        min-idle: 1

4、自定義註解

這裡稍微說明下,定義了一個key對應不同功效的開關,定義了一個val作為開關是否開啟的標識,以及一個message作為訊息提示。

package com.example.commonswitch.annotation;

import com.example.commonswitch.constant.Constant;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * <p>
 * 通用開關注解
 * </p>
 *
 * @author 程式設計師濟癲
 * @since 2023-10-16 17:38
 */
@Target({ElementType.METHOD})  // 作用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 執行時起作用
public @interface ServiceSwitch {

   /**
    * 業務開關的key(不同key代表不同功效的開關)
    * {@link Constant.ConfigCode}
    */
   String switchKey();

   // 開關,0:關(拒絕服務並給出提示),1:開(放行)
   String switchVal() default "0";

   // 提示資訊,預設值可在使用註解時自行定義。
   String message() default "當前請求人數過多,請稍後重試。";
}

5、定義常數

主要用來存放各種開關的key

package com.example.commonswitch.constant;

/**
 * <p>
 * 常數類
 * </p>
 *
 * @author 程式設計師濟癲
 * @since 2023-10-16 17:45
 */
public class Constant {

   // .... 其他業務相關的常數 ....

   // 設定相關的常數
   public static class ConfigCode {

      // 掛號支付開關(0:關,1:開)
      public static final String REG_PAY_SWITCH = "reg_pay_switch";
      // 門診支付開關(0:關,1:開)
      public static final String CLINIC_PAY_SWITCH = "clinic_pay_switch";

      // 其他業務相關的設定常數
      // ....
   }
}

6、AOP核心實現

核心實現中我專門加了詳細的註釋說明,保證大家一看就懂,而且把查詢開關的方式列舉出來供大家自己選擇。

package com.example.commonswitch.aop;

import com.example.commonswitch.annotation.ServiceSwitch;
import com.example.commonswitch.constant.Constant;
import com.example.commonswitch.exception.BusinessException;
import com.example.commonswitch.util.Result;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * <p>
 * 開關實現的切面類
 * </p>
 *
 * @author 程式設計師濟癲
 * @since 2023-10-16 17:56
 */
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class ServiceSwitchAOP {

   private final StringRedisTemplate redisTemplate;

   /**
    * 定義切點,使用了@ServiceSwitch註解的類或方法都攔截
    */
   @Pointcut("@annotation(com.example.commonswitch.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 switchVal = annotation.switchVal();
            String message = annotation.message();

            /*
              獲取設定項說明
              這裡有兩種方式:1、設定加在Redis,查詢時從Redis獲取;
                          2、設定加在資料庫,查詢時從表獲取。(MySQL單表查詢其實很快,設定表其實也沒多少資料)
              我在工作中的做法:直接放到資料庫,但是獲取設定項的方法用SpringCache快取,
                           然後在後臺管理中操作設定項,變更時清除快取即可。
                           我這麼做就是結合了上面兩種各自的優點,因為專案中設定一般都是用後臺管理來操作的,
                           查表當然更舒適,同時加上快取提高查詢效能。
             */

            // 下面這塊查詢設定項,大家可以自行接入並修改。
            // 資料庫這麼查詢:String configVal = systemConfigService.getConfigByKey(switchKey);
            // 這裡我直接從redis中取,使用中大家可以按照意願自行修改。
            String configVal = redisTemplate.opsForValue().get(Constant.ConfigCode.REG_PAY_SWITCH);
            if (switchVal.equals(configVal)) {
               // 開關開啟,則返回提示。
               return new Result<>(HttpStatus.FORBIDDEN.value(), message);
            }
         }

         // 放行
         return point.proceed(args);

      } catch (Throwable e) {
         throw new BusinessException(e.getMessage(), e);
      }
   }
}

7、使用註解

我們定義一個服務來使用這個開關,我設定了一個場景是掛號下單,也就是把開關用在支付業務這裡。

因為支付場景線上上有可能出現未知問題,比如第三方rpc呼叫超時或不響應,或者對方業務出現缺陷,導致我方不斷出現長款,那麼我們此時立馬操作後臺將支付開關關掉,能最大程度止損。

package com.example.commonswitch.service;

import com.example.commonswitch.annotation.ServiceSwitch;
import com.example.commonswitch.constant.Constant;
import com.example.commonswitch.util.Result;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 掛號服務
 * </p>
 *
 * @author 程式設計師濟癲
 * @since 2023-10-16 18:48
 */
@Service
public class RegService {

   /**
    * 掛號下單
    */
   @ServiceSwitch(switchKey = Constant.ConfigCode.REG_PAY_SWITCH)
   public Result createOrder() {

      // 具體下單業務邏輯省略....

      return new Result(HttpStatus.OK.value(), "掛號下單成功");
   }
}

8、測試效果

好了,接下來我們定義一個介面來測試效果如何。

package com.example.commonswitch.controller;

import com.example.commonswitch.service.RegService;
import com.example.commonswitch.util.Result;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 掛號介面
 * </p>
 *
 * @author 程式設計師濟癲
 * @since 2023-10-16 18:51
 */
@RestController
@RequestMapping("/api/reg")
@AllArgsConstructor
public class RegController {

   private final RegService regService;

   @PostMapping("/createOrder")
   public Result createOrder() {

      return regService.createOrder();
   }
}

Redis中把開關加上去(實際工作中是後臺新增的哈),此時開關是1,表示開關開啟。

調介面,可以發現,目前是正常的業務流程。

接下來,我們假定線上出了問題,要立馬將開關關閉。(還是操作Redis,實際工作中是後臺直接關掉哈)

我們將其改為0,也就是表示開關給關閉。

看效果,OK,沒問題,是我們預想的結果。

這裡要記住一點,提示可以自定義,但是不要直接返回給使用者系統異常,給一個友好提示即可。

總結

文中使用到的技術主要是這些:SpringBoot、自定義註解、AOP、Redis、Lombok。

其中,自定義註解和AOP是核心實現,Redis是可選項,你也可以接入到資料庫。

lombok的話大家可以仔細看程式碼,我用它幫助省略了所有@Autowaird,這樣就使用了官方及IDEA推薦的構造器注入方式。

好了,今天的小案例,xdm學會了嗎。


如果喜歡,請點贊+關注↓↓↓,持續分享乾貨哦!