前言

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。

StepDescription
1引入Maven依赖
2在Spring Boot中启用Swagger
3创建SwaggerConfig类
4创建Docket Bean
5提供API信息
6配置Swagger UI
7应用Swagger

项目背景

版本

SpringBoot 2.7.*
springfox 3.0

Maven依赖

<dependency>    <groupId>io.springfox</groupId>    <artifactId>springfox-swagger2</artifactId>    <version>3.0.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency>
@Configuration@EnableSwagger2@EnableAutoConfiguration@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)public class SwaggerAutoConfiguration{    /**     * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点     */    private static final List<String> DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**");    private static final String BASE_PATH = "/**";    @Bean    @ConditionalOnMissingBean    public SwaggerProperties swaggerProperties()    {        return new SwaggerProperties();    }    @Bean    public Docket api(SwaggerProperties swaggerProperties)    {        // base-path处理        if (swaggerProperties.getBasePath().isEmpty())        {            swaggerProperties.getBasePath().add(BASE_PATH);        }        // noinspection unchecked        List<Predicate<String>> basePath = new ArrayList<Predicate<String>>();        swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path)));        // exclude-path处理        if (swaggerProperties.getExcludePath().isEmpty())        {            swaggerProperties.getExcludePath().addAll(DEFAULT_EXCLUDE_PATH);        }        List<Predicate<String>> excludePath = new ArrayList<>();        swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));        ApiSelectorBuilder builder = new Docket(DocumentationType.SWAGGER_2).host(swaggerProperties.getHost())                .apiInfo(apiInfo(swaggerProperties)).select()                .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()));        swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p)));        swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate()));        return builder.build().securitySchemes(securitySchemes()).securityContexts(securityContexts()).pathMapping("/");    }    /**     * 安全模式,这里指定token通过Authorization头请求头传递     */    private List<SecurityScheme> securitySchemes()    {        List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();        apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));        return apiKeyList;    }    /**     * 安全上下文     */    private List<SecurityContext> securityContexts()    {        List<SecurityContext> securityContexts = new ArrayList<>();        securityContexts.add(                SecurityContext.builder()                        .securityReferences(defaultAuth())                        .operationSelector(o -> o.requestMappingPattern().matches("/.*"))                        .build());        return securityContexts;    }    /**     * 默认的全局鉴权策略     *     * @return     */    private List<SecurityReference> defaultAuth()    {        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];        authorizationScopes[0] = authorizationScope;        List<SecurityReference> securityReferences = new ArrayList<>();        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));        return securityReferences;    }    private ApiInfo apiInfo(SwaggerProperties swaggerProperties)    {         return new ApiInfoBuilder()             .title(swaggerProperties.getTitle())             .description(swaggerProperties.getDescription())             .license(swaggerProperties.getLicense())             .licenseUrl(swaggerProperties.getLicenseUrl())             .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())             .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail()))             .version(swaggerProperties.getVersion())             .build();    }}
@Component@ConfigurationProperties("swagger")public class SwaggerProperties{    /**     * 是否开启swagger     */    private Boolean enabled;    /**     * swagger会解析的包路径     **/    private String basePackage = "";    /**     * swagger会解析的url规则     **/    private List<String> basePath = new ArrayList<>();    /**     * 在basePath基础上需要排除的url规则     **/    private List<String> excludePath = new ArrayList<>();    /**     * 标题     **/    private String title = "";    /**     * 描述     **/    private String description = "";    /**     * 版本     **/    private String version = "";    /**     * 许可证     **/    private String license = "";    /**     * 许可证URL     **/    private String licenseUrl = "";    /**     * 服务条款URL     **/    private String termsOfServiceUrl = "";    /**     * host信息     **/    private String host = "";    /**     * 联系人信息     */    private Contact contact = new Contact();    /**     * 全局统一鉴权配置     **/    private Authorization authorization = new Authorization();    public Boolean getEnabled()    {        return enabled;    }    public void setEnabled(Boolean enabled)    {        this.enabled = enabled;    }    public String getBasePackage()    {        return basePackage;    }    public void setBasePackage(String basePackage)    {        this.basePackage = basePackage;    }    public List<String> getBasePath()    {        return basePath;    }    public void setBasePath(List<String> basePath)    {        this.basePath = basePath;    }    public List<String> getExcludePath()    {        return excludePath;    }    public void setExcludePath(List<String> excludePath)    {        this.excludePath = excludePath;    }    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 Authorization getAuthorization()    {        return authorization;    }    public void setAuthorization(Authorization authorization)    {        this.authorization = authorization;    }    public static class Contact    {        /**         * 联系人         **/        private String name = "";        /**         * 联系人url         **/        private String url = "";        /**         * 联系人email         **/        private String email = "";        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 Authorization    {        /**         * 鉴权策略ID,需要和SecurityReferences ID保持一致         */        private String name = "";        /**         * 需要开启鉴权URL的正则         */        private String authRegex = "^.*$";        /**         * 鉴权作用域列表         */        private List<AuthorizationScope> authorizationScopeList = new ArrayList<>();        private List<String> tokenUrlList = new ArrayList<>();        public String getName()        {            return name;        }        public void setName(String name)        {            this.name = name;        }        public String getAuthRegex()        {            return authRegex;        }        public void setAuthRegex(String authRegex)        {            this.authRegex = authRegex;        }        public List<AuthorizationScope> getAuthorizationScopeList()        {            return authorizationScopeList;        }        public void setAuthorizationScopeList(List<AuthorizationScope> authorizationScopeList)        {            this.authorizationScopeList = authorizationScopeList;        }        public List<String> getTokenUrlList()        {            return tokenUrlList;        }        public void setTokenUrlList(List<String> tokenUrlList)        {            this.tokenUrlList = tokenUrlList;        }    }    public static class AuthorizationScope    {        /**         * 作用域名称         */        private String scope = "";        /**         * 作用域描述         */        private String description = "";        public String getScope()        {            return scope;        }        public void setScope(String scope)        {            this.scope = scope;        }        public String getDescription()        {            return description;        }        public void setDescription(String description)        {            this.description = description;        }    }
/** * swagger 资源映射路径 *  */@Configurationpublic class SwaggerWebConfiguration implements WebMvcConfigurer{    @Override    public void addResourceHandlers(ResourceHandlerRegistry registry)    {        /** swagger-ui 地址 */        registry.addResourceHandler("/swagger-ui/**")                .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");    }}

报错一:: Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerExceptionat org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181)at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54)at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356)at java.lang.Iterable.forEach(Iterable.java:75)at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155)at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123)at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935)at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586)at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)at *Application.main(Application.java:22)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)Caused by: java.lang.NullPointerException: nullat springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56)at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113)at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89)at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469)at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)at java.util.TimSort.sort(TimSort.java:234)at java.util.Arrays.sort(Arrays.java:1512)at java.util.ArrayList.sort(ArrayList.java:1454)at java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:387)at java.util.stream.Sink$ChainedReference.end(Sink.java:258)at java.util.stream.Sink$ChainedReference.end(Sink.java:258)at java.util.stream.Sink$ChainedReference.end(Sink.java:258)at java.util.stream.Sink$ChainedReference.end(Sink.java:258)at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107)at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91)at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82)at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100)at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178)... 19 common frames omitted

从报错顺序跟踪源码查看执行步骤

step操作
1SpringBoot 启动同时Bean初始化完毕
2初始化DocumentationPluginsBootstrapper同时将RequestHandlerProvider通过构造器驻入
3调用DocumentationPluginsBootstrapper start 方法加载Docket插件
4解析RequestHandlerProvider并将数据存到DocumentationCache中
5请求Swagger2Controller 的v2/api-docs接口,通过groupName从DocumentationCache中将数据取出,在将数据统一封装到Swagger类中,序列化成json返回给

报错二:No operations defined in spec!

解决

SpringBoot 2.6.0开始,请求路径与SpringMVC处理映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser。可以通过设置spring.mvc.pathmatch.matching-strategy为ant-path-matcher来改变。

除了basePackage包路径配错以外。以下方案可解决以上问题。

/** * swagger 在 springboot 2.6.x 不兼容问题的处理 * */@Componentpublic class SwaggerBeanPostProcessor implements BeanPostProcessor{    @Override    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException    {        if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider)        {            customizeSpringfoxHandlerMappings(getHandlerMappings(bean));        }        return bean;    }    private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings)    {        List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null)                .collect(Collectors.toList());        mappings.clear();        mappings.addAll(copy);    }    @SuppressWarnings("unchecked")    private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean)    {        try        {            Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");            field.setAccessible(true);            return (List<RequestMappingInfoHandlerMapping>) field.get(bean);        }        catch (IllegalArgumentException | IllegalAccessException e)        {            throw new IllegalStateException(e);        }    }}

配置:

spring:  mvc:    pathmatch:      matching-strategy: ant_path_matcher

心如欲壑,后土难填。