关于springboot:谁家面试往死里问-Swagger-啊

57次阅读

共计 13565 个字符,预计需要花费 34 分钟才能阅读完成。

大家好,我是小富~

前言

说个挺奇葩的事,有个老铁给我发私信吐槽了一下它的面试经验,他去了个国企单位面试,而后面试官跟他就 Swagger 的问题聊了半个多小时。额~ 面试嘛这些都不稀奇,总能遇到是千奇百怪的人,千奇百怪的问题。不过,我剖析这个面试官是不太好意思间接让他走,哈哈哈!

什么是 Swagger?

Swagger 目前是比拟支流的 RESTful 格调的 API 文档工具,做过开发的人应该都用过它吧!

它提供了一套工具和标准,让开发人员可能更轻松地创立和保护可读性强、易于应用和交互的 API 文档(官网口气)。

title: Swagger
desc: Swagger 官方网站
logo: https://static1.smartbear.co/swagger/media/assets/images/swagger_logo.svg
link: https://swagger.io/

为什么用 Swagger?

以往在没有这样的 API 文档工具,开发人员须要手动编写和保护性能 API 的文档。而且,因为 API 变更往往难以及时更新到文档中,这可能会给依赖文档的开发者带来困惑。

说几个 Swagger 的特点:

  • 最重要的一点能够依据代码注解主动生成 API 文档,能生成的相对不手写,而且 API 文档与 API 定义会同步更新。
  • 它提供了一个可执行的 Web 界面,反对 API 在线测试,能够间接在界面上间接设置参数测试,不必额定的测试工具或插件。
  • 反对多种编程语言,JavaPHPPython等语言都反对,喜爱什么语言构建 API 都行。

总的来说,Swagger 能够让咱们更多工夫在专一于编写代码(摸鱼),而不是破费额定精力来保护文档,实际出真知先跑个 demo 试试。

Swagger 搭建

maven 依赖

目前应用的版本是 Swagger3.0、Springboot 2.7.6,Swagger2.0 与 3.0 依赖包名称的变动有些大,须要特地留神一下。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.6</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

配置类

首先咱们创立一个控制器 TestController 类,里边只有一个最简略的申请 /test

@RestController
public class TestController {@RequestMapping("/test")
    public String test(String name) {return name;}
}

接下来创立配置类 SwaggerConfig,类标注@EnableSwagger2 注解是要害,到这最简略的 Swagger 文档环境就搭建好了。

import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {}

启动报错

启动时可能会报如下的谬误,这是因为高版本的 SpringbootSwagger版本应用的门路匹配策略抵触导致的。

Springfox应用的门路匹配规定为 AntPathMatcher 的,而SpringBoot2.7.6 应用的是PathPatternMatcher,两者抵触了。

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.24.jar:5.3.24]
    at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_341]
    at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.6.jar:2.7.6]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.6.jar:2.7.6]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.6.jar:2.7.6]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.6.jar:2.7.6]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.6.jar:2.7.6]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.6.jar:2.7.6]
    at com.springboot101.SwaggerApplication.main(SwaggerApplication.java:10) [classes/:na]

解决方案

这个谬误的解决办法比拟多,我整顿了四种解决此问题的计划,你看哪个更适合你。

1、升高版本

SpringBoot 版本升高到 2.5.X、springfox 降到 3.X 以下能够解决问题,不过不举荐这么做,毕竟降配置做兼容显得有点傻。

2、对立门路匹配策略

SpringMVC 的匹配 URL 门路的策略改为 ant_path_matcherapplication.yml 文件减少如下的配置:

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

3、@EnableWebMvc 注解

在配置类 SwaggerConfig 上标注 @EnableWebMvc 注解也能够解决。

Swagger框架须要通过解析和扫描带有注解的 Controller 类和办法来生成 API 文档。@EnableWebMvc注解会注册一个 RequestMappingHandlerMapping 的 Bean,并将其作为默认的申请映射处理器,以确保这些 Controller 类和办法可能被正确处理,能够与 Swagger 的门路配置规定相匹配,从而使得 Swagger 可能胜利生成 API 文档。

@EnableWebMvc
@Configuration
@EnableSwagger2
public class SwaggerConfig {}

4、注册 BeanPostProcessor

也能够自行实现兼容逻辑来解决这个问题,咱们能够在 Spring 容器中注册一个BeanPostProcessor,在该处理器中对 HandlerMappings 进行定制。

通过过滤掉已存在 PatternParser 的映射,意味着咱们能够将 Swagger 特定的 HandlerMappings 增加到 HandlerMappings 列表中,从而应用自定义的设置来代替原有的 HandlerMappings。

这样修复了可能导致 Swagger 无奈失常应用的问题。

@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {return new 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) {log.warn("批改 WebMvcRequestHandlerProvider 的属性:handlerMappings 出错,可能导致 swagger 不可用", e);
                throw new IllegalStateException(e);
            }
        }
    };
}

拜访 swagger-ui

到这,问题解决!咱们拜访 Swagger 文档门路 http://127.0.0.1:9002/swagger-ui/index.html,可能看到咱们写的 API 信息以及一些 Swagger 文档的默认配置信息。

留神到咱们只写了一个 /test接口,但这里确把这个办法的所有申请形式都列了进去,因为咱们在 controller 办法中应用了 @RequestMapping 注解,并没有具体的指定接口的申请形式,所以防止文档冗余,尽量指定申请形式或者应用指定申请形式的 @XXXMapping 注解。

指定申请形式后:

API 文档配置

上边咱们拜访的文档中展现的数据都是默认的配置,当初咱们来定制化一下文档。

Springfox提供了一个 Docket 对象,供咱们灵便的配置 Swagger 的各项属性。Docket对象内提供了很多的办法来配置文档,下边介绍下罕用的配置项。

select

select()返回一个 ApiSelectorBuilder 对象,是应用 apis()paths() 两个办法的前提,用于指定 Swagger 要扫描的接口和门路。

apis

默认状况下,Swagger 会扫描整个我的项目中的接口,通过 apis()办法,你能够传入一个 RequestHandlerSelector 对象实例来指定要蕴含的接口所在的包门路。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.springboot101.controller"))
            .build();}

paths

仅将某些特定申请门路的 API 展现在 Swagger 文档中,例如门路中蕴含 /test。能够应用 apis()paths() 办法一起来过滤接口。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .paths(PathSelectors.ant("/test/**"))
            .build();}

groupName

为生成的 Swagger 文档指定分组的名称,用来辨别不同的文档组。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .groupName("用户分组")
            .build();}

实现文档的多个分组,则需创立多个 Docket 实例,设置不同的组名,和组内过滤 API 的条件。

@Bean
public Docket docket1(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .groupName("商家分组")
            .select()
            .paths(PathSelectors.ant("/test1/**"))
            .build();}

apiInfo

设置 API 文档的根本信息,例如题目、形容、版本等。你能够应用 ApiInfo 对象自定义信息。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo()); // 文档根底配置
}

private ApiInfo apiInfo() {Contact contact = new Contact("小富", "http://fire100.top", "email@example.com");
    return new ApiInfoBuilder()
            .title("Swagger 学习")
            .description("程序员小富 - 带你一起学习 Swagger")
            .version("v1.0.1")
            .termsOfServiceUrl("http://fire100.top")
            .contact(contact)
            .license("许可证")
            .licenseUrl("许可链接")
            .extensions(Arrays.asList(new StringVendorExtension("我是", "小富"),
                    new StringVendorExtension("你是", "谁")
            ))
            .build();}

对应的 Swagger 文档页面上展现的地位

enable

启用或禁用 Swagger 文档的生成,有时测试环境会凋谢 API 文档,但在生产环境则要禁用,能够依据环境变量管制是否显示。

@Bean
public Docket docket(Environment environment) {
    // 可显示 swagger 文档的环境
    Profiles of = Profiles.of("dev", "test","pre");
    boolean enable = environment.acceptsProfiles(of);

    return new Docket(DocumentationType.SWAGGER_2)
            .enable(enable)
            .apiInfo(apiInfo()); // 文档根底配置
}

host

API 文档显示的主机名称或 IP 地址,即在测试执行接口时应用的 IP 或域名。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .host("http://test.com") // 申请地址
            .apiInfo(apiInfo()); // 文档根底配置
}

securitySchemes

配置 API 平安认证形式,比方常见的在 header 中设置如 BearerAuthorizationBasic 等鉴权字段,ApiKey对象中字段含意别离是别名、鉴权字段 key、鉴权字段增加的地位。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .securitySchemes(
                    Arrays.asList(new ApiKey("Bearer 鉴权", "Bearer", "header"),
                            new ApiKey("Authorization 鉴权", "Authorization", "header"),
                            new ApiKey("Basic 鉴权", "Basic", "header")
                    )
            );
}

这样配置后,Swagger 文档将会蕴含一个 Authorize 按钮,供用户输出咱们设定的 BearerAuthorizationBasic 进行身份验证。

securityContexts

securitySchemes办法中尽管设置了鉴权字段,但此时在测试接口的时候不会主动在 header中加上鉴权字段和值,还要配置 API 的平安上下文,指定哪些接口须要进行平安认证。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .securitySchemes(
                    Arrays.asList(new ApiKey("Bearer 鉴权", "Bearer", "header"),
                            new ApiKey("Authorization 鉴权", "Authorization", "header"),
                            new ApiKey("Basic 鉴权", "Basic", "header")
                    )
            )
            .securityContexts(Collections.singletonList(securityContext()));
}

private SecurityContext securityContext() {return SecurityContext.builder()
            .securityReferences(
                    Arrays.asList(new SecurityReference("Authorization", new AuthorizationScope[0]),
                            new SecurityReference("Bearer", new AuthorizationScope[0]),
                            new SecurityReference("Basic", new AuthorizationScope[0])))
            .build();}

当初在测试调用 API 接口时,header中能够失常加上鉴权字段和值了。

tags

为 API 文档中的接口增加标签,标签能够用来对 API 进行分类或分组,并提供更好的组织和导航性能。

@Bean
public Docket docket(Environment environment) {return new Docket(DocumentationType.SWAGGER_2)
            .tags(new Tag("小富接口", "小富相干的测试接口"))
}

受权登录

出于对系统安全性的思考,通常咱们还会为 API 文档减少登录性能。

引入 maven 依赖

swagger 的平安登录是基于 security 实现的,引入相干的 maven 依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

登录配置

application.yml文件中配置登录 swagger 的用户名和明码。

spring:
  security:
    user:
      name: admin
      password: 123456

再次拜访文档就会呈现如下的登录页

文档注解

当咱们心愿在 Swagger 文档中提供具体和残缺的内容时,还能够应用其余许多 Swagger 内置注解来进一步丰盛和定制 API 文档。

@ApiIgnore

上边咱们提到能够依据指定门路或者包门路来提供 API,也能够应用粒度更细的 @ApiIgnore 注解,来实现某个 API 在文档中疏忽。

@ApiIgnore
@GetMapping("/user2/{id}")
public User test2(@PathVariable Integer id, @RequestBody User user) {return user;}

@ApiModel

在咱们的接口中,只有应用实体作为参数或响应体,Swagger 就会主动扫描到它们,但你会发现目前这些实体不足具体的形容信息。为了让使用者通俗易懂,须要应用 swagger 提供的注解为这些实体增加具体的形容。

@ApiModel注解的应用在实体类上,提供对 Swagger Model 额定信息的形容。

@ApiModelProperty

@ApiModelProperty 注解为实体类中的属性增加形容,提供了字段名称、是否必填、字段示例等形容信息。

@ApiModel(value = "用户实体类", description = "用于寄存用户登录信息")
@Data
public class User {@ApiModelProperty(value = "用户名字段", required = true, example = "# 公众号:程序员小富")
    private String name;

    @ApiModelProperty(value = "年龄", required = true, example = "19")
    private Integer age;

    @ApiModelProperty(value = "邮箱", required = true, example = "# 公众号:程序员小富")
    private String email;
}

@Api

@Api 注解用于标记一个控制器(controller)类,并提供接口的详细信息和配置项。

  • value:API 接口的形容信息,因为版本 swagger 版本起因,value可能会不失效能够应用description
  • hidden:该 API 是否在 Swagger 文档中暗藏
  • tags:API 的标签,如果此处与 new Docket().tags 中设置的标签统一,则会将该 API 放入到这个标签组内
  • authorizations:鉴权配置,配合 @AuthorizationScope 注解管制权限范畴或者特定密钥能力拜访该 API。
  • produces:API 的响应内容类型,例如 application/json。
  • consumes:API 的申请内容类型,例如 application/json。
  • protocols:API 反对的通信协议。
@Api(value = "用户治理接口形容",
        description = "用户治理接口形容",
        hidden = false,
        produces = "application/json",
        consumes = "application/json",
        protocols = "https",
        tags = {"用户治理"},
        authorizations = {
                @Authorization(value = "apiKey", scopes = {@AuthorizationScope(scope = "read:user", description = "读权限"),
                        @AuthorizationScope(scope = "write:user", description = "写权限")
                }),
                @Authorization(value = "basicAuth")
        })
@RestController
public class TestController {}

@ApiOperation

@ApiOperation该注解作用在接口办法上,用来对一个操作或 HTTP 办法进行形容。

  • value:对接口办法的简略阐明
  • notes:对操作的具体阐明。
  • httpMethod:申请形式
  • code:状态码,默认为 200。能够传入符合标准的 HTTP Status Code Definitions。
  • hidden:在文档中暗藏该接口
  • response:返回的对象
  • tags:应用该注解后,该接口办法会独自进行分组
  • produces:API 的响应内容类型,例如 application/json。
  • consumes:API 的申请内容类型,例如 application/json。
  • protocols:API 反对的通信协议。
  • authorizations:鉴权配置,配合 @AuthorizationScope 注解管制权限范畴或者特定密钥能力拜访该 API。
  • responseHeaders:响应的 header 内容

@ApiOperation(
        value = "获取用户信息",
        notes = "通过用户 ID 获取用户的详细信息",
        hidden = false,
        response = UserDto.class,
        tags = {"用户治理"},
        produces = "application/json",
        consumes = "application/json",
        protocols = "https",
        authorizations = {@Authorization(value = "apiKey", scopes = {@AuthorizationScope(scope = "read:user", description = "读权限")}),
                @Authorization(value = "Basic")
        },
        responseHeaders = {@ResponseHeader(name = "X-Custom-Header", description = "Custom header", response = String.class)},
        code = 200,
        httpMethod = "GET"
)
@GetMapping("/user1")
public UserDto user1(@RequestBody User user) {return new UserDto();
}

@ApiImplicitParams

@ApiImplicitParams注解用在办法上,以数组形式存储,配合@ApiImplicitParam 注解应用。

@ApiImplicitParam

@ApiImplicitParam注解对 API 办法中的繁多参数进行注解。

留神 这个注解 @ApiImplicitParam 必须被蕴含在注解 @ApiImplicitParams 之内。

  • name:参数名称
  • value:参数的简短形容
  • required:是否为必传参数
  • dataType:参数类型,能够为类名,也能够为根本类型(String,int、boolean 等)
  • paramType:参数的传入(申请)类型,可选的值有 path、query、body、header、form。
@ApiImplicitParams({@ApiImplicitParam(name = "用户名", value = "用户名称信息", required = true, dataType = "String", paramType = "query")
})
@GetMapping("/user")
public String user(String name) {return name;}

@ApiParam()

@ApiParam()也是对 API 办法中的繁多参数进行注解,其外部属性和 @ApiImplicitParam 注解类似。

@GetMapping("/user4")
public String user4(@ApiParam(name = "主键 ID", value = "@ApiParam 注解测试", required = true) String id) {return id;}

@ApiResponses

@ApiResponses注解可用于形容申请的状态码,作用在办法上,以数组形式存储,配合 @ApiResponse注解应用。

@ApiResponse

@ApiResponse注解形容一种申请的状态信息。

  • code:HTTP 申请响应码。
  • message:响应的文本音讯
  • response:返回类型信息。
  • responseContainer:如果返回类型为容器类型,能够设置相应的值。有效值为 “List”、“Set”、”Map” 其余任何有效的值都会被疏忽。
@ApiResponses(value = {@ApiResponse(code = 200, message = "@ApiResponse 注解测试通过", response = String.class),
        @ApiResponse(code = 401, message = "可能参数填的有问题", response = String.class),
        @ApiResponse(code = 404, message = "可能申请门路写的有问题", response = String.class)
})
@GetMapping("/user4")
public String user4(@ApiParam(name = "主键 ID", value = "@ApiParam 注解测试", required = true) String id) {return id;}

总结

只管在面试中不会过多考查 Swagger 这类工具,但作为开发者,养成良好的文档标准习惯是十分重要的,无论应用 Swagger 还是其余文档工具,编写清晰、详尽的 API 文档都应该是咱们的素养之一。

代码示例

https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/%E9%80%9A%E7%94%A8%E5%8A%9F%E8%83%BD/springboot-swagger

正文完
 0