SpringBoot v2.7.x+ 整合Swagger3入坑记?

目录

一、依赖

[二、集成Swagger Java Config](#二、集成Swagger Java Config)

三、配置完毕

四、解决方案

彩蛋


想尝鲜,坑也多,一起入个坑~

一、依赖

SpringBoot版本:2.7.14

Swagger版本:3.0.0

XML 复制代码
<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

java 复制代码
@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;
}
java 复制代码
/**
 * 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

java 复制代码
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

此时控制台报错

java 复制代码
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(大多情况此方案可解决)

XML 复制代码
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

2.若还是不能解决,添加如下配置

java 复制代码
@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));
}

涉及依赖

XML 复制代码
<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完整的集成方案已上架,欢迎查收:彩蛋~

相关推荐
ok!ko3 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622664 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰4 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没5 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
编程、小哥哥5 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程6 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码7 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端