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

目录

一、依赖

二、集成Swagger Java Config

三、配置完毕

四、解决方案

彩蛋


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

一、依赖

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Swagger 是一个接口文档生成工具,可以方便地生成 RESTful API 文档。在 Spring Boot 中,使用 Swagger 也非常简单,只需要添加对应的依赖,然后在配置文件中进行简单的配置即可。 下面是在 Spring Boot 中添加 Swagger 的步骤: 1. 在 pom.xml 文件中添加 Swagger 的依赖: ``` <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> ``` 2. 在 Spring Boot 的启动类上添加 `@EnableSwagger2` 注解,启用 Swagger: ``` @SpringBootApplication @EnableSwagger2 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 3. 添加 Swagger 配置类,配置 Swagger 的基本信息: ``` @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo")) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("API 文档") .description("API 接口文档") .version("1.0.0") .build(); } } ``` 其中,`@Bean` 注解的 `Docket` 对象是 Swagger 的主要配置对象,可以设置 API 的基本信息,如文档标题、版本号等。`apis` 方法和 `paths` 方法可以设置 API 的扫描范围,这里的示例是扫描 `com.example.demo` 包下的所有 API。 4. 启动应用程序,在浏览器中访问 `http://localhost:8080/swagger-ui.html`,即可看到自动生成的 API 文档。 以上就是在 Spring Boot 中使用 Swagger 的简单步骤,你还可以根据自己的需求进行更加详细的配置。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流沙QS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值