swagger整合实战

2023年 9月 21日 43.6k 0

在我们开发新的业务模块前,我们再搞完最后一块内容——Rest API在线文档。为啥要在线文档?这个主要不是给我们后端开发人员用的,而是给前端开发的mm来对接用的。

后台开发的gg在开发一个完整的业务模块前,可以先定好输入输出,并返回构造的数据,然后就把在线文档服务提供出来,给前端开发mm先熟悉和调用在线文档,这样后台开发的gg再放心的做业务层的逻辑开发,也不耽误前端开发嘛。废话不多说,开整!

集成swagger

集成swagger很简单,只需要下面几步:

  • 首先我们引入相关依赖:

    dependencies {
        ...
    
        implementation 'org.springdoc:springdoc-openapi-ui:1.5.13'
    
    }
    
  • 在application.yml中加入swagger的配置和自定义的文档说明配置:

    ...
    
    springdoc:
      packagesToScan: com.xiaojuan.boot.web.controller
      pathsToMatch: /**
      swagger-ui:
        disable-swagger-default-url: true
        path: /swagger-ui.html
      api-docs:
        # 页面访问地址 http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config
        path: /v3/api-docs
    
    # 自定义open API公共描述信息
    api:
      common:
        version: 3.0.0
        title: 小卷生鲜在线API
        description: 这是小卷生鲜电商项目的在线API
        termsOfService:
        license: 未经许可不得转载
        licenseUrl: xxx.license.com
        externalDocDesc:
        externalDocUrl:
        contact:
          name: xiaojuan
          url: edu.xiaojuan.com
          email: xiaojuan@xxx.com
    
  • 配置类

    新加一个SwaggerConfig配置类来完成swagger配置:

    package com.xiaojuan.boot.web;
    
    import io.swagger.v3.oas.models.ExternalDocumentation;
    import io.swagger.v3.oas.models.OpenAPI;
    import io.swagger.v3.oas.models.info.Contact;
    import io.swagger.v3.oas.models.info.Info;
    import io.swagger.v3.oas.models.info.License;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SwaggerConfig {
    
        @Value("${api.common.version}")         String apiVersion;
        @Value("${api.common.title}")           String apiTitle;
        @Value("${api.common.description}")     String apiDescription;
        @Value("${api.common.termsOfService}")  String apiTermsOfService;
        @Value("${api.common.license}")         String apiLicense;
        @Value("${api.common.licenseUrl}")      String apiLicenseUrl;
        @Value("${api.common.externalDocDesc}") String apiExternalDocDesc;
        @Value("${api.common.externalDocUrl}")  String apiExternalDocUrl;
        @Value("${api.common.contact.name}")    String apiContactName;
        @Value("${api.common.contact.url}")     String apiContactUrl;
        @Value("${api.common.contact.email}")   String apiContactEmail;
    
        @Bean
        public OpenAPI getOpenApiDocumentation() {
            return new OpenAPI()
                    .info(new Info().title(apiTitle)
                            .description(apiDescription)
                            .version(apiVersion)
                            .contact(new Contact()
                                    .name(apiContactName)
                                    .url(apiContactUrl)
                                    .email(apiContactEmail))
                            .termsOfService(apiTermsOfService)
                            .license(new License()
                                    .name(apiLicense)
                                    .url(apiLicenseUrl)))
                    .externalDocs(new ExternalDocumentation()
                            .description(apiExternalDocDesc)
                            .url(apiExternalDocUrl));
        }
    
    }
    
  • 在全局响应处理类RestBodyAdvice中排除掉swagger的请求地址:

    @Override
    public boolean supports(MethodParameter returnType, Class> converterType) {
        // 排除swagger的rest api请求
        String clzName = returnType.getDeclaringClass().getName();
        if (clzName.startsWith("org.springdoc")) return false;
        return true;
    }
    
  • 前面我们实现了通过filter组件来记录请求日志,现在我们要排除swagger的请求了,看下相关类内部的调整:

    package com.xiaojuan.boot.web.filter;
    
    import ...
    
    @Slf4j
    public class RequestLogFilter extends OncePerRequestFilter {
    
        ...
        
        private AntPathMatcher pathMatcher = new AntPathMatcher();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            
            if (excludeUrl(request)) {
                filterChain.doFilter(request, response);
                return;
            }
            ...
        }
    
        ...
        
        private boolean excludeUrl(HttpServletRequest request) {
            List excludeUrlPatterns = Arrays.asList("/**/swagger-ui/**", "/**/v3/api-docs/**");
            String uri = request.getRequestURI();
            for (String pattern : excludeUrlPatterns) {
                if (pathMatcher.match(pattern, uri)) {
                    return true;
                }
            }
            return false;
        }
    }
    
  • 完成上面几个步骤后,我们的swagger就集成进来了。

    启动web后,在线api访问地址:http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config

    看到的界面:

    image.png

    统一Response格式

    为了进一步规范我们统一响应格式的输出,我们制定出更明确的响应规范,这些规范也将会在我们的swagger在线文档的响应模块中体现出来。我们定的规范如下:

    • 正常响应

      响应成功的情况下,我们只需要返回status0即可,不需要把诸如success的成功信息返回,因为只要不是异常状态,就是执行成功,就像linux上执行命令的结果一样。

      • api接口返回值为void

        这种情况下,后台不需要返回数据,只需要响应一个状态即可:

        {
            "status": 0
        }
        
      • 返回非void

        说明不管取到结果是否为空,都要返回,响应格式:

        {
            "status": 0,
            "data": ...
        }
        

        这里的data类型不固定,在swagger中会由具体的数据类型的schema来决定。

    • 异常响应

      异常响应的情况下,我们将所有的信息都返回,格式如下:

      {
          "status": 1,
          "msg": "错误消息",
          "data": ...,
          "errCode": "10001"
      }
      

      其中,dataerrCode可以为空。

    按照如上的约定,现在我们调整下RestBodyAdvice的正常响应包装逻辑:

    package com.xiaojuan.boot.common.web.support;
    
    import ...
    
    @Slf4j
    @RestControllerAdvice
    public class RestBodyAdvice implements ResponseBodyAdvice {
    
        ...
    
        @SneakyThrows
        @Override
        public Object beforeBodyWrite(...) {
    
            ...
    
            Map result = new HashMap();
            result.put("status", 0);
    
            Type type = returnType.getGenericParameterType();
            if (type == String.class) {
                // 字符串需要手动序列化为json
                response.getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE);
                result.put("data", body);
                logSuccess(result);
                return objectMapper.writeValueAsString(result);
            }
            if (StringUtils.equals("void", type.getTypeName())) {
                logSuccess(result);
                return result;
            }
            result.put("data", body);
            logSuccess(result);
            return result;
        }
    
        ...
    
        @SneakyThrows
        private void logSuccess(Map result) {
            log.info("========== 响应结果: {}", objectMapper.writeValueAsString(result));
            log.info("====================================================================================================");
        }
    }
    

    因为我们之前实现了全局统一的响应格式,而对Rest API接口的返回值类型做了精简,现在要生成swagger响应的schema,默认无法展示出统一响应格式的全部内容,比如,看下用户信息查询的swagger schema默认展示:

    image.png

    swagger为我们提供了对API操作的定制形式,可以个性化展示各种信息,现在我们做如下定制:

    package com.xiaojuan.boot.web;
    
    import ...
    
    @Slf4j
    @Configuration
    public class SwaggerConfig {
    
        ...
    
        @Bean
        public OperationCustomizer operationCustomizer() {
            return (operation, method) -> {
                ApiResponses responses = operation.getResponses();
                for (String statusKey : responses.keySet()) {
                    ApiResponse resp = responses.get(statusKey);
                    Type type = method.getReturnType().getGenericParameterType();
                    ResolvedSchema resolvedSchema;
                    if (type instanceof ParameterizedTypeImpl) {
                        resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType(type).resolveAsRef(true));
                    } else {
                        resolvedSchema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(type));
                    }
    
                    Schema respSchema = null;
                    if ("200".equals(statusKey)) {
                        respSchema = new ObjectSchema()
                                .type("object")
                                .addProperties("status", new IntegerSchema()._default(0))
                                .addProperties("data", StringUtils.equals("void", type.getTypeName()) ? null : resolvedSchema.schema);
                    } else {
                        respSchema = new ObjectSchema()
                                .type("object")
                                .addProperties("status", new IntegerSchema()._default(1))
                                .addProperties("msg", new StringSchema())
                                .addProperties("errCode", new IntegerSchema())
                                .addProperties("data", new ObjectSchema());
                    }
                    resp.setContent(new Content().addMediaType("application/json",new MediaType().schema(respSchema)));
                }
                return operation;
            };
        }
    
    }
    

    代码说明

    这里我们获取controller每个方法的返回值类型后,通过swagger提供的工具API将其转成swagger的schema对象,用于在swagger响应中描述响应对象的schema。这里要注意返回值类型可能是一个泛型类型,则我们用其提供的readAllAsResolvedSchema方法进行级联解析,并以$ref属性来绑定schema的引用关系,而普通类型这直接解析为相应的schema即可。

    然后我们判断这条响应操作的http状态码,也就是我们在API接口上通过如下形式定义的响应状态值:

    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "注册成功"),
        @ApiResponse(responseCode = "500 - 10001", description = "参数校验失败"),
        @ApiResponse(responseCode = "500 - 10002", description = "用户已注册")
    })
    @PostMapping("register")
    void register(@Valid UserRegisterDTO userRegisterDTO);
    

    我们判断响应操作是不是200的http状态码,再构造成功响应和失败响应的schema结构,对响应操作进行设置。这样我们就避免了手动在@ApiResponse中通过相关注解设置schema,而该方式对于正常响应无法指定泛型类型。

    这样我们看到的效果,

    查看用户列表的swagger响应参考:

    image.png

    这里我们并没有在API接口上加@ApiResponses注解来指定一条200状态码的响应记录,swagger会默认给我们生成一条对应的响应文档。而前面我们对用户注册的响应描述做了定义后,会看到:

    image.png

    这里为了排版看的舒服,截图后横向排列了。

    这里我们为响应码约定了一种格式:http状态码 - 业务错误码。除了这种方式,我们也可以用一个500的http状态码,通过swagger响应注解提供的扩展配置来实现其他信息的设置:

    @ApiResponse(responseCode = "500", description = "注册失败",
                 extensions = @Extension(name = SysConst.ERROR_CODE,
                                         properties = {
                                             @ExtensionProperty(name = "10001", value="参数校验失败"),
                                             @ExtensionProperty(name = "10002", value="用户已注册")
                                         }))
    

    这样的话,我们可以将额外的信息设置到schemaerrCode字段描述中,调整下个性化的配置:

    package com.xiaojuan.boot.web;
    
    import ...
    
    @Slf4j
    @Configuration
    public class SwaggerConfig {
    
        private static final String EXTENSION_PREFIX = "x-";
    
        ...
    
        @Bean
        public OperationCustomizer operationCustomizer() {
            return (operation, method) -> {
                ApiResponses responses = operation.getResponses();
                for (String statusKey : responses.keySet()) {
                    ...
    
                    Schema respSchema;
                    if ("200".equals(statusKey)) {
                        ...
                    } else {
                        respSchema = new ObjectSchema()...
                        appendErrCodeInfo(resp, (ObjectSchema) respSchema);
                    }
                    ...
                }
                return operation;
            };
        }
    
        private void appendErrCodeInfo(ApiResponse resp, ObjectSchema schema) {
            Map extMap = resp.getExtensions();
            if (ObjectUtils.isNotEmpty(extMap)) {
                String key = EXTENSION_PREFIX + SysConst.ERROR_CODE;
                if (extMap.containsKey(key)) {
                    Map errCodeMap = (Map) extMap.get(key);
                    StringBuilder builder = new StringBuilder("可能返回的错误码:");
                    int i = 0;
                    for (Map.Entry entry : errCodeMap.entrySet()) {
                        builder.append(entry.getKey()).append("-").append(entry.getValue());
                        if (i < errCodeMap.keySet().size() - 1) {
                            builder.append("、");
                        }
                        i++;
                    }
                    schema.getProperties().get("errCode").setDescription(builder.toString());
                }
            }
        }
    
    }
    

    要注意,扩展的配置属性名会以x-开头。这样我们可以看到schema所展示的信息多了一个:

    image.png

    这里不得吐槽下,swagger官方的文档风格不够大气,后面我们将用其他的UI依赖来升级这种用户体验。

    swagger常用注解

    关于swagger常用注解的用法,这里我们将以实际的例子来介绍。

    package com.xiaojuan.boot.web.api;
    
    import ...
    
    @Tag(name = "UserAPI", description = "用户API")
    ...
    public interface UserAPI {
    
        ...
    
        @Operation(summary = "用户注册接口", description = "注册一个普通用户")
        ...
        void register(@Valid UserRegisterDTO userRegisterDTO);
    
        ...
    }
    

    这些说明性的注解,会给swagger文档带来如下装饰:

    image.png

    用于方法参数(@Parameter)和DTO字段(@Schema)修饰的swagger注解用法示例如下:

    @PostMapping("login")
    UserInfoDTO login(
        @Parameter(name = "用户名", description = "请输入用户名")
        ...
        String username,
        @Parameter(name = "密码", description = "请输入密码", schema = @Schema(minLength = 3, maxLength = 10))
        ...
        String password
    );
    
    public class UserRegisterDTO {
    
        ...
        @Schema(description = "年龄", maximum = "60")
        private Integer age;
        
        ...
        @Schema(description = "手机号", pattern = PatternConst.MOBILE)
        private String mobileNo;
    
        ...
    }
    

    上面我们用swagger注解为密码参数和手机号字段指定了约束,这些约束仅用于swagger在线文档上文本框输入的检查或DTO类型schema的格式描述,以增加可读性,而不会应用到后台的校验上;另外,我们原来加的validation校验注解同样会被应用到swagger在线文档中,对字段约束和格式进行描述或控制表单参数输入文本框的检查。看下应用的效果:

    image.png

    image.png

    只是这种展示风格不够大气罢了。

    api分组

    当我们开发的一个单体应用中业务模块太多的话,我们可以实现api的分组,按照分组来隔开各模块的API在线文档。

    现在我们将开发的controller组件按照模块划分到不同的子包下:

    image.png

    在application.yml中进行分组配置:

    springdoc:
    #  packagesToScan: com.xiaojuan.boot.web.controller
    #  pathsToMatch: /**
      group-configs:
        - group: '用户模块'
          packagesToScan: com.xiaojuan.boot.web.controller.user
          pathsToMatch: /**
        - group: '管理模块'
          packagesToScan: com.xiaojuan.boot.web.controller.admin
          pathsToMatch: /**
        - group: '测试模块'
          packagesToScan: com.xiaojuan.boot.web.controller.test
          pathsToMatch: /**  
    

    这样就可以选择一个分组来进入其api文档页面了:

    image.png

    本节我们实现了spring boot与swagger在线文档的整合,但我们也发现本身swagger官方的文档功能都展示出来了,就排版不是那么友好。且我们在开发Rest API时要手动声明swagger文档的注解,会做很多额外的工作。后面我们会介绍用swagger-codegen插件来基于我们定义的swagger遵循的open api3规范的yml配置来帮我们生成Rest API的接口代码,并包含生成好的swagger注解。这样我们只要维护配置文件即可。大家加油!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论