目录
想尝鲜,坑也多,一起入个坑~
一、依赖
SpringBoot版本:2.7.14
Swagger版本:3.0.0
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
二、集成Swagger Java Config
@Value("${server.port:8080}")
private String port;
@Value("${server.servlet.context-path:}")
private String rootPath;
@Bean
Docket docket(SwaggerProperties properties) {
Docket docket = new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo(properties))
.groupName(properties.getGroupName())
.select()
.apis(scanBasePackages(properties.getBasePackage()))
.paths(PathSelectors.any())
.build()
.globalRequestParameters(globalRequestParameters(properties))
.globalResponses(HttpMethod.POST, responses())
.globalResponses(HttpMethod.GET, responses()).pathMapping("/");
log.info("Swagger3 successfully started: http://{}:{}{}/doc.html", IPUtils.getLocalIP(), port, rootPath);
return docket;
}
@Bean
public ModelPropertyBuilderPlugin modelPropertyBuilderPlugin() {
return new DictPropertyPlugin();
}
/**
* 构建响应状态码
*/
private List<Response> responses() {
List<Response> responses = new LinkedList<>();
responses.add(new ResponseBuilder().code("S").description("响应成功").build());
responses.add(new ResponseBuilder().code("E").description("非'S'即为响应失败").build());
return responses;
}
private ApiInfo apiInfo(SwaggerProperties properties) {
return new ApiInfoBuilder()
.title(properties.getTitle())
.description(properties.getDescription())
.license(properties.getLicense())
.licenseUrl(properties.getLicenseUrl())
.termsOfServiceUrl(properties.getTermsOfServiceUrl())
.contact(new Contact(properties.getContact().getName(), properties.getContact().getUrl(), properties.getContact().getEmail()))
.version(properties.getVersion())
.build();
}
/**
* 自定义请求参数
*
* @return - list
*/
private List<RequestParameter> globalRequestParameters(SwaggerProperties properties) {
List<RequestParameter> params = new ArrayList<>();
properties.getParams().forEach(e -> {
RequestParameter parameter = new RequestParameterBuilder()
.name(e.getName())
.description(e.getDesc())
.required(e.isRequired())
.in(e.getParamType())
.hidden(e.isHidden())
.build();
params.add(parameter);
});
return params;
}
/**
* 多包扫描支持,扫描的包生成{@linkplain Predicate < RequestHandler >}
*
* @param basePackages - 扫描的包
*/
private Predicate<RequestHandler> scanBasePackages(final String... basePackages) {
if (basePackages == null || basePackages.length == 0) {
throw new IllegalArgumentException("basePackages不能为空");
}
Predicate<RequestHandler> predicate = null;
for (int i = basePackages.length - 1; i >= 0; i--) {
String strBasePackage = basePackages[i];
if (StrUtil.isNotBlank(strBasePackage)) {
Predicate<RequestHandler> tempPredicate = RequestHandlerSelectors.basePackage(strBasePackage);
predicate = predicate == null ? tempPredicate : predicate.or(tempPredicate);
}
}
if (predicate == null) {
throw new IllegalArgumentException("basePackage配置不正确");
}
return predicate;
}
/**
* swagger3 自定义展示枚举类型信息
*/
public class DictPropertyPlugin implements ModelPropertyBuilderPlugin {
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public void apply(ModelPropertyContext ctx) {
Optional<BeanPropertyDefinition> opt = ctx.getBeanPropertyDefinition();
opt.ifPresent(bean -> {
Class<?> cls = bean.getRawPrimaryType();
if (IDict.class.isAssignableFrom(cls) && Enum.class.isAssignableFrom(cls)) {
if (cls.getEnumConstants() == null) {
return;
}
try {
Field f = PropertySpecificationBuilder.class.getDeclaredField("description");
f.setAccessible(true);
String prefix = cls.getSimpleName() + "(" + f.get(ctx.getSpecificationBuilder()) + ")【";
StringJoiner join = new StringJoiner(",", prefix, "】");
for (IDict<?, ?> d : (IDict<?, ?>[]) cls.getEnumConstants()) {
join.add(d.getDesc() + "[" + d.getCode() + "]-" + ((Enum<?>) d).name());
}
ctx.getSpecificationBuilder().description(join.toString());
} catch (Exception e) {
log.error("字典值处理失败:{}", cls.getName(), e);
}
}
});
}
@Override
public boolean supports(DocumentationType type) {
return DocumentationType.OAS_30.equals(type);
}
}
properties
package com.muchenx.common.swagger.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import springfox.documentation.service.ParameterType;
import java.util.ArrayList;
import java.util.List;
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
/**
* swagger会解析的包路径
**/
private String basePackage = "com.muchenx";
/**
* 分组名
*/
private String groupName = "default";
/**
* 标题
**/
private String title = "MuchenX";
/**
* 描述
**/
private String description = "MuchenX Cloud Project supports by Spring Cloud Alibaba";
/**
* 版本
**/
private String version = "v1.0";
/**
* 许可证
**/
private String license = "";
/**
* 许可证URL
**/
private String licenseUrl = "";
/**
* 服务条款URL
**/
private String termsOfServiceUrl = "";
/**
* host信息
**/
private String host = "";
/**
* 联系人信息
*/
private Contact contact = new Contact();
/**
* 自定义参数
*/
private List<Param> params = new ArrayList<>();
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getBasePackage() {
return basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getLicense() {
return license;
}
public void setLicense(String license) {
this.license = license;
}
public String getLicenseUrl() {
return licenseUrl;
}
public void setLicenseUrl(String licenseUrl) {
this.licenseUrl = licenseUrl;
}
public String getTermsOfServiceUrl() {
return termsOfServiceUrl;
}
public void setTermsOfServiceUrl(String termsOfServiceUrl) {
this.termsOfServiceUrl = termsOfServiceUrl;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Contact getContact() {
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
public List<Param> getParams() {
return params;
}
public void setParams(List<Param> params) {
this.params = params;
}
public static class Contact {
/**
* 联系人
**/
private String name = "Ian Geng";
/**
* 联系人url
**/
private String url = "www.muchenx.com";
/**
* 联系人email
**/
private String email = "gzhygz@gmail.com";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public static class Param {
// 请求参数名
private String name;
// 请求参数描述
private String desc;
/**
* 请求参数类型:QUERY("query"),HEADER("header"),PATH("path"),
* COOKIE("cookie"),FORM("form"),FORMDATA("formData"),BODY("body");
*/
private ParameterType paramType = ParameterType.HEADER;
// 请求参数默认值
private String defaultValue = "";
// 是否必填
private boolean required = false;
// 是否隐藏
private boolean hidden = false;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public ParameterType getParamType() {
return paramType;
}
public void setParamType(ParameterType paramType) {
this.paramType = paramType;
}
public String getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public boolean isHidden() {
return hidden;
}
public void setHidden(boolean hidden) {
this.hidden = hidden;
}
}
}
三、配置完毕
在启动类增加注解开起swagger:@springfox.documentation.oas.annotations.EnableOpenApi
此时控制台报错
Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113) ~[springfox-core-3.0.0.jar:3.0.0]
at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89) ~[springfox-spi-3.0.0.jar:3.0.0]
at java.base/java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:473) ~[na:na]
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:na]
at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na]
at java.base/java.util.Arrays.sort(Arrays.java:1307) ~[na:na]
at java.base/java.util.ArrayList.sort(ArrayList.java:1721) ~[na:na]
at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:392) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
...........
原因是:主要出现在Spring Boot 2.6及以后,只要是Spring Boot 2.6引入的新PathPatternParser导致的。
四、解决方案
spring官方提及此issue:because "this.condition" is null · Issue #28794 · spring-projects/spring-boot · GitHub
但尚未解决,issue已关闭。
springfox社区活跃,已有大神解决此问题:Spring 5.3/Spring Boot 2.4 support · Issue #3462 · springfox/springfox · GitHub
适合的方案如下
1.Path匹配策略切换回ant_path_matcher(大多情况此方案可解决)
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
2.若还是不能解决,添加如下配置
@Bean
ic WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
涉及依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<version>2.7.14</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<version>2.7.14</version>
<scope>compile</scope>
</dependency>
配置完重启服务问题解决!
彩蛋
swagger3与springboot完整的集成方案已上架,欢迎查收:彩蛋~