優(yōu)雅實現API接口開關:讓你的應用更可控
環(huán)境:SpringBoot2.7.12
1. 概述
本文將介紹如何為API接口動態(tài)添加開關功能。通過這個功能,我們可以控制API接口的正常訪問或顯示提示信息。這有助于在開發(fā)和維護過程中更好地管理和控制API接口的行為。通過使用開關功能,我們可以輕松地在不同情況下調整接口的行為,提高系統(tǒng)的靈活性和可維護性。
為什么要給API接口添加開關功能呢?從以下幾方面考慮:
- 靈活性和可擴展性:我們可以根據需要動態(tài)地控制API接口的行為。這使得在面對不同場景和需求時,可以更加靈活地調整接口的行為,而無需對代碼進行修改。
- 安全性和控制:有時,我們可能不希望在特定情況下API接口被正常訪問,例如在測試、維護或敏感數據訪問等場景下。通過關閉開關并顯示提示信息,我們可以對用戶進行必要的通知和引導,確保接口不被未經授權的訪問。
- 錯誤處理和日志記錄:當API接口出現錯誤或異常時,關閉開關并顯示提示信息可以幫助我們更好地追蹤和記錄問題。這對于后續(xù)的問題排查和系統(tǒng)優(yōu)化非常有幫助。
- 系統(tǒng)監(jiān)控和管理:通過監(jiān)控開關狀態(tài)的變化,我們可以了解系統(tǒng)的運行狀況,以及用戶對API接口的使用情況。這有助于進行系統(tǒng)性能優(yōu)化和資源管理。
- 用戶體驗:在某些情況下,當API接口不可用或需要維護時,向用戶顯示友好的提示信息可以避免用戶感到困惑或帶來不必要的困擾。同時,提前通知用戶也體現了對用戶體驗的關注。
- 合規(guī)性和隱私保護:在涉及敏感數據或受限制的API接口中,通過關閉開關并顯示提示信息,可以確保遵守相關法規(guī)和隱私政策,對數據進行適當的管理和保護。
2. 實現方案
首先,定義一個AOP切面(Aspect),該切面負責控制接口(Controller)的開關行為。
在切面中,我們可以使用Spring AOP的切入點(Pointcut)來指定需要攔截的方法。一旦方法被攔截,我們可以在切面的通知(Advice)中定義相應的默認行為。接下來我們將一步一步的實現接口開關功能。
- 自定義注解
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiSwitch {
/**接口對應的key,通過可以該key查詢接口是否關閉*/
String key() default "" ;
/**解析器beanName,通過具體的實現獲取key對應的值*/
String resolver() default "" ;
/**開啟后降級方法名*/
String fallback() default "" ;
}
- 定義解析器接口
public interface SwitchResolver {
boolean resolver(String key) ;
public void config(String key, Integer onoff) ;
}
- 接口默認實現
@Component
public class ConcurrentMapResolver implements SwitchResolver {
private Map<String, Integer> keys = new ConcurrentHashMap<>() ;
@Override
public boolean resolver(String key) {
Integer value = keys.get(key) ;
return value == null ? false : (value == 1) ;
}
public void config(String key, Integer onoff) {
keys.put(key, onoff) ;
}
}
- 基于redis實現
@Component
public class RedisResolver implements SwitchResolver {
private final StringRedisTemplate stringRedisTemplate ;
public RedisResolver(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate ;
}
@Override
public boolean resolver(String key) {
String value = this.stringRedisTemplate.opsForValue().get(key) ;
return !(value == null || "0".equals(value)) ;
}
@Override
public void config(String key, Integer onoff) {
this.stringRedisTemplate.opsForValue().set(key, String.valueOf(onoff)) ;
}
}
這里就提供兩種默認的實現。
- 定義切面
@Component
@Aspect
public class ApiSwitchAspect implements ApplicationContextAware {
private ApplicationContext context ;
private final SwitchProperties switchProperties ;
public static final Map<String, Class<? extends SwitchResolver>> MAPPINGS;
static {
// 初始化所有的解析器
Map<String, Class<? extends SwitchResolver>> mappings = new HashMap<>() ;
mappings.put("map", ConcurrentMapResolver.class) ;
mappings.put("redis", RedisResolver.class) ;
MAPPINGS = Collections.unmodifiableMap(mappings) ;
}
public ApiSwitchAspect(SwitchProperties switchProperties) {
this.switchProperties = switchProperties ;
}
@Pointcut("@annotation(apiSwitch)")
private void onoff(ApiSwitch apiSwitch) {}
@Around("onoff(apiSwitch)")
public Object ctl(ProceedingJoinPoint pjp, ApiSwitch apiSwitch) throws Throwable {
// 對應接口開關的key
String key = apiSwitch.key() ;
// 解析器bean的名稱
String resolverName = apiSwitch.resolver() ;
// 降級方法名
String fallback = apiSwitch.fallback() ;
SwitchResolver resolver = null ;
// 根據指定的beanName獲取具體的解析器;以下都不考慮不存在的情況
if (StringUtils.hasLength(resolverName)) {
resolver = this.context.getBean(resolverName, SwitchResolver.class) ;
} else {
resolver = this.context.getBean(MAPPINGS.get(this.switchProperties.getResolver())) ;
}
// 解析器不存在則直接調用目標接口
if (resolver == null || !resolver.resolver(key)) {
return pjp.proceed() ;
}
// 調用降級的方法;關于降級的方法簡單點,都必須在當前接口類中,同時還不考慮方法參數的情況
if (!StringUtils.hasLength(fallback)) {
// 未配置的情況
return "接口不可用" ;
}
Class<?> clazz = pjp.getSignature().getDeclaringType() ;
Method fallbackMethod = clazz.getDeclaredMethod(fallback) ;
return fallbackMethod.invoke(pjp.getTarget()) ;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext ;
}
}
- 修改開關接口
@GetMapping("/onoff/{state}")
public Object onoff(String key, @PathVariable("state") Integer state) {
String resolverType = switchProperties.getResolver();
if (!StringUtils.hasLength(resolverType)) {
SwitchResolver bean = this.context.getBean(ApiSwitchAspect.MAPPINGS.get("map")) ;
if (bean instanceof ConcurrentMapResolver resolver) {
resolver.config(key, state) ;
}
} else {
SwitchResolver resolver = this.context.getBean(ApiSwitchAspect.MAPPINGS.get(resolverType)) ;
resolver.config(key, state) ;
}
return "success" ;
}
通過該接口修改具體哪個接口的開關狀態(tài)。(注意:這里有小問題,如果接口上指定了resolver類型且配置文件中指定的類型不一致,就會出現不生效問題。這個問題大家可以自行解決)
- 接下來進行測試
@GetMapping("/q1")
@ApiSwitch(key = "swtich$q1", fallback = "q1_fallback", resolver = "redisResolver")
public Object q1() {
return "q1" ;
}
public Object q1_fallback() {
return "接口維護中" ;
}
這是完整的配置示例,這里除了key必須外,其它的都可以不填寫。
具體測試結果就不貼了,大家可以自行測試基于jvm內存和redis的方式。
總結:通過上述介紹,我們可以看到使用Spring AOP實現接口的開關功能是一種非常有效的方法。通過定義AOP切面和切入點,我們可以精確地攔截需要控制的方法,并在通知中根據開關狀態(tài)執(zhí)行相應的邏輯。這種技術手段有助于提高代碼的可維護性和可擴展性,同時提供更好的靈活性和控制性來管理接口的行為