关于knife4j的文档自动注册功能的解决 1 存在问题 2

这是我参与11月更文挑战的第20天,活动详情查看:11月更文挑战

1 存在问题

上一篇,关于knife4j整合微服务聚合文档, 在日常项目中,使用简单,方便, 可是存在一个问题, 就是需要在文档服务中,手动的配置其他服务的路由地址,而且, 每次新增一个服务,都需要配置,使用起来不是很灵活便捷,那有没有解决方案, 文档服务,主动去nacos中获取服务,自动注册到文档服务的呢?, 答案是肯定的, 对于这一块,knife4j工具提供了相关的入口.

2 解决方案

对于上次的knife4j整合微服务聚合文档文章做增强功能, 业务服务可复用之前的, 本次只对文档服务改造即可.

文档服务案列

Nacos服务类

主要处理nacos中服务实例,包括鉴权,nacos配置等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码public class DocNacosService extends NacosService {

Logger logger = LoggerFactory.getLogger(NacosService.class);
/**
* Nacos获取实例列表OpenAPI接口,详情参考:https://nacos.io/zh-cn/docs/open-api.html
*/
private static final String NACOS_INSTANCE_LIST_API = "/v1/ns/instance/list";
/**
* 服务名称
*/
private final String serviceUrl;
/**
* Nacos注册中心鉴权,参考issue:https://gitee.com/xiaoym/knife4j/issues/I28IF9 since 2.0.9
*/
private final String accessToken;
/**
* Nacos配置
*/
private final NacosRoute nacosRoute;

public DocNacosService(String serviceUrl, String accessToken,
NacosRoute nacosRoute) {
super(serviceUrl, accessToken, nacosRoute);
this.serviceUrl = serviceUrl;
this.accessToken = accessToken;
this.nacosRoute = nacosRoute;
}


@Override
public Optional<NacosInstance> call() throws Exception {
List<String> params = new ArrayList<>();
params.add("serviceName=" + nacosRoute.getServiceName());
//默认聚合时只返回健康实例
params.add("healthyOnly=true");
if (StrUtil.isNotBlank(nacosRoute.getGroupName())) {
params.add("groupName=" + nacosRoute.getGroupName());
}
if (StrUtil.isNotBlank(nacosRoute.getNamespaceId())) {
params.add("namespaceId=" + nacosRoute.getNamespaceId());
}
if (StrUtil.isNotBlank(nacosRoute.getClusters())) {
params.add("clusters=" + nacosRoute.getClusters());
}
//Nacos鉴权 since2.0.9
if (StrUtil.isNotBlank(this.accessToken)) {
params.add("accessToken=" + this.accessToken);
}
String parameter = CollectionUtil.join(params, "&");
String api = serviceUrl + NACOS_INSTANCE_LIST_API + "?" + parameter;
if (logger.isDebugEnabled()) {
logger.debug("Nacos API:{}", api);
}
HttpGet get = new HttpGet(api);
CloseableHttpResponse response = getClient().execute(get);
if (response != null) {
int statusCode = response.getStatusLine().getStatusCode();
if (logger.isDebugEnabled()) {
logger.debug("Nacos Response Status:{}", statusCode);
}
if (statusCode == HttpStatus.SC_OK) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
if (StrUtil.isNotBlank(content)) {
if (logger.isDebugEnabled()) {
logger.debug("Response Content:{}", content);
}
JsonElement jsonElement = JsonParser.parseString(content);
if (jsonElement != null && jsonElement.isJsonObject()) {
JsonElement instances = jsonElement.getAsJsonObject().get("hosts");
if (instances != null && instances.isJsonArray()) {
Type type = new TypeToken<List<NacosInstance>>() {
}.getType();
List<NacosInstance> nacosInstances = new Gson()
.fromJson(instances, type);
if (CollectionUtil.isNotEmpty(nacosInstances)) {
NacosInstance nacosInstance = nacosInstances.stream().findAny()
.get();
nacosInstance.setServiceName(nacosRoute.getServiceName());
return Optional.of(nacosInstance);
}
}
}
}
} else {
get.abort();
}
}
return Optional.empty();
}

}

Nacos服务资源库类

主要是初始化nacos资源库, 从本地文件获取,从nacos注册中心获取服务加载到本地资源库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
java复制代码public class DocNacosRepository extends NacosRepository {

@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private Environment environment;

private volatile boolean stop = false;
private Thread thread;
Logger logger = LoggerFactory.getLogger(NacosRepository.class);

private NacosSetting nacosSetting;

final ThreadPoolExecutor threadPoolExecutor = ThreadUtil.newExecutor(5, 5);

private Map<String, NacosInstance> nacosInstanceMap = new HashMap<>();


public DocNacosRepository(
NacosSetting nacosSetting) {
super(nacosSetting);
this.nacosSetting = nacosSetting;

if (nacosSetting != null && CollectionUtil.isNotEmpty(nacosSetting.getRoutes())) {
initNacos(nacosSetting);
applyRoutes(nacosSetting);
}

}

/**
* 初始化 nacos配置属性
*/
private void applyRoutes(NacosSetting nacosSetting) {
if (CollectionUtil.isNotEmpty(nacosInstanceMap)) {
nacosSetting.getRoutes().forEach(nacosRoute -> {
if (nacosRoute.getRouteAuth() == null || !nacosRoute.getRouteAuth().isEnable()) {
nacosRoute.setRouteAuth(nacosSetting.getRouteAuth());
}
this.routeMap.put(nacosRoute.pkId(), new SwaggerRoute(nacosRoute,
nacosInstanceMap.get(nacosRoute.getServiceName())));
});
nacosSetting.getRoutes().forEach(nacosRoute -> this.routeMap.put(nacosRoute.pkId(),
new SwaggerRoute(nacosRoute,
nacosInstanceMap.get(nacosRoute.getServiceName()))));
}
}

@Override
public void initNacos(NacosSetting nacosSetting) {
List<Future<Optional<NacosInstance>>> optionalList = new ArrayList<>();
nacosSetting.initAccessToken();
nacosSetting.getRoutes().forEach(nacosRoute -> optionalList.add(threadPoolExecutor
.submit(new NacosService(nacosSetting.getServiceUrl(), nacosSetting.getSecret(),
nacosRoute))));
optionalList.stream().forEach(optionalFuture -> {
try {
Optional<NacosInstance> nacosInstanceOptional = optionalFuture.get();
if (nacosInstanceOptional.isPresent()) {
nacosInstanceMap.put(nacosInstanceOptional.get().getServiceName(),
nacosInstanceOptional.get());
}
} catch (Exception e) {
logger.error("nacos get error:" + e.getMessage(), e);
}
});
}

@Override
public NacosSetting getNacosSetting() {
return nacosSetting;
}

@Override
public BasicAuth getAuth(String header) {
BasicAuth basicAuth = null;
if (nacosSetting != null && CollectionUtil.isNotEmpty(nacosSetting.getRoutes())) {
if (nacosSetting.getRouteAuth() != null && nacosSetting.getRouteAuth().isEnable()) {
basicAuth = nacosSetting.getRouteAuth();
//判断route服务中是否再单独配置
BasicAuth routeBasicAuth = getAuthByRoute(header, nacosSetting.getRoutes());
if (routeBasicAuth != null) {
basicAuth = routeBasicAuth;
}
} else {
basicAuth = getAuthByRoute(header, nacosSetting.getRoutes());
}
}
return basicAuth;
}

@Override
public void start() {
logger.info("start Nacos hearbeat Holder thread.");
thread = new Thread(() -> {
while (!stop) {
try {
ThreadUtil.sleep(HEART_BEAT_DURATION);
logger.debug("nacos hearbeat start working...");
this.nacosSetting.initAccessToken();

List<NacosRoute> routes = this.nacosSetting.getRoutes();
// yaml配置文件中没有路由,则自动从注册中心去获取在线服务,转为route
if (CollectionUtil.isEmpty(routes)) {
routes = getServiceToRouteList();
}

//校验该服务是否在线
routes.forEach(nacosRoute -> {
try {
NacosService nacosService = new DocNacosService(
this.nacosSetting.getServiceUrl(),
this.nacosSetting.getSecret(), nacosRoute);
//单线程check即可
Optional<NacosInstance> nacosInstanceOptional = nacosService.call();
if (nacosInstanceOptional.isPresent()) {
this.routeMap.put(nacosRoute.pkId(),
new SwaggerRoute(nacosRoute, nacosInstanceOptional.get()));
} else {
//当前服务下线,剔除
this.routeMap.remove(nacosRoute.pkId());
}
} catch (Exception e) {
//发生异常,剔除服务
this.routeMap.remove(nacosRoute.pkId());
logger.debug(e.getMessage(), e);
}
});
} catch (Exception e) {
logger.debug(e.getMessage(), e);
}

}
});
thread.setDaemon(true);
thread.start();
}

/**
* 从nacos中获取服务列表
* @return
*/
private List<NacosRoute> getServiceToRouteList() {
List<NacosRoute> nacosRouteList = Lists.newArrayList();
List<String> services = discoveryClient.getServices();
if (CollectionUtil.isEmpty(services)){
return nacosRouteList;
}
for (String service : services) {
NacosRoute nacosRoute = new NacosRoute();
nacosRoute.setGroupName(environment.getProperty("spring.cloud.nacos.discovery.group"));
nacosRoute.setNamespaceId(environment.getProperty("spring.cloud.nacos.discovery.namespace"));
nacosRoute.setClusters(environment.getProperty("spring.cloud.nacos.discovery.cluster-name"));
nacosRoute.setName(service);
nacosRoute.setServiceName(service);
nacosRoute.setServicePath(service);
nacosRoute.setLocation("/v2/api-docs");
nacosRouteList.add(nacosRoute);
}

return nacosRouteList;
}

@Override
public void close() {
logger.info("stop Nacos heartbeat Holder thread.");
this.stop = true;
if (thread != null) {
ThreadUtil.interrupt(thread, true);
}
}

}

路由分发类

主要处理请求的路由转发等, 对于返回的结果, 可以根据业务的不同,返回不同状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
java复制代码public class DocRouteDispatcher extends RouteDispatcher {

/**
* 请求头
*/
public static final String ROUTE_PROXY_HEADER_NAME = "knfie4j-gateway-request";
public static final String ROUTE_PROXY_HEADER_BASIC_NAME = "knife4j-gateway-basic-request";
public static final String OPENAPI_GROUP_ENDPOINT = "/swagger-resources";
public static final String OPENAPI_GROUP_INSTANCE_ENDPOINT = "/swagger-instance";
public static final String ROUTE_BASE_PATH = "/";

Logger logger = LoggerFactory.getLogger(RouteDispatcher.class);
/**
* 当前项目的contextPath
*/
private String rootPath;

private RouteRepository routeRepository;

private RouteExecutor routeExecutor;

private RouteCache<String, SwaggerRoute> routeCache;

private Set<String> ignoreHeaders = new HashSet<>();

public DocRouteDispatcher(RouteRepository routeRepository,
RouteCache<String, SwaggerRoute> routeRouteCache,
ExecutorEnum executorEnum,
String rootPath) {
super(routeRepository, routeRouteCache, executorEnum, rootPath);

this.routeRepository = routeRepository;
this.routeCache = routeRouteCache;
this.rootPath = rootPath;
initExecutor(executorEnum);
ignoreHeaders.addAll(Arrays.asList(new String[]{
"host", "content-length", ROUTE_PROXY_HEADER_NAME, ROUTE_PROXY_HEADER_BASIC_NAME, "Request-Origion"
}));
}

private void initExecutor(ExecutorEnum executorEnum) {
if (executorEnum == null) {
throw new IllegalArgumentException("ExecutorEnum can not be empty");
}
switch (executorEnum) {
case APACHE:
this.routeExecutor = new ApacheClientExecutor();
break;
case OKHTTP:
this.routeExecutor = new OkHttpClientExecutor();
break;
default:
throw new UnsupportedOperationException("UnSupported ExecutorType:" + executorEnum.name());
}
}


@Override
public boolean checkRoute(String header) {
if (StrUtil.isNotBlank(header)) {
SwaggerRoute swaggerRoute = routeRepository.getRoute(header);
if (swaggerRoute != null) {
return StrUtil.isNotBlank(swaggerRoute.getUri());
}
}
return false;
}

@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
try {
RouteRequestContext routeContext = new RouteRequestContext();
this.buildContext(routeContext, request);
RouteResponse routeResponse = routeExecutor.executor(routeContext);
writeResponseStatus(routeResponse, response);
// todo
// 请求/v2/api-docs 响应状态设为200 不抛出其他状态
if(response.getStatus()!=200 && request.getRequestURI().equals("/v2/api-docs")){
response.setStatus(200);
}
writeResponseHeader(routeResponse, response);
writeBody(routeResponse, response);
} catch (Exception e) {
logger.error("has Error:{}", e.getMessage());
logger.error(e.getMessage(), e);
//write Default
writeDefault(request, response, e.getMessage());
}
}

@Override
protected void writeDefault(HttpServletRequest request, HttpServletResponse response, String errMsg) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
try {
PrintWriter printWriter = response.getWriter();
Map<String, String> map = new HashMap<>();
map.put("message", errMsg);
// todo
// 请求/v2/api-docs 响应状态设为200 不抛出其他状态
map.put("code", "200");
map.put("path", request.getRequestURI());
new JSONObject(map).write(printWriter);
printWriter.close();
} catch (IOException e) {
//ignore
}
}

/**
* Write 响应状态码
*
* @param routeResponse routeResponse
* @param response response
*/
@Override
protected void writeResponseStatus(RouteResponse routeResponse, HttpServletResponse response) {
if (routeResponse != null) {
response.setStatus(routeResponse.getStatusCode());
}
}

/**
* Write响应头
*
* @param routeResponse route响应对象
* @param response 响应response
*/
@Override
protected void writeResponseHeader(RouteResponse routeResponse, HttpServletResponse response) {
if (routeResponse != null) {
if (CollectionUtil.isNotEmpty(routeResponse.getHeaders())) {
for (HeaderWrapper header : routeResponse.getHeaders()) {
if (!StrUtil.equalsIgnoreCase(header.getName(), "Transfer-Encoding")) {
response.addHeader(header.getName(), header.getValue());
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("响应类型:{},响应编码:{}", routeResponse.getContentType(), routeResponse.getCharsetEncoding());
}
response.setContentType(routeResponse.getContentType());
if (routeResponse.getContentLength() > 0) {
response.setContentLengthLong(routeResponse.getContentLength());
}
response.setCharacterEncoding(routeResponse.getCharsetEncoding().displayName());
}
}

/**
* 响应内容
*
* @param routeResponse route响应对象
* @param response 响应对象
*/
@Override
protected void writeBody(RouteResponse routeResponse, HttpServletResponse response) throws IOException {
if (routeResponse != null) {
if (routeResponse.success()) {
InputStream inputStream = routeResponse.getBody();
if (inputStream != null) {
int read = -1;
byte[] bytes = new byte[1024 * 1024];
ServletOutputStream outputStream = response.getOutputStream();
while ((read = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
}
IoUtil.close(inputStream);
IoUtil.close(outputStream);
}
} else {
String text = routeResponse.text();
if (StrUtil.isNotBlank(text)) {
PrintWriter printWriter = response.getWriter();
printWriter.write(text);
printWriter.close();
}
}

}
}

/**
* 构建路由的请求上下文
*
* @param routeRequestContext 请求上下文
* @param request 请求对象
*/
@Override
protected void buildContext(RouteRequestContext routeRequestContext, HttpServletRequest request) throws IOException {
// 当前请求是否basic请求
String basicHeader = request.getHeader(ROUTE_PROXY_HEADER_BASIC_NAME);
if (StrUtil.isNotBlank(basicHeader)) {
BasicAuth basicAuth = routeRepository.getAuth(basicHeader);
if (basicAuth != null) {
//增加Basic请求头
routeRequestContext.addHeader("Authorization", RouteUtils.authorize(basicAuth.getUsername(),
basicAuth.getPassword()));
}
}
SwaggerRoute swaggerRoute = getRoute(request.getHeader(ROUTE_PROXY_HEADER_NAME));
//String uri="http://knife4j.xiaominfo.com";
String uri = swaggerRoute.getUri();
if (StrUtil.isBlank(uri)) {
throw new RuntimeException("Uri is Empty");
}
String host = URI.create(uri).getHost();
String fromUri = request.getRequestURI();
StringBuilder requestUrlBuilder = new StringBuilder();
requestUrlBuilder.append(uri);
// 判断当前聚合项目的contextPath
if (StrUtil.isNotBlank(this.rootPath) && !StrUtil.equals(this.rootPath, ROUTE_BASE_PATH)) {
fromUri = fromUri.replaceFirst(this.rootPath, "");
}
// 判断servicePath
if (StrUtil.isNotBlank(swaggerRoute.getServicePath()) && !StrUtil.equals(swaggerRoute.getServicePath(),
ROUTE_BASE_PATH)) {
if (StrUtil.startWith(fromUri, swaggerRoute.getServicePath())) {
//实际在请求时,剔除servicePath,否则会造成404
fromUri = fromUri.replaceFirst(swaggerRoute.getServicePath(), "");
}
}
requestUrlBuilder.append(fromUri);
//String requestUrl=uri+fromUri;
String requestUrl = requestUrlBuilder.toString();
if (logger.isDebugEnabled()) {
logger.debug("目标请求Url:{},请求类型:{},Host:{}", requestUrl, request.getMethod(), host);
}
routeRequestContext.setOriginalUri(fromUri);
routeRequestContext.setUrl(requestUrl);
routeRequestContext.setMethod(request.getMethod());
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
if (!ignoreHeaders.contains(key.toLowerCase())) {
routeRequestContext.addHeader(key, value);
}
}
routeRequestContext.addHeader("Host", host);
Enumeration<String> params = request.getParameterNames();
while (params.hasMoreElements()) {
String name = params.nextElement();
String value = request.getParameter(name);
//logger.info("param-name:{},value:{}",name,value);
routeRequestContext.addParam(name, value);
}
// 增加文件,sinc 2.0.9
try {
Collection<Part> parts=request.getParts();
if (CollectionUtil.isNotEmpty(parts)){
parts.forEach(part -> routeRequestContext.addPart(part));
}
} catch (ServletException e) {
//ignore
logger.warn("get part error,message:"+e.getMessage());
}
routeRequestContext.setRequestContent(request.getInputStream());
}

@Override
public SwaggerRoute getRoute(String header) {
//去除缓存机制,由于Eureka以及Nacos设立了心跳检测机制,服务在多节点部署时,节点ip可能存在变化,导致调试最终转发给已经下线的服务
//since 2.0.9
SwaggerRoute swaggerRoute = routeRepository.getRoute(header);
return swaggerRoute;
}

@Override
public List<SwaggerRoute> getRoutes() {
return routeRepository.getRoutes();
}

}

Nacos配置类

自动配置, 将nacos资源库,路由分发等加载到容器中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
java复制代码@Configuration
@AutoConfigureAfter(Knife4jAggregationAutoConfiguration.class)
@ConditionalOnProperty(name = "knife4j.enableAggregation", havingValue = "true")
public class DocNacosConfiguration {

final Environment environment;

@Autowired
public DocNacosConfiguration(Environment environment) {
this.environment = environment;
}

@Primary
@Bean(initMethod = "start", destroyMethod = "close")
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
@RefreshScope
public NacosRepository customNacosRepository(
@Autowired Knife4jAggregationProperties customKnife4jAggregationProperties) {
return new DocNacosRepository(customKnife4jAggregationProperties.getNacos());
}

/**
* 配合nacos配置中心动态刷新
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "knife4j")
@RefreshScope
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
public Knife4jAggregationProperties customKnife4jAggregationProperties() {
return new Knife4jAggregationProperties();
}

@Bean
@Primary
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
public RouteDispatcher customRouteDispatcher(@Autowired RouteRepository routeRepository,
@Autowired RouteCache<String, SwaggerRoute> routeCache) {
//获取当前项目的contextPath
String contextPath = environment.getProperty("server.servlet.context-path");
if (StrUtil.isBlank(contextPath)) {
contextPath = "/";
}
if (StrUtil.isNotBlank(contextPath) && !StrUtil
.equals(contextPath, RouteDispatcher.ROUTE_BASE_PATH)) {
//判断是否/开头
if (!StrUtil.startWith(contextPath, RouteDispatcher.ROUTE_BASE_PATH)) {
contextPath = RouteDispatcher.ROUTE_BASE_PATH + contextPath;
}
}
return new DocRouteDispatcher(routeRepository, routeCache, ExecutorEnum.APACHE,
contextPath);
}

}

服务启动类

1
2
3
4
5
6
7
8
9
java复制代码@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
log.info("启动成功");
}
}

配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
application:
name: knife4j-doc # 服务名称

knife4j:
# 开启聚合
enableAggregation: true
nacos:
enable: true
serviceUrl: http://localhost:8848/nacos

配置文件spring.factories

1
2
factories复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.cf.config.DocNacosConfiguration

测试

测试步骤

1 启动nacos服务

2 启动两个Demo业务服务

3 启动文档服务

4 本地访问 http://localhost:8000/doc.html

测试结果

1 测试结果, 发现业务服务的在线文档和文档服务完美聚合.

2 通过测试, 下线其中一个服务, 文档服务中,也会剔除掉相应的在文档.

3 将在线文档转为离线文档下载到本地, 功能正常

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%