knife4j 接口文档神器改造实现内部接口调试
背景
当我们项目是单体项目时,集成swagger-ui也非常简单一个api-docs暴露openapi接口json数据就行了。
swagger-ui提供了页面在线调试的功能,单体应用是可以直接测试的。
当我们使用SpringCloudGateway集成swagger-ui或者Knife4j-ui时,这个时间我们可能调用不到具体的接口,调试模式则不能使用。因为网关重写了接口地址。而api-docs返回的地址还是单个服务的地址。
文档问题描述
通过网关提供的接口地址调用会出现404的情况。
接口 | 服务地址 | 网关的地址 |
---|---|---|
测试接口 | /v1/test | /api/serviceA/v1/test |
Swagger文档输出的地址为/v1/test, 通过网关的swagger调用则出现404,因为网关无法路由到对应的地址。
其实这个问题也非常简单,地址是内部服务的地址,网关只是不清楚这个地址应该路由到哪个服务。那如果说我们将服务的名称告诉网关,通过网关filter实现转向对应的服务,这样所有的接口都可以实现在接口文档页面上调试了。
### 基于Knife-ui改造
现在的问题就回到了knife4j-ui他的调试请求里面带不带服务名称了。
首先分析knife-ui调试请求信息
他不包含服务名称的,现在只有一个办法可以增加服务名称那就是拉取他的源代码进行改造。
knife4j的代码是通过vue开发的,要改造还算简单。还要解决一个问题服务名称从哪里来,下图的url地址中就包含了服务地址
拉代码:https://gitee.com/xiaoym/knife4j.git
knife4j的分支版本管理感觉有点不清晰,和我自己的开源项目一样,哈哈哈。
找到vue代码,修改服务地址为网关地址 vue.config.js
devServer: {
watchOptions: {
ignored: /node_modules/
},
proxy: {
"/": {
target: 'http://网关地址',
//target: 'http://localhost:17812',
/* target: 'http://knife4j.xiaominfo.com/', */
ws: true,
changeOrigin: true
}
}
npm install
npm run serve
跑起来看看,然后开始改造
简简单单增加几行垃圾代码,就实现了debug调试追加服务名称(这里是路由id,通过路由id找到对应的服务名称)
来看看请求效果,请求中携带了knife4j-router-id
头
到此前端部分的改造就完毕。打包npm run build ,然后dist文件夹内的文件直接放在后端网关代码或者放在nginx上。
### 网关新增handler 实现拦截带有knife4j-router-id
的请求
这个是自定义路由增加请求头的拦截到对应的handler
Hannder代码(基于reactive响应式编程,走了好多弯路,最后还是成功了)如下:
package com.xxx.tech.gateway.swagger.api;
/**
* 为了暴露所有接口作为调试使用
* 通过包含knife knife4j-router-id请求头识别
* @Author marker
* @date 2023-04-13 SwaggerRouteIdApiDocsHandler
*/
@Profile({"dev","test"})
@Slf4j
@Component
@AllArgsConstructor
@Order(Integer.MIN_VALUE)
public class SwaggerRouteIdApiDocsHandler implements HandlerFunction<ServerResponse> {
private SwaggerResourcesProvider swaggerResources;
private final RouteDefinitionRepository routeDefinitionRepository;
@LoadBalanced
@Autowired(required = false)
private RestTemplate restTemplate;
/**
* Handle the given request.
* @param request the request to handler
* @return the response
*/
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String uri = request.uri().getPath() + "?" + request.uri().getRawQuery();
String gatewayRouterId = request.headers().firstHeader("knife4j-router-id");
RouteDefinition routeDefinition = routeDefinitionRepository.getRouteDefinitions()
.filter(item->item.getId().equals(gatewayRouterId)).blockFirst();
String url = String.format("http://%s%s", routeDefinition.getUri().getHost(), uri);
if (request.method().equals(HttpMethod.GET)) {
HttpEntity<String> httpEntity = new HttpEntity<>(null, request.headers().asHttpHeaders());
ResponseEntity<String> result = restTemplate.exchange(url, request.method(), httpEntity, String.class);
return ServerResponse.status(result.getStatusCode())
.headers(h->result.getHeaders())
.contentType(Objects.requireNonNull(result.getHeaders().getContentType()))
.body(BodyInserters.fromValue(Objects.requireNonNull(result.getBody())));
} else {
Mono<String> monoBody = request.bodyToMono(String.class);
Mono<ServerResponse> mono = monoBody.flatMap(body -> {
HttpEntity<String> httpEntity = new HttpEntity<>(body, request.headers().asHttpHeaders());
ResponseEntity<String> result = restTemplate.exchange(url, Objects.requireNonNull(request.method()), httpEntity, String.class);
return ServerResponse.status(result.getStatusCode())
.headers(h->result.getHeaders())
.contentType(Objects.requireNonNull(result.getHeaders().getContentType()))
.body(BodyInserters.fromValue(Objects.requireNonNull(result.getBody())));
});
return mono;
}
}
}
结束语
通过网关改造,实现了基于knife4j接口调试的互通性。完美实现了内部接口公开接口的调试调用,我都快开始放弃postman了,但是knife4j存储请求参数的方式还是很容易丢失。