環(huán)境:SpringBoot2.7.12
1. 概述
本文將介紹如何為API接口動(dòng)態(tài)添加開(kāi)關(guān)功能。通過(guò)這個(gè)功能,我們可以控制API接口的正常訪問(wèn)或顯示提示信息。這有助于在開(kāi)發(fā)和維護(hù)過(guò)程中更好地管理和控制API接口的行為。通過(guò)使用開(kāi)關(guān)功能,我們可以輕松地在不同情況下調(diào)整接口的行為,提高系統(tǒng)的靈活性和可維護(hù)性。
為什么要給API接口添加開(kāi)關(guān)功能呢?從以下幾方面考慮:
- 靈活性和可擴(kuò)展性:我們可以根據(jù)需要?jiǎng)討B(tài)地控制API接口的行為。這使得在面對(duì)不同場(chǎng)景和需求時(shí),可以更加靈活地調(diào)整接口的行為,而無(wú)需對(duì)代碼進(jìn)行修改。
- 安全性和控制:有時(shí),我們可能不希望在特定情況下API接口被正常訪問(wèn),例如在測(cè)試、維護(hù)或敏感數(shù)據(jù)訪問(wèn)等場(chǎng)景下。通過(guò)關(guān)閉開(kāi)關(guān)并顯示提示信息,我們可以對(duì)用戶進(jìn)行必要的通知和引導(dǎo),確保接口不被未經(jīng)授權(quán)的訪問(wèn)。
- 錯(cuò)誤處理和日志記錄:當(dāng)API接口出現(xiàn)錯(cuò)誤或異常時(shí),關(guān)閉開(kāi)關(guān)并顯示提示信息可以幫助我們更好地追蹤和記錄問(wèn)題。這對(duì)于后續(xù)的問(wèn)題排查和系統(tǒng)優(yōu)化非常有幫助。
- 系統(tǒng)監(jiān)控和管理:通過(guò)監(jiān)控開(kāi)關(guān)狀態(tài)的變化,我們可以了解系統(tǒng)的運(yùn)行狀況,以及用戶對(duì)API接口的使用情況。這有助于進(jìn)行系統(tǒng)性能優(yōu)化和資源管理。
- 用戶體驗(yàn):在某些情況下,當(dāng)API接口不可用或需要維護(hù)時(shí),向用戶顯示友好的提示信息可以避免用戶感到困惑或帶來(lái)不必要的困擾。同時(shí),提前通知用戶也體現(xiàn)了對(duì)用戶體驗(yàn)的關(guān)注。
- 合規(guī)性和隱私保護(hù):在涉及敏感數(shù)據(jù)或受限制的API接口中,通過(guò)關(guān)閉開(kāi)關(guān)并顯示提示信息,可以確保遵守相關(guān)法規(guī)和隱私政策,對(duì)數(shù)據(jù)進(jìn)行適當(dāng)?shù)墓芾砗捅Wo(hù)。
2. 實(shí)現(xiàn)方案
首先,定義一個(gè)AOP切面(Aspect),該切面負(fù)責(zé)控制接口(Controller)的開(kāi)關(guān)行為。
在切面中,我們可以使用Spring AOP的切入點(diǎn)(Pointcut)來(lái)指定需要攔截的方法。一旦方法被攔截,我們可以在切面的通知(Advice)中定義相應(yīng)的默認(rèn)行為。接下來(lái)我們將一步一步的實(shí)現(xiàn)接口開(kāi)關(guān)功能。
- 自定義注解
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiSwitch {
/**接口對(duì)應(yīng)的key,通過(guò)可以該key查詢接口是否關(guān)閉*/
String key() default "" ;
/**解析器beanName,通過(guò)具體的實(shí)現(xiàn)獲取key對(duì)應(yīng)的值*/
String resolver() default "" ;
/**開(kāi)啟后降級(jí)方法名*/
String fallback() default "" ;
}
- 定義解析器接口
public interface SwitchResolver {
boolean resolver(String key) ;
public void config(String key, Integer onoff) ;
}
- 接口默認(rèn)實(shí)現(xiàn)
@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實(shí)現(xiàn)
@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)) ;
}
}
這里就提供兩種默認(rèn)的實(shí)現(xiàn)。
- 定義切面
@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 {
// 對(duì)應(yīng)接口開(kāi)關(guān)的key
String key = apiSwitch.key() ;
// 解析器bean的名稱
String resolverName = apiSwitch.resolver() ;
// 降級(jí)方法名
String fallback = apiSwitch.fallback() ;
SwitchResolver resolver = null ;
// 根據(jù)指定的beanName獲取具體的解析器;以下都不考慮不存在的情況
if (StringUtils.hasLength(resolverName)) {
resolver = this.context.getBean(resolverName, SwitchResolver.class) ;
} else {
resolver = this.context.getBean(MAPPINGS.get(this.switchProperties.getResolver())) ;
}
// 解析器不存在則直接調(diào)用目標(biāo)接口
if (resolver == null || !resolver.resolver(key)) {
return pjp.proceed() ;
}
// 調(diào)用降級(jí)的方法;關(guān)于降級(jí)的方法簡(jiǎn)單點(diǎn),都必須在當(dāng)前接口類中,同時(shí)還不考慮方法參數(shù)的情況
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 ;
}
}
- 修改開(kāi)關(guān)接口
@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" ;
}
通過(guò)該接口修改具體哪個(gè)接口的開(kāi)關(guān)狀態(tài)。(注意:這里有小問(wèn)題,如果接口上指定了resolver類型且配置文件中指定的類型不一致,就會(huì)出現(xiàn)不生效問(wèn)題。這個(gè)問(wèn)題大家可以自行解決)
- 接下來(lái)進(jìn)行測(cè)試
@GetMapping("/q1")
@ApiSwitch(key = "swtich$q1", fallback = "q1_fallback", resolver = "redisResolver")
public Object q1() {
return "q1" ;
}
public Object q1_fallback() {
return "接口維護(hù)中" ;
}
這是完整的配置示例,這里除了key必須外,其它的都可以不填寫(xiě)。
具體測(cè)試結(jié)果就不貼了,大家可以自行測(cè)試基于jvm內(nèi)存和redis的方式。
總結(jié):通過(guò)上述介紹,我們可以看到使用Spring AOP實(shí)現(xiàn)接口的開(kāi)關(guān)功能是一種非常有效的方法。通過(guò)定義AOP切面和切入點(diǎn),我們可以精確地?cái)r截需要控制的方法,并在通知中根據(jù)開(kāi)關(guān)狀態(tài)執(zhí)行相應(yīng)的邏輯。這種技術(shù)手段有助于提高代碼的可維護(hù)性和可擴(kuò)展性,同時(shí)提供更好的靈活性和控制性來(lái)管理接口的行為