詳解OpenAPI開發(fā)如何動(dòng)態(tài)的添加接口實(shí)現(xiàn)
0 | 需求說(shuō)明
在如何動(dòng)態(tài)的處理接口的返回?cái)?shù)據(jù) 里提到了我們的業(yè)務(wù)場(chǎng)景:服務(wù)A對(duì)接了服務(wù)B,服務(wù)C等服務(wù)的一些接口,然后由服務(wù)A統(tǒng)一暴露接口給到外部用戶使用。
其中有個(gè)需求是:服務(wù)A可以動(dòng)態(tài)的接入服務(wù)B/C的接口,對(duì)外暴露,并無(wú)需重啟服務(wù)A,即支持API接口的動(dòng)態(tài)添加。
1 | 思路方案
傳統(tǒng)的API接口暴露方法
傳統(tǒng)的業(yè)務(wù)開發(fā),使用 springboot 的話,會(huì)把服務(wù)需要暴露的 API 接口寫在 controller 層里,然后調(diào)用 service 層的接口方法,在實(shí)現(xiàn)層 implement 該 service 接口方法的具體實(shí)現(xiàn)函數(shù)。
- controller層
@RestController @RequestMapping({"/v1"}) @Slf4j public class HelloController { @Autowired private HelloService helloService; @PostMapping(path = {"/hello"}) public String hello() { return Optional.ofNullable(helloService.hello()) .map(ret -> new ResponseEntity<>(ret, HttpStatus.OK)) .orElseThrow(() -> new MMException("something wrong")); } }
- service層
public interface HelloService { String hello(); }
- 實(shí)現(xiàn)層
@Service public class HelloServiceImpl implements HelloService { @Override public String hello(){ return "hello world"; } }
我們可以看到,在 controller 層 API 接口的 subpath 是寫好的,構(gòu)建部署之后,服務(wù)具有的 API 接口列表就固定了。如果需要新增 API 接口,就需要重新在 controller 里寫代碼,編譯構(gòu)建,再部署上線。這樣效率很低,而且每次部署,都會(huì)影響到線上服務(wù),也不安全。
泛化的方法
對(duì)于 OpenAPI 的業(yè)務(wù)場(chǎng)景來(lái)說(shuō),其實(shí)是不關(guān)心接入的 API subpath 具體是什么,只關(guān)心能不能通過(guò) subpath 找到對(duì)應(yīng)的需要轉(zhuǎn)發(fā)的服務(wù)。
那么,在 controller 層,就可以不根據(jù)具體的 subpath 來(lái)匹配對(duì)應(yīng)的服務(wù),而是通過(guò)請(qǐng)求的方法get、post、put + 通配符的方式來(lái)分類接收外部請(qǐng)求,然后利用 AOP切面 和 Threadlocal 來(lái)處理和傳遞 subpath 攜帶的信息,給到實(shí)現(xiàn)層,最終在實(shí)現(xiàn)層分發(fā)請(qǐng)求到各個(gè)業(yè)務(wù)服務(wù)的 API 接口。
2 | 具體實(shí)施
OpenAPI 的 URL 規(guī)范
分為幾個(gè)組成部分:
- http method: 請(qǐng)求方法,get、post、put、delete等
- http scheme: http or https
- OpenAPI統(tǒng)一域名: 外部訪問(wèn) OpenAPI 的統(tǒng)一域名
- 資源訪問(wèn)類型: 訪問(wèn)的資源,api、web等
- OpenAPI版本號(hào): OpenAPI 服務(wù)自身的版本號(hào)
- 內(nèi)部服務(wù)的名稱: OpenAPI 對(duì)接的內(nèi)部服務(wù)名稱(標(biāo)識(shí))
- 內(nèi)部服務(wù)的path: 對(duì)接內(nèi)部服務(wù)API的 subpath
代碼實(shí)現(xiàn)
泛化的 controller 層實(shí)現(xiàn) 以Get、Post請(qǐng)求為例:
@RestController @RequestMapping({"/v1"}) @Slf4j @ServicePath public class OpenApiController { @Autowired private OpenApiService openApiService; @ApiOperation("OpenAPI POST接收") @PostMapping(path = {"/**"}) public ResponseEntity<ReturnBase> filterPost(@Validated @RequestBody(required = false) Map<String, Object> reqMap) { return Optional.ofNullable(openApiService.filter(reqMap, null)) .map(ret -> new ResponseEntity<>(ret, HttpStatus.OK)) .orElseThrow(() -> new MMException("error.openapi.filter", ReturnEnum.C_GENERAL_BUSINESS_ERROR.getMsgCode())); } @ApiOperation("OpenAPI GET接收") @GetMapping(path = {"/**"}) public ResponseEntity<ReturnBase> filterGet(@RequestParam(required = false) MultiValueMap<String, String> params) { return Optional.ofNullable(openApiService.filter(null, params)) .map(ret -> new ResponseEntity<>(ret, HttpStatus.OK)) .orElseThrow(() -> new MMException("error.openapi.filter", ReturnEnum.C_GENERAL_BUSINESS_ERROR.getMsgCode())); } }
service層和service實(shí)現(xiàn)層
public interface OpenApiService { ReturnBase filter(Map<String, Object> reqBodyMap, MultiValueMap<String, String> reqGetParamsMap); } @Service @Slf4j public class OpenApiServiceImpl implements OpenApiService { @Override public ReturnBase filter(Map<String, Object> reqBodyMap, MultiValueMap<String, String> reqGetParamsMap) { String svcName = (String) OpenapiThreadlocal.getServiceParams().get(BizConstant.SVC_NAME); String svcPathPublic = (String) OpenapiThreadlocal.getServiceParams().get(BizConstant.SVC_PATH_PUBLIC); return doBizHandler(svcName, svcPathPublic); } }
AOP切面和注解
@Aspect @Component @Slf4j @Order(1) public class ServicePathAspect { static final Pattern PATTERN = Pattern.compile("/v\\d+(/.+)"); @Resource private CustomProperty customProperty; @Pointcut("@within(org.xxx.annotation.ServicePath)") public void servicePathOnClass() {} @Pointcut("@annotation(org.xxx.annotation.ServicePath)") public void servicePathOnMethod() { } @Before(value = "servicePathOnClass() || servicePathOnMethod()") public void before() { HttpServletRequest hsr = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String reqUri = hsr.getRequestURI(); String httpMethod = hsr.getMethod(); if (StrUtil.isEmpty(reqUri)) { log.error("request uri is empty"); throw new MMException(ReturnEnum.A_PARAM_VALIDATION_ERROR); } Matcher matcher = PATTERN.matcher(reqUri); String servicePath = ""; while (matcher.find()) { servicePath = matcher.group(1); } if (StrUtil.isEmpty(servicePath)) { log.error("can't parse service path from {}", reqUri); throw new MMException(ReturnEnum.A_PARAM_VALIDATION_ERROR); } String[] split = servicePath.split("\\/"); if (split.length < 3) { log.error("api format error: {}", servicePath); throw new MMException(ReturnEnum.A_PARAM_VALIDATION_ERROR); } String serviceName = split[1]; servicePath = servicePath.substring(serviceName.length() + 1); Map<String, Object> map = Maps.newHashMap(); map.put(BizConstant.SVC_NAME, serviceName); map.put(BizConstant.SVC_PATH_PUBLIC, servicePath); map.put(BizConstant.START_TIMESTAMP, start); OpenapiThreadlocal.addServiceParams(map); } } @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServicePath { }
Threadlocal工具
public class OpenapiThreadlocal { private final static ThreadLocal<Map<String,Object>> SERVICE_PARAMS_HOLDER = new ThreadLocal<>(); public static void addServiceParams(Map<String, Object> svcParamsMap) { SERVICE_PARAMS_HOLDER.set(svcParamsMap); } public static Map<String, Object> getServiceParams() { return SERVICE_PARAMS_HOLDER.get(); } public static void removeServiceParams() { SERVICE_PARAMS_HOLDER.remove(); } }
至此,服務(wù)A可以動(dòng)態(tài)的接入服務(wù)B/C的接口,對(duì)外暴露,并無(wú)需重啟服務(wù)A,即支持API接口的動(dòng)態(tài)添加的業(yè)務(wù)需求實(shí)現(xiàn)完畢。
以上就是詳解OpenAPI開發(fā)如何動(dòng)態(tài)的添加接口實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于OpenAPI動(dòng)態(tài)添加接口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java超詳細(xì)教你寫一個(gè)銀行存款系統(tǒng)案例
這篇文章主要介紹了怎么用Java來(lái)寫一個(gè)銀行的存款系統(tǒng),銀行存款主要有賬號(hào)和存款金額兩個(gè)屬性,感興趣的朋友跟隨文章往下看看吧2022-03-03java web項(xiàng)目實(shí)現(xiàn)文件下載實(shí)例代碼
現(xiàn)在項(xiàng)目里面有個(gè)需求,需要把系統(tǒng)產(chǎn)生的日志文件給下載到本地 先獲取所有的日志文件列表,顯示到界面,選擇一個(gè)日志文件,把文件名傳到后臺(tái)2013-09-09一天時(shí)間用Java寫了個(gè)飛機(jī)大戰(zhàn)游戲,朋友直呼高手
前兩天我發(fā)現(xiàn)論壇有兩篇飛機(jī)大戰(zhàn)的文章異?;鸨?但都是python寫的,竟然不是我大Java,說(shuō)實(shí)話作為老java選手,我心里是有那么一些失落的,難道我大java打飛機(jī)不行?今天特地整理了這篇文章,需要的朋友可以參考下2021-05-05Java Swing窗體關(guān)閉事件的調(diào)用關(guān)系
這篇文章主要為大家詳細(xì)介紹了Java Swing窗體關(guān)閉事件的調(diào)用關(guān)系,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07spring+springmvc+mybatis 開發(fā)JAVA單體應(yīng)用
這篇文章主要介紹了spring+springmvc+mybatis 開發(fā)JAVA單體應(yīng)用的相關(guān)知識(shí),本文通過(guò)圖文實(shí)例代碼的形式給大家介紹的非常詳細(xì) ,需要的朋友可以參考下2018-11-11Java界面編程實(shí)現(xiàn)界面跳轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Java界面編程實(shí)現(xiàn)界面跳轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06Java中的關(guān)鍵字synchronized 詳解
這篇文章主要介紹了Java中的關(guān)鍵字synchronized,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Java util.List如何實(shí)現(xiàn)列表分段處理
這篇文章主要介紹了Java util.List如何實(shí)現(xiàn)列表分段處理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09在非spring環(huán)境中調(diào)用service中的方法
非Spring環(huán)境指的是不使用Spring框架來(lái)管理和配置應(yīng)用程序的運(yùn)行時(shí)環(huán)境,本文將給大家介紹如何在非spring環(huán)境中調(diào)用service中的方法,文中有詳細(xì)實(shí)現(xiàn)步驟,需要的朋友可以參考下2024-03-03