之前写的整合文章还有些缺陷,本此全部处理。
参考资料:
官方文档地址:knife4j (xiaominfo.com) (谷歌打不开就用ie)
github项目:microservices-platform-master
地址:https://github.com/Aisii/microservices-platform-master
博客:https://blog.csdn.net/qq_39878940/article/details/123181951
博客:https://www.jianshu.com/p/aef7d953ae70
思路:
- 将swagger模块抽出为一个公共模块,或者是自定义springboot starter模块(本文采用后者),供其他模块快速整合swagger。
- gateway服务配置所有服务swagger文档聚合
实现:
step1:自定义 swagger-springboot-starter
1.1 引入依赖
com.github.xiaoymin
knife4j-spring-boot-starter
3.0.3
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework
spring-web
true
1.2 swagger 属性配置实体类
/**
* swagger2 属性配置
*
* @author zlt Not registered via @EnableConfigurationProperties, marked as Spring component, or scanned via @ConfigurationPropertiesScan
* @date 2018/11/18 9:17
*/
@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {
/**
* 是否开启swagger
**/
private Boolean enabled;
/**
* 标题
**/
private String title = "";
/**
* 描述
**/
private String description = "";
/**
* 版本
**/
private String version = "";
/**
* 许可证
**/
private String license = "";
/**
* 许可证URL
**/
private String licenseUrl = "";
/**
* 服务条款URL
**/
private String termsOfServiceUrl = "";
/**
* 联系人
**/
private Contact contact = new Contact();
/**
* swagger会解析的包路径
**/
private String basePackage = "";
/**
* swagger会解析的url规则
**/
private List basePath = new ArrayList<>();
/**
* 在basePath基础上需要排除的url规则
**/
private List excludePath = new ArrayList<>();
/**
* 分组文档
**/
private Map docket = new LinkedHashMap<>();
/**
* host信息
**/
private String host = "";
/**
* 全局参数配置
**/
private List globalOperationParameters;
@Setter
@Getter
public static class GlobalOperationParameter {
/**
* 参数名
**/
private String name;
/**
* 描述信息
**/
private String description;
/**
* 指定参数类型 ScalarType.STRING
**/
private String modelRef;
/**
* 参数放在哪个地方
* QUERY("query"),
* HEADER("header"),
* PATH("path"),
* COOKIE("cookie"),
* FORM("form"),
* FORMDATA("formData"),
* BODY("body");
**/
private String parameterType;
/**
* 参数是否必须传
**/
private String required;
}
@Data
public static class DocketInfo {
/**
* 标题
**/
private String title = "";
/**
* 描述
**/
private String description = "";
/**
* 版本
**/
private String version = "";
/**
* 许可证
**/
private String license = "";
/**
* 许可证URL
**/
private String licenseUrl = "";
/**
* 服务条款URL
**/
private String termsOfServiceUrl = "";
private Contact contact = new Contact();
/**
* swagger会解析的包路径
**/
private String basePackage = "";
/**
* swagger会解析的url规则
**/
private List basePath = new ArrayList<>();
/**
* 在basePath基础上需要排除的url规则
**/
private List excludePath = new ArrayList<>();
private List globalOperationParameters;
}
@Data
public static class Contact {
/**
* 联系人
*/
private String name = "";
/**
* 联系人url
*/
private String url = "";
/**
* 联系人email
*/
private String email = "";
}
}
1.3 构建文档配置
package com.ly.tulip.commons.swagger.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 类说明: swagger 配置类
*
* @author wqf
* @date 2022/7/26 10:23
*/
@Configuration
@EnableOpenApi //开启 Swagger3,可以不写
@EnableKnife4j //开启 knife4j,可以不写
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerAutoConfiguration implements BeanFactoryAware {
private static final String AUTH_KEY = "Authorization";
private BeanFactory beanFactory;
@Bean
@ConditionalOnMissingBean
public SwaggerProperties swaggerProperties() {
return new SwaggerProperties();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "tulip.swagger.enabled", matchIfMissing = true)
public List createRestApi(SwaggerProperties swaggerProperties) {
ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
List docketList = new LinkedList<>();
// 没有分组
if (swaggerProperties.getDocket().size() == 0) {
final Docket docket = createDocket(swaggerProperties);
configurableBeanFactory.registerSingleton("defaultDocket", docket);
docketList.add(docket);
return docketList;
}
// 分组创建
for (String groupName : swaggerProperties.getDocket().keySet()) {
SwaggerProperties.DocketInfo docketInfo = swaggerProperties.getDocket().get(groupName);
ApiInfo apiInfo = new ApiInfoBuilder()
.title(docketInfo.getTitle().isEmpty() ? swaggerProperties.getTitle() : docketInfo.getTitle())
.description(docketInfo.getDescription().isEmpty() ? swaggerProperties.getDescription() : docketInfo.getDescription())
.version(docketInfo.getVersion().isEmpty() ? swaggerProperties.getVersion() : docketInfo.getVersion())
.license(docketInfo.getLicense().isEmpty() ? swaggerProperties.getLicense() : docketInfo.getLicense())
.licenseUrl(docketInfo.getLicenseUrl().isEmpty() ? swaggerProperties.getLicenseUrl() : docketInfo.getLicenseUrl())
.contact(
new Contact(
docketInfo.getContact().getName().isEmpty() ? swaggerProperties.getContact().getName() : docketInfo.getContact().getName(),
docketInfo.getContact().getUrl().isEmpty() ? swaggerProperties.getContact().getUrl() : docketInfo.getContact().getUrl(),
docketInfo.getContact().getEmail().isEmpty() ? swaggerProperties.getContact().getEmail() : docketInfo.getContact().getEmail()
)
)
.termsOfServiceUrl(docketInfo.getTermsOfServiceUrl().isEmpty() ? swaggerProperties.getTermsOfServiceUrl() : docketInfo.getTermsOfServiceUrl())
.build();
Docket docket = new Docket(DocumentationType.OAS_30)
.host(swaggerProperties.getHost())
.apiInfo(apiInfo)
.globalRequestParameters(assemblyGlobalOperationParameters(swaggerProperties.getGlobalOperationParameters(),
docketInfo.getGlobalOperationParameters()))
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(docketInfo.getBasePackage()))
.paths(buildPredicateSelector(docketInfo))
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
configurableBeanFactory.registerSingleton(groupName, docket);
docketList.add(docket);
}
return docketList;
}
/**
* 创建 Docket对象
*
* @param swaggerProperties swagger配置
* @return Docket
*/
private Docket createDocket(final SwaggerProperties swaggerProperties) {
ApiInfo apiInfo = new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.version(swaggerProperties.getVersion())
.license(swaggerProperties.getLicense())
.licenseUrl(swaggerProperties.getLicenseUrl())
.contact(new Contact(swaggerProperties.getContact().getName(),
swaggerProperties.getContact().getUrl(),
swaggerProperties.getContact().getEmail()))
.termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
.build();
SwaggerProperties.DocketInfo dockerInfo = new SwaggerProperties.DocketInfo();
dockerInfo.setBasePackage(swaggerProperties.getBasePackage());
dockerInfo.setExcludePath(swaggerProperties.getExcludePath());
return new Docket(DocumentationType.OAS_30)
.host(swaggerProperties.getHost())
.apiInfo(apiInfo)
.enable(swaggerProperties.isEnabled())
.globalRequestParameters(buildGlobalOperationParametersFromSwaggerProperties(
swaggerProperties.getGlobalOperationParameters()))
.select()
.apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
.paths(buildPredicateSelector(dockerInfo))
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private List securityContexts() {
List contexts = new ArrayList<>(1);
SecurityContext securityContext = SecurityContext.builder()
.securityReferences(defaultAuth())
.build();
contexts.add(securityContext);
return contexts;
}
private List defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List references = new ArrayList<>(1);
references.add(new SecurityReference(AUTH_KEY, authorizationScopes));
return references;
}
private List securitySchemes() {
List apiKeys = new ArrayList<>(1);
ApiKey apiKey = new ApiKey(AUTH_KEY, AUTH_KEY, "header");
apiKeys.add(apiKey);
return apiKeys;
}
/**
* 生成swagger全局通用参数 如:请求token
*/
private List buildGlobalOperationParametersFromSwaggerProperties(
List globalOperationParameters) {
List parameters = Lists.newArrayList();
if (Objects.isNull(globalOperationParameters)) {
return parameters;
}
for (SwaggerProperties.GlobalOperationParameter globalOperationParameter : globalOperationParameters) {
parameters.add(new RequestParameterBuilder()
//参数名称
.name(globalOperationParameter.getName())
//参数说明
.description(globalOperationParameter.getDescription())
//参数存放位置
.in(globalOperationParameter.getParameterType())
//参数数据类型 暂时写死为string 有其他需求再改
.query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
//参数是否必须
.required(Boolean.parseBoolean(globalOperationParameter.getRequired()))
.build());
}
return parameters;
}
/**
* 局部参数按照name覆盖局部参数
*
* @param globalOperationParameters
* @param docketOperationParameters
* @return
*/
private List assemblyGlobalOperationParameters(
List globalOperationParameters,
List docketOperationParameters) {
if (Objects.isNull(docketOperationParameters) || docketOperationParameters.isEmpty()) {
return buildGlobalOperationParametersFromSwaggerProperties(globalOperationParameters);
}
Set docketNames = docketOperationParameters.stream()
.map(SwaggerProperties.GlobalOperationParameter::getName)
.collect(Collectors.toSet());
List resultOperationParameters = Lists.newArrayList();
if (Objects.nonNull(globalOperationParameters)) {
for (SwaggerProperties.GlobalOperationParameter parameter : globalOperationParameters) {
if (!docketNames.contains(parameter.getName())) {
resultOperationParameters.add(parameter);
}
}
}
resultOperationParameters.addAll(docketOperationParameters);
return buildGlobalOperationParametersFromSwaggerProperties(resultOperationParameters);
}
private java.util.function.Predicate buildPredicateSelector(SwaggerProperties.DocketInfo config) {
// 当没有配置任何path的时候,解析/**
if (config.getBasePath().isEmpty()) {
config.getBasePath().add("/**");
}
List> basePath = new ArrayList<>();
for (String path : config.getBasePath()) {
basePath.add(PathSelectors.ant(path));
}
// exclude-path处理
List> excludePath = new ArrayList<>();
for (String path : config.getExcludePath()) {
excludePath.add(PathSelectors.ant(path));
}
// 当没有配置任何path的时候,解析/.*
if (config.getBasePath().isEmpty() || config.getBasePath() == null) {
return PathSelectors.any().and(excludePath.stream().reduce(each -> true, (a, b) -> a.and(b.negate())));
}
//组装 base-path 和 exclude-path each为false原因是,如果是true,有任何or,都不会走右边
return basePath.stream().reduce(each -> false, Predicate::or)
.and(excludePath.stream().reduce(each -> true, (a, b) -> a.and(b.negate())));
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
1.3 springboot注定装配配置
resources目录下先建文件夹 META-INF
文件夹下创建 spring.factories 文件 ,内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ly.tulip.commons.swagger.config.SwaggerAutoConfiguration
step1 做完,其他服务引入该公共模块即整合了swagger3.0
1.3,1.4两个文件可以根据需求搞一个简单的配置,这个配置网上都有,官方文档也有,我也是参考其他项目的,仅当学习
step2:gateway聚合配置
2.1.引入step1 的模块
2.2. 引入两个核心配置文件
SwaggerProvider
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* 类说明: 聚合系统接口
*
* @author wqf
* @date 2022/7/26 16:47
*/
@Component
@Primary
@AllArgsConstructor
public class SwaggerProvider implements SwaggerResourcesProvider {
// SWAGGER3默认的URL后缀
public static final String SWAGGER3URL = "/v3/api-docs";
public static final String SWAGGER_VERSION = "3.0";
// 网关路由
@Autowired
private RouteLocator routeLocator;
@Autowired
private GatewayProperties gatewayProperties;
// 聚合其他服务接口
@Override
public List get() {
List resourceList = new ArrayList<>();
List routes = new ArrayList<>();
// 获取网关中配置的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> {
String id = routeDefinition.getId();
String location = id + SWAGGER3URL;
resourceList.add(swaggerResource(id, location));
}
));
return resourceList;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(SWAGGER_VERSION);
return swaggerResource;
}
}
SwaggerHandler
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import java.util.Optional;
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration)
.orElse(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration)
.orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@SuppressWarnings("rawtypes")
@GetMapping("")
public Mono swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
2.3 yml文件配置转发路由
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#配置网关路由规则 按服务名进行路由
gateway:
routes:
# 唯一标识 - 系统核心服务
- id: base-server
uri: lb://base-server
#断言,路径相匹配的进行路由,配置服务端的方法路
predicates:
- Path=/base-server/**
filters:
- StripPrefix=0
# 唯一标识 - 系统用户服务
- id: auth-server
uri: lb://auth-server
predicates:
- Path=/auth-server/**
filters:
- StripPrefix=0
以上基本配置完成,但是测试时发现,接口调试时请求地址缺少转发路由地址,解决配置如下
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 类说明: 处理gateway集成knife4j后接口请求没带上服务名称问题
*
* @author wqf
* @date 2022/8/11 10:17
*/
@Slf4j
@Component
public class SwaggerGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path=exchange.getRequest().getPath().toString();
if (!path.endsWith("/v3/api-docs")){
return chain.filter(exchange);
}
String[] pathArray=path.split("/");
String basePath=pathArray[1];
ServerHttpResponse originalResponse = exchange.getResponse();
// 定义新的消息头
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getResponse().getHeaders());
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
if (Objects.equals(getStatusCode(), HttpStatus.OK) && body instanceof Flux) {
Flux extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
List list = new ArrayList();
dataBuffers.forEach(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
});
String s = listToString(list);
int length = s.getBytes().length;
headers.setContentLength(length);
JSONObject jsonObject= JSONUtil.parseObj(s);
jsonObject.set("basePath",basePath);
s=jsonObject.toString();
return bufferFactory().wrap(s.getBytes());
}));
}
return super.writeWith(body);
};
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
//由于修改了请求体的body,导致content-length长度不确定,因此使用分块编码
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
private String listToString(List list){
StringBuilder stringBuilder=new StringBuilder();
for (String s:list){
stringBuilder.append(s);
}
return stringBuilder.toString();
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -2;
}
}
注意点:
- 网关对:/swagger-ui/**; /v3/api-docs 等swagger访问路径记得放行
- 网关配置文件需要配置路由规则,以下配置是按服务名称进行路由转发的,好处是服务端口改了也没事。
以上整合完毕,两个访问地址如下:
swagger 访问地址:
swagger原生版: http://ip:端口号/swagger-ui/index.html
knife4j界面美化版:http://ip:端口号/doc.html#/home
其他问题:
1.开启配置增强,一般生产环境不需要这个文档的访问
2.设置Ui默认显示语言为中文
3.开启请求参数缓存
配置如下:
knife4j:
#生产环境禁止访问文档
production: true
enable: true
setting:
#设置Ui默认显示语言为中文
language: zh-CN
#开启请求参数缓存
enableRequestCache: true
以上基本够用了,有其他的需求再看看官方文档吧。