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注解。这样我们只要维护配置文件即可。大家加油!

相关推荐
fu15935745684 小时前
sealos部署Java后端(若依为例)
spring boot
( •̀∀•́ )9204 小时前
Spring Boot 启动报错 `BindException: Permission denied`
java·spring boot·后端
杰克尼4 小时前
苍穹外卖--day10
java·数据库·spring boot·mybatis·notepad++
Darkdreams6 小时前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端
bropro6 小时前
【Spring Boot】Spring AOP中的环绕通知
spring boot·后端·spring
lhbian6 小时前
【Spring Cloud Alibaba】基于Spring Boot 3.x 搭建教程
java·spring boot·后端
luom01026 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
2401_895521349 小时前
springboot集成onlyoffice(部署+开发)
java·spring boot·后端
xuboyok29 小时前
【Spring Boot】统一数据返回
java·spring boot·后端
gp32102610 小时前
什么是Spring Boot 应用开发?
java·spring boot·后端