swagger整合实战

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

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

集成swagger

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

  1. 首先我们引入相关依赖:

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

    yaml 复制代码
    ...
    
    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
  3. 配置类

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

    java 复制代码
    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));
        }
    
    }
  4. 在全局响应处理类RestBodyAdvice中排除掉swagger的请求地址:

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

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

看到的界面:

统一Response格式

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

  • 正常响应

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

    • api接口返回值为void

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

      json 复制代码
      {
          "status": 0
      }
    • 返回非void

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

      json 复制代码
      {
          "status": 0,
          "data": ...
      }

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

  • 异常响应

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

    json 复制代码
    {
        "status": 1,
        "msg": "错误消息",
        "data": ...,
        "errCode": "10001"
    }

    其中,dataerrCode可以为空。

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

java 复制代码
package com.xiaojuan.boot.common.web.support;

import ...

@Slf4j
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {

    ...

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(...) {

        ...

        Map<String, Object> 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<String, Object> result) {
        log.info("========== 响应结果: {}", objectMapper.writeValueAsString(result));
        log.info("====================================================================================================");
    }
}

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

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

java 复制代码
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接口上通过如下形式定义的响应状态值:

java 复制代码
@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响应参考:

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

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

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

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

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

java 复制代码
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<String, Object> extMap = resp.getExtensions();
        if (ObjectUtils.isNotEmpty(extMap)) {
            String key = EXTENSION_PREFIX + SysConst.ERROR_CODE;
            if (extMap.containsKey(key)) {
                Map<String, String> errCodeMap = (Map<String, String>) extMap.get(key);
                StringBuilder builder = new StringBuilder("可能返回的错误码:");
                int i = 0;
                for (Map.Entry<String, String> 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所展示的信息多了一个:

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

swagger常用注解

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

java 复制代码
package com.xiaojuan.boot.web.api;

import ...

@Tag(name = "UserAPI", description = "用户API")
...
public interface UserAPI {

    ...

    @Operation(summary = "用户注册接口", description = "注册一个普通用户")
    ...
    void register(@Valid UserRegisterDTO userRegisterDTO);

    ...
}

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

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

java 复制代码
@PostMapping("login")
UserInfoDTO login(
    @Parameter(name = "用户名", description = "请输入用户名")
    ...
    String username,
    @Parameter(name = "密码", description = "请输入密码", schema = @Schema(minLength = 3, maxLength = 10))
    ...
    String password
);
java 复制代码
public class UserRegisterDTO {

    ...
    @Schema(description = "年龄", maximum = "60")
    private Integer age;
    
    ...
    @Schema(description = "手机号", pattern = PatternConst.MOBILE)
    private String mobileNo;

    ...
}

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

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

api分组

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

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

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

yaml 复制代码
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文档页面了:

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

相关推荐
.生产的驴6 分钟前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
琴智冰24 分钟前
SpringBoot
java·数据库·spring boot
爱码少年25 分钟前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
《源码好优多》43 分钟前
基于SpringBoot+Vue+Uniapp的植物园管理小程序系统(2024最新,源码+文档+远程部署+讲解视频等)
vue.js·spring boot·uni-app
计算机学姐1 小时前
基于微信小程序的调查问卷管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
弥琉撒到我3 小时前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
2401_857622668 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589368 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没9 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥10 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring