springcloud-gateway集成knife4j(swagger2)
环境信息
- spring-boot:2.6.3
- spring-cloud-alibaba:2021.0.1.0
- knife4j-openapi2-spring-boot-starter:4.0.0
准备工作
各微服务&网关引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>
微服务集成knife4j
第一步:编写Knife4jApiInfoProperties
import com.ideaaedi.springcloud.jd.commonspring.config.Knife4jConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* api 基础信息配置。更多配置信息项见{@link Knife4jConfig}
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
* src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 2021.0.1.D
*/
@Data
@Component
public class Knife4jApiInfoProperties {
/**
* 要扫描api的base包
*/
@Value("${api-info.base-package:com}")
private String basePackage;
/**
* 是否启用登录认证
*/
@Value("${api-info.enable-security:true}")
private boolean enableSecurity;
/**
* 文档标题
*/
@Value("${api-info.title:}")
private String title;
/**
* 文档描述
*/
@Value("${api-info.description:api info}")
private String description;
/**
* 文档版本
*/
@Value("${api-info.version:1.0.0}")
private String version;
/**
* 联系人姓名
*/
@Value("${api-info.contact-name:JustryDeng}")
private String contactName;
/**
* 联系人网址
*/
@Value("${api-info.contact-url:https://gitee.com/JustryDeng/projects}")
private String contactUrl;
/**
* 联系人邮箱
*/
@Value("${api-info.contact-email:13548417409@163.com}")
private String contactEmail;
}
第二步:编写配置类Knife4jConfig
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.ideaaedi.springcloud.jd.commonds.constant.BaseConstant;
import com.ideaaedi.springcloud.jd.commonds.support.EnumDescriptor;
import com.ideaaedi.springcloud.jd.commonspring.config.properties.Knife4jApiInfoProperties;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ModelPropertyBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelPropertyContext;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* knife4j配置类
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
* src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 2021.0.1.D
*/
@Slf4j
@Configuration
public class Knife4jConfig implements WebMvcConfigurer {
/**
* 文档相关资源的链接(需保证这些资源不需要鉴权即可访问)
*/
public static String[] RESOURCE_URLS = new String[]{"/webjars/**", "/swagger**", "/v2/api-docs", "/doc.html"};
@Value("${spring.application.name:}")
private String applicationName;
@Bean
public Docket docket(Knife4jApiInfoProperties knife4jApiInfoProperties) {
String apiBasePackage = knife4jApiInfoProperties.getBasePackage();
Docket docket =
new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo(knife4jApiInfoProperties)).select().apis(RequestHandlerSelectors.basePackage(apiBasePackage)).paths(PathSelectors.any()).build();
if (knife4jApiInfoProperties.isEnableSecurity()) {
docket.securitySchemes(securitySchemes()).securityContexts(securityContexts());
}
return docket;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
private ApiInfo apiInfo(Knife4jApiInfoProperties knife4jApiInfoProperties) {
return new ApiInfoBuilder().title(knife4jApiInfoProperties.getTitle()).description(knife4jApiInfoProperties.getDescription()).termsOfServiceUrl(StringUtils.isBlank(applicationName) ? "" : "/" + applicationName).contact(new Contact(knife4jApiInfoProperties.getContactName(), knife4jApiInfoProperties.getContactUrl(), knife4jApiInfoProperties.getContactEmail())).version(knife4jApiInfoProperties.getVersion()).build();
}
private List<SecurityScheme> securitySchemes() {
// 设置请求头信息
List<SecurityScheme> result = new ArrayList<>();
// 第一个参数name,自定义即可。 在配置securityContexts时,通过此name对应到apiKey即可
// 第二个参数,header name自定义即可。 如:BaseConstant.JWT_TOKEN_KEY=Auth-Token,然后在代码里request.getHeader(BaseConstant.JWT_TOKEN_KEY)取值
ApiKey apiKey = new ApiKey("JustryDengApiKey", BaseConstant.JWT_TOKEN_KEY, "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
List<SecurityReference> securityReferences = defaultAuth();
result.add(
SecurityContext.builder().securityReferences(securityReferences).forPaths(
// 当直接使用swagger文档发送请求时,这些api需要满足securityReferences认证要求
PathSelectors.regex("/*/.*")
.and(
// 当直接使用swagger文档发送请求时,这些api不需要满足securityReferences认证要求. '.*'表示匹配所有
PathSelectors.regex("/sys-login/.*").or(PathSelectors.regex("/tmp.*"))
.negate()
)
).build()
);
return result;
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
// 这里指定使用哪个apiKey进行认证鉴权. 这里指定使用上面名为JustryDengApiKey的apiKey
result.add(new SecurityReference("JustryDengApiKey", authorizationScopes));
return result;
}
/**
* 显示自定义枚举类型注释
* <p>
* <br/> 参考<a
* href="https://blog.gelu.me/2021/Knife4j-Swagger%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B
* /">here</a>
*/
@Component
@SuppressWarnings("unchecked")
public static class Knife4jSwaggerEnumPlugin implements ModelPropertyBuilderPlugin, ParameterBuilderPlugin {
private static final Field parameterDescriptionField;
private static final Field modelPropertyBuilderDescriptionField;
static {
// swagger3用: parameterDescriptionField = ReflectionUtils.findField(RequestParameterBuilder.class, "description");
parameterDescriptionField = ReflectionUtils.findField(ParameterBuilder.class, "description");
Objects.requireNonNull(parameterDescriptionField, "parameterDescriptionField should noe be null.");
ReflectionUtils.makeAccessible(parameterDescriptionField);
// swagger3用: modelPropertyBuilderDescriptionField = ReflectionUtils.findField(PropertySpecificationBuilder.class, "description");
modelPropertyBuilderDescriptionField = ReflectionUtils.findField(ModelPropertyBuilder.class, "description");
Objects.requireNonNull(modelPropertyBuilderDescriptionField, "ModelPropertyBuilder_descriptionField should noe be null.");
ReflectionUtils.makeAccessible(modelPropertyBuilderDescriptionField);
}
/**
* {@link ApiModelProperty}相关
* <p>
* 主要处理枚举对象直接作为方法参数的内部字段的情况. 如:
* <pre>
* @Data
* public class LoginTokenRespVO {
*
* @ApiModelProperty("用户类型")
* private UserTypeEnum userType;
* }
* </pre>
*/
@Override
public void apply(ModelPropertyContext context) {
Optional<BeanPropertyDefinition> optional = context.getBeanPropertyDefinition();
if (!optional.isPresent()) {
return;
}
// 对应被@ApiModelProperty标注的字段
BeanPropertyDefinition beanPropertyDefinition = optional.get();
Class<?> fieldType = beanPropertyDefinition.getField().getRawType();
if (!Enum.class.isAssignableFrom(fieldType)) {
return;
}
Class<Enum<?>> enumType = (Class<Enum<?>>) fieldType;
Enum<?>[] enumConstants = enumType.getEnumConstants();
// swagger3用: PropertySpecificationBuilder modelPropertyBuilder = context.getSpecificationBuilder();
ModelPropertyBuilder modelPropertyBuilder = context.getBuilder();
Object oldValue = ReflectionUtils.getField(modelPropertyBuilderDescriptionField, modelPropertyBuilder);
// 解析枚举
List<String> enumDescList =
Arrays.stream(enumConstants).map(this::obtainEnumDescription).collect(Collectors.toList());
/*
* swagger3用:
* modelPropertyBuilder.description((oldValue == null ? BaseConstant.EMPTY : oldValue) + buildHtmlUnOrderList(enumDescList))
* .type(new ModelSpecificationBuilder().scalarModel(ScalarType.UUID).build());
*/
modelPropertyBuilder.description((oldValue == null ? BaseConstant.EMPTY : oldValue) + buildHtmlUnOrderList(enumDescList))
.type(context.getResolver().resolve(Enum.class));
}
/**
* {@link ApiParam}、{@link io.swagger.v3.oas.annotations.Parameter}相关.
* <p> 主要处理:枚举对象直接作为方法参数的情况. 如:
* <pre>
* @PostMapping("/test1")
* @ApiOperation(value = "测试1")
* public void test1(@ApiParam(value = "用户类型", required = true) UserTypeEnum userTypeEnum)
* </pre>
*/
@Override
public void apply(ParameterContext context) {
Class<?> type = context.resolvedMethodParameter().getParameterType().getErasedType();
// swagger3用: RequestParameterBuilder parameterBuilder = context.requestParameterBuilder();
ParameterBuilder parameterBuilder = context.parameterBuilder();
if (!Enum.class.isAssignableFrom(type)) {
return;
}
Class<Enum<?>> enumType = (Class<Enum<?>>) type;
Enum<?>[] enumConstants = enumType.getEnumConstants();
// 解析枚举
List<String> enumDescList = Arrays.stream(enumConstants).map(this::obtainEnumDescription).collect(Collectors.toList());
Object oldValue = ReflectionUtils.getField(parameterDescriptionField, parameterBuilder);
parameterBuilder.description((oldValue == null ? BaseConstant.EMPTY : oldValue) + buildHtmlUnOrderList(enumDescList));
}
/**
* 此插件是否支持处理该DocumentationType
*/
@Override
public boolean supports(DocumentationType documentationType) {
return true;
}
/**
* 获取枚举描述
*
* @param enumObj 枚举对象
*
* @return 枚举描述
*/
private String obtainEnumDescription(@NonNull Enum<?> enumObj) {
String name = enumObj.name();
/*
* 枚举说明器示例:
*
* public interface EnumDescriptor {
* // 获取枚举项说明
* String obtainDescription();
* }
*/
if (enumObj instanceof EnumDescriptor) {
return name + ":" + ((EnumDescriptor) enumObj).obtainDescription();
}
return name;
}
/**
* 构建无序列表html
*
* @param itemList 列表元素
*
* @return 无序列表html
*/
private String buildHtmlUnOrderList(@Nullable List<String> itemList) {
if (CollectionUtils.isEmpty(itemList)) {
return BaseConstant.EMPTY;
}
StringBuilder sb = new StringBuilder();
sb.append("<ul>");
for (String item : itemList) {
sb.append("<li>");
sb.append(item);
sb.append("</li>");
}
sb.append("</ul>");
return sb.toString();
}
}
}
第三步:放行相关资源 & 保证启动了knife4j
-
放行相关资源
对于管控了权限的微服务,应放行以下资源
// 需要放行的资源已经定义进上面编写的Knife4jConfig中 public static String[] RESOURCE_URLS = new String[]{"/webjars/**", "/swagger**", "/v2/api-docs", "/doc.html"};
-
保证启动了knife4j
# 启动knife4j(注:有时,如果我们不进行此配置,knife4j不会开启) # 其实核心是:保证com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration生效即可。如果knife4j怎么也没启动,请debug此类,确保其被加载 knife4j.enable=true
网关集成knife4j
编写配置类Knife4jGatewayConfig
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
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.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 网关knife4j配置
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 2021.0.1.D
*/
@RestController
public class Knife4jGatewayConfig {
private final SecurityConfiguration securityConfiguration;
private final UiConfiguration uiConfiguration;
private final SwaggerResourceAdapter swaggerResourceAdapter;
public Knife4jGatewayConfig(@Autowired(required = false) SecurityConfiguration securityConfiguration,
@Autowired(required = false) UiConfiguration uiConfiguration,
SwaggerResourceAdapter swaggerResourceAdapter) {
this.securityConfiguration = securityConfiguration;
this.uiConfiguration = uiConfiguration;
this.swaggerResourceAdapter = swaggerResourceAdapter;
}
/**
* 安全配置
*/
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
* ui配置
*/
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
* 资源配置,自动路由到微服务中的各个服务的api-docs信息
*/
@GetMapping("/swagger-resources")
public Mono<ResponseEntity<List<SwaggerResource>>> swaggerResources() {
return Mono.just(new ResponseEntity<>(swaggerResourceAdapter.get(), HttpStatus.OK));
}
/**
* favicon.ico
*/
@GetMapping("/favicon.ico")
public Mono<ResponseEntity<?>> favicon() {
return Mono.just(new ResponseEntity<>(null, HttpStatus.OK));
}
/**
* swagger资源适配器
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 2021.0.1.D
*/
@Slf4j
@Component
public static class SwaggerResourceAdapter implements SwaggerResourcesProvider {
/**
* spring-cloud-gateway是否开启了根据服务发现自动为服务创建router
*/
@Value("${spring.cloud.gateway.discovery.locator.enabled:false}")
private boolean autoCreateRouter;
@Value("${spring.application.name:}")
private String applicationName;
@Resource
private RouteLocator routeLocator;
@Resource
private GatewayProperties gatewayProperties;
/**
* 根据当前所有的微服务路由信息,创建对应的SwaggerResource
*/
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> finalResources;
Set<String> routes = new LinkedHashSet<>(16);
// 获取所有路由的id
routeLocator.getRoutes().subscribe(route -> {
String routeId = route.getId();
routeId = routeId.replace("ReactiveCompositeDiscoveryClient_", "");
routes.add(routeId);
});
// 没有开启自动创建路由,那么走配置文件中配置的路由
if (!autoCreateRouter) {
finalResources = new ArrayList<>(16);
gatewayProperties.getRoutes().stream()
// 过滤出配置文件中定义的路由
.filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
route.getPredicates().stream()
// 过滤出设置有Path Predicate的路由
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
// 根据路径拼接成api-docs路径,生成SwaggerResource
.forEach(predicateDefinition -> finalResources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
// 如果对应的微服务设置了server.servlet.context-path,那么这里应该是{context-path}/v2/api-docs
.replace("**", "v2/api-docs"))));
});
} else {
// 如果对应的微服务设置了server.servlet.context-path,那么这里应该是/{context-path}/v2/api-docs
finalResources = routes.stream().map(routeId -> swaggerResource(routeId, routeId + "/v2/api-docs")).collect(Collectors.toList());
}
List<SwaggerResource> resources = new ArrayList<>(finalResources);
// resources过滤掉网关的SwaggerResource, 我们一般也不会在网关中编写业务controller
if (StringUtils.isNotBlank(applicationName)) {
resources = resources.stream().filter(x -> !applicationName.equalsIgnoreCase(x.getName())).collect(Collectors.toList());
}
// 排序
resources.sort(Comparator.comparing(x -> x.getName().length()));
return resources;
}
/**
* 创建swagger资源
*
* @param name
* swagger资源名(注:一般对应 {路由id})
* @param location
* swagger资源路径(注:一般对应 {路由id}/v2/api-docs)
* @return swager资源
*/
private SwaggerResource swaggerResource(String name, String location) {
// 确保首字符不是/
location = location.startsWith("/") ? location.substring(1) : location;
log.info("name:{},location:{}", name, location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
}
测试验证
启动微服务后,访问{网关}/doc.html
完成验证