SpringBoot3.x使用Swagger

2023-11-11  本文已影响0人  言午日尧耳总

SpringBoot3.x使用Swagger

设置

依赖(pom.xml)

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>
<!--  搭配校验使用,使用与SpringBoot相同的版本号  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>3.1.5</version>
</dependency>
<!--  SpringBoot  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.5</version>
</dependency>
<!-- 让响应结果更美观 -->
<dependency>
    <groupId>com.alibaba.cola</groupId>
    <artifactId>cola-component-dto</artifactId>
    <version>4.3.2</version>
</dependency>
<!--  lombok  -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.26</version>
    <optional>true</optional>
</dependency>
<!--  日志  -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

环境配置(application.yml)

springdoc:
  api-docs:
    enabled: true # 开启OpenApi接口
    path: /user-service/v3/api-docs  # 自定义路径,默认为 "/v3/api-docs"
  swagger-ui:
    enabled: true # 开启swagger界面,依赖OpenApi,需要OpenApi同时开启
    path: /user-service/swagger-ui/index.html # 自定义路径,默认为"/swagger-ui/index.html"
springdoc:
  api-docs:
    enabled: false # 关闭OpenApi接口
  swagger-ui:
    enabled: false # 关闭swagger界面

配置

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI swaggerOpenApi() {
        return new OpenAPI()
                .info(new Info().title("XXX平台YYY微服务")
                        .description("描述平台多牛逼")
                        .version("v1.0.0"))
                .externalDocs(new ExternalDocumentation()
                        .description("设计文档")
                        .url("https://juejin.cn/user/254742430749736/posts"));
    }
}
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public Response handleException(Exception e) {
        log.warn("未知异常", e);
        return Response.buildFailure("未知异常", e.getMessage());
    }
}

Restful接口

Post

用户类

@Data
@Schema(title = "新增用户模型")
public class UserAddCO {
    @Schema(title = "名字", example = "老王", minLength = 1, maxLength = 5)
    @NotBlank(message = "名字不能为空")
    private String name;

    @Schema(title = "年龄", example = "18", minimum = "0", maximum = "150")
    @NotNull(message = "年龄不能为空")
    @Range(min = 0, max = 150, message = "年龄在0~150之间")
    private Integer age;
//    private int age;
//    既然不能为空,为什么不使用int?
//    1. 因为int是基本类型不会为空,所以@NotNull校验无效
//    2. 类初始化时生成默认值(int默认为0),@Range中最小值包含0,所以没有age参数校验通过,不符合预期

    @Schema(title = "电话(可选)")
    private String phone;
}

请求方法

@RestController
@RequestMapping("/demo")
@Tag(name = "示例控制器", description = "演示Restful接口")
public class DemoController {
    @PostMapping("/")
    @Operation(summary = "Post方法示例", description = "Post通常用于新增")
    public SingleResponse<Long> add(@Validated @RequestBody UserAddCO user) {
        // TODO:添加到数据库中,然后返回记录id
        return SingleResponse.of(1L);
    }
}

异常处理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public Response handleBindException(BindException e) {
        // 拼接错误信息,用于多个校验不通过的错误信息拼接
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String message = allErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(";"));
        log.info("参数校验不通过:{}", message);
        return Response.buildFailure("参数校验不通过", message);
    }

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageConversionException.class)
    public Response handleHttpMessageConversionException(HttpMessageConversionException e) {
        log.info("参数转换失败:{}", e.getMessage());
        return Response.buildFailure("参数转换失败", e.getMessage());
    }
}

Post(FormData)

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "上传文件示例")
    public SingleResponse<String> upload(@RequestPart("file") MultipartFile file) {
        log.info("接收到文件:{}, 大小:{}", file.getOriginalFilename(), file.getSize());
        try {
            // 保存到本地
            File localTempFile = new File(file.getOriginalFilename());
            localTempFile.createNewFile();
            file.transferTo(localTempFile);
            return SingleResponse.of(localTempFile.getAbsolutePath());
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败");
        }
    }

    @PostMapping(value = "/upload/multi", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "上传多个文件示例")
    public MultiResponse<String> uploadMulti(@RequestPart("files") MultipartFile[] files) {
        List<String> result = new LinkedList<>();
        for (MultipartFile file : files) {
            log.info("接收到文件:{}, 大小:{}", file.getOriginalFilename(), file.getSize());
            result.add(file.getOriginalFilename());
            // TODO:如上保存到本地
        }
        return MultiResponse.of(result);
    }

Get(分页)

用户详情类(响应结果)

@Data
@Schema(title = "用户详情")
public class UserDetailCO {
    @Schema(title = "用户id", minimum = "1")
    private long id;

    @Schema(title = "用户名", minLength = 1, maxLength = 5, example = "言午日尧耳总")
    private String name;

    @Schema(title = "手机")
    private String phone;
}

请求分页类

@Data
public class UserPageQry {
    @Parameter(required = true, description = "页码,从1开始", example = "1")
    @Min(value = 1, message = "pageIndex必须大于1")
    private int pageIndex;
//    @NotNull(message="pageIndex参数必须填写")
//    private Integer pageIndex;
//    1. 此处可以使用int类型,因为限制最小值为1,不填写默认赋值0,@Min校验不通过会抛出异常
//    2. 也可以写成Integer+@NotNull的组合(如下pageSize),更加语义化

    @Parameter(required = true, description = "页面大小", example = "20")
    @NotNull(message = "pageSize参数必须填写")
    @Range(min = 1, max = 100, message = "pageSize必须在1-100之间")
    private Integer pageSize;

    @Parameter(description = "搜索名字(模糊搜索),不搜索就传null或空字符")
    private String phone;
}

请求方法1(推荐)

    @GetMapping("/")
    @Operation(summary = "获取分页列表")
    public PageResponse<UserDetailCO> getPageList(@Validated @ParameterObject UserPageQry qry) {
        log.info("{}", qry);
        List<UserDetailCO> result = List.of(new UserDetailCO());
        return PageResponse.of(result, 1, qry.getPageSize(), qry.getPageIndex());
    }

请求方法2(不推荐,没有方法1优雅)

@Slf4j
@Validated // 控制器加上改注解到才能进行校验
@RestController
@RequestMapping("/demo")
@Tag(name = "示例控制器", description = "演示Restful接口")
public class DemoController {
    @GetMapping("/other/")
    @Operation(summary = "另一种获取分页列表方式")
    @Parameter(name = "pageIndex", description = "页码,从1开始", example = "1")
    @Parameter(name = "pageSize", description = "数量,不小于1", example = "20")
    @Parameter(name = "name", description = "搜索名字(模糊搜索),不搜索就传null或空字符")
    public PageResponse<UserDetailCO> getPage(@NotNull(message = "pageIndex不能为空") @Min(value = 1, message = "pageIndex必须大于1") Integer pageIndex,
                                              @NotNull(message = "pageIndex不能为空") @Range(min = 1, max = 100, message = "pageSize必须在1-100之间") Integer pageSize,
                                              @Nullable String name) {
        log.info("{} {} {}", pageIndex, pageSize, name);
        List<UserDetailCO> result = List.of(new UserDetailCO());
        return PageResponse.of(result, 1, pageSize, pageIndex);
    }
}
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ValidationException.class)
    public Response handleValidationException(ValidationException e) {
        log.info("参数验证失败: {}", e.getMessage());
        return Response.buildFailure("参数验证失败", e.getMessage());
    }

Get(一条记录详情)

    @GetMapping("/{id}/")
    @Operation(summary = "获取用户详情")
    public SingleResponse<UserDetailCO> getUser(@PathVariable("id") long id) {
        log.info("{}", id);
        return SingleResponse.of(new UserDetailCO());
    }

Put/Patch

    @PutMapping("/{id}/")
    @Operation(summary = "修改参数")
    public Response putUser(@PathVariable("id") long id, @Validated @RequestBody UserPutCO user) {
        // TODO:使用id在数据查找到用户,然后修改值并保存
        return Response.buildSuccess();
    }

    @PatchMapping("/{id}/")
    @Operation(summary = "修改部分参数")
    public Response patchUser(@PathVariable("id") long id, @Validated @RequestBody UserPatchCO user) {
        // TODO:使用id在数据查找到用户,然后修改值并保存
        return Response.buildSuccess();
    }

Delete

    @DeleteMapping("/{id}/")
    @Operation(summary = "删除")
    public Response deleteUser(@PathVariable("id") long id) {
        // TODO:根据id直接删除用户
        return Response.buildSuccess();
    }

其他

屏蔽过滤器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 除了swagger和登录,其他全部拦截验证jwt
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/**/*.html",
                        "/**/*.js",
                        "/**/*.css",
                        "/**/*.woff",
                        "/**/*.ttf",
                        "/**/*.js",
                        "/**/*.map",
                        "/**/*.png",
                        "/v3/api-docs", // 如果配置里改了,这里也记得修改
                        "/v3/api-docs/swagger-config",
                        "/auth/login"); // 登录接口
    }
}

不同风格注解

@Tag(name = "User", description = "用户管理")
@RestController
@RequestMapping("/user")
public class UserController {
    // 这个样式看起来更整齐
    @Operation(summary = "获取用户信息", description = "用户详情")
    @Parameter(name = "id", description = "用户id")
    @ApiResponse(responseCode = "200", description = "用户信息")
    @GetMapping("/{id}")
    public Resp<?> detail(@PathVariable("id") Integer id) {
        User user = new User();
        return new Resp<>(user);
    }

    @Operation(summary = "新增", description = "新增用户")
    @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "用户")
    @ApiResponse(responseCode = "201", description = "成功")
    @PostMapping("/")
    public Resp<User> add(@RequestBody User user) {
        return new Resp<>(user);
    }

    // 写到一起的示例
    @Operation(summary = "获取列表",
            description = "获取用户列表",
            parameters = {@Parameter(name = "page", description = "页码"),
                    @Parameter(name = "size", description = "每页数量")},
            responses = {@ApiResponse(responseCode = "200", description = "用户列表")})
    @GetMapping("/")
    public Resp<List<User>> list(@PathParam("page") Integer page, @PathParam("size") Integer size) {
        return new Resp<>(new ArrayList<>());
    }
}

使用Apifox自测

完整示例代码

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI swaggerOpenApi() {
        return new OpenAPI()
                .info(new Info().title("XXX平台YYY服务")
                        .description("描述平台多牛逼")
                        .version("v0.0.1"))
                .externalDocs(new ExternalDocumentation()
                        .description("设计文档")
                        .url("https://juejin.cn/user/254742430749736/posts"));
    }
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public Response handleException(Exception e) {
        log.warn("未知异常", e);
        return Response.buildFailure("未知异常", e.getMessage());
    }

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public Response handleBindException(BindException e) {
        // 拼接错误信息,用于多个校验不通过的错误信息拼接
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String message = allErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(";"));
        log.info("参数校验不通过:{}", message);
        return Response.buildFailure("参数校验不通过", message);
    }

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageConversionException.class)
    public Response handleHttpMessageConversionException(HttpMessageConversionException e) {
        log.info("参数转换失败:{}", e.getMessage());
        return Response.buildFailure("参数转换失败", e.getMessage());
    }

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ValidationException.class)
    public Response handleValidationException(ValidationException e) {
        log.info("参数验证失败: {}", e.getMessage());
        return Response.buildFailure("参数验证失败", e.getMessage());
    }
}
@Slf4j
@Validated
@RestController
@RequestMapping("/demo")
@Tag(name = "示例控制器", description = "演示Restful接口")
public class DemoController {
    @PostMapping("/")
    @Operation(summary = "Post方法示例", description = "Post通常用于新增")
    public SingleResponse<Long> add(@Validated @RequestBody UserAddCO user) {
        // TODO:添加到数据库中,然后返回记录id
        return SingleResponse.of(1L);
    }

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "上传文件示例")
    public SingleResponse<String> upload(@RequestPart("file") MultipartFile file) {
        log.info("接收到文件:{}, 大小:{}", file.getOriginalFilename(), file.getSize());
        try {
            // 保存到本地
            File localTempFile = new File(file.getOriginalFilename());
            localTempFile.createNewFile();
            file.transferTo(localTempFile);
            return SingleResponse.of(localTempFile.getAbsolutePath());
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败");
        }
    }

    @PostMapping(value = "/upload/multi", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "上传多个文件示例")
    public MultiResponse<String> uploadMulti(@RequestPart("files") MultipartFile[] files) {
        List<String> result = new LinkedList<>();
        for (MultipartFile file : files) {
            log.info("接收到文件:{}, 大小:{}", file.getOriginalFilename(), file.getSize());
            result.add(file.getOriginalFilename());
            // TODO:如上保存到本地
        }
        return MultiResponse.of(result);
    }

    @GetMapping("/")
    @Operation(summary = "获取分页列表")
    public PageResponse<UserDetailCO> getPageList(@Validated @ParameterObject UserPageQry qry) {
        log.info("{}", qry);
        List<UserDetailCO> result = List.of(new UserDetailCO());
        return PageResponse.of(result, 1, qry.getPageSize(), qry.getPageIndex());
    }

    @GetMapping("/other/")
    @Operation(summary = "另一种获取分页列表方式")
    @Parameter(name = "pageIndex", description = "页码,从1开始", example = "1")
    @Parameter(name = "pageSize", description = "数量,不小于1", example = "20")
    @Parameter(name = "name", description = "搜索名字(模糊搜索),不搜索就传null或空字符")
    public PageResponse<UserDetailCO> getPage(@NotNull(message = "pageIndex不能为空") @Min(value = 1, message = "pageIndex必须大于1") Integer pageIndex,
                                              @NotNull(message = "pageIndex不能为空") @Range(min = 1, max = 100, message = "pageSize必须在1-100之间") Integer pageSize,
                                              @Nullable String name) {
        log.info("{} {} {}", pageIndex, pageSize, name);
        List<UserDetailCO> result = List.of(new UserDetailCO());
        return PageResponse.of(result, 1, pageSize, pageIndex);
    }

    @GetMapping("/{id}/")
    @Operation(summary = "获取用户详情")
    public SingleResponse<UserDetailCO> getUser(@PathVariable("id") long id) {
        // TODO:从数据库查询
        return SingleResponse.of(new UserDetailCO());
    }

    @PutMapping("/{id}/")
    @Operation(summary = "修改参数")
    public Response putUser(@PathVariable("id") long id, @Validated @RequestBody UserPutCO user) {
        // TODO:使用id在数据查找到用户,然后修改值并保存
        return Response.buildSuccess();
    }

    @PatchMapping("/{id}/")
    @Operation(summary = "修改部分参数")
    public Response patchUser(@PathVariable("id") long id, @Validated @RequestBody UserPatchCO user) {
        // TODO:使用id在数据查找到用户,然后修改值并保存
        return Response.buildSuccess();
    }

    @DeleteMapping("/{id}/")
    @Operation(summary = "删除")
    public Response deleteUser(@PathVariable("id") long id) {
        // TODO:根据id直接删除用户
        return Response.buildSuccess();
    }
}
@Data
@Schema(title = "新增用户模型")
public class UserAddCO {
    @Schema(title = "名字", example = "老王", minLength = 1, maxLength = 5)
    @NotBlank(message = "名字不能为空")
    private String name;

    @Schema(title = "年龄", example = "18", minimum = "0", maximum = "150")
    @NotNull(message = "年龄不能为空")
    @Range(min = 0, max = 150, message = "年龄在0~150之间")
    private Integer age;
//    private int age;
//    既然不能为空,为什么不使用int?
//    1. 因为int是基本类型不会为空,所以@NotNull校验无效
//    2. 类初始化时生成默认值(int默认为0),@Range中最小值包含0,所以没有age参数校验通过,不符合预期

    @Schema(title = "电话(可选)")
    private String phone;
}
@Data
@Schema(title = "用户详情")
public class UserDetailCO {
    @Schema(title = "用户id", minimum = "1")
    private long id;

    @Schema(title = "用户名", minLength = 1, maxLength = 5, example = "言午日尧耳总")
    private String name;

    @Schema(title = "手机")
    private String phone;
}
@Data
public class UserPageQry {
    @Parameter(required = true, description = "页码,从1开始", example = "1")
    @Min(value = 1, message = "pageIndex必须大于1")
    private int pageIndex;
//    @NotNull(message="pageIndex参数必须填写")
//    private Integer pageIndex;
//    1. 此处可以使用int类型,因为限制最小值为1,不填写默认赋值0,@Min校验不通过会抛出异常
//    2. 也可以写成Integer+@NotNull的组合(如下pageSize),更加语义化

    @Parameter(required = true, description = "页面大小", example = "20")
    @NotNull(message = "pageSize参数必须填写")
    @Range(min = 1, max = 100, message = "pageSize必须在1-100之间")
    private Integer pageSize;

    @Parameter(description = "搜索名字(模糊搜索),不搜索就传null或空字符")
    private String phone;
}
@Data
@Schema(title = "新增用户模型")
public class UserPatchCO {
    @Schema(title = "电话(可选)")
    private String phone;
}
@Data
@Schema(title = "新增用户模型")
public class UserPutCO {
    @Schema(title = "名字", example = "老王", minLength = 1, maxLength = 5)
    @NotBlank(message = "名字不能为空")
    private String name;

    @Schema(title = "年龄", example = "18", minimum = "0", maximum = "150")
    @NotNull(message = "年龄不能为空")
    @Range(min = 0, max = 150, message = "年龄在0~150之间")
    private Integer age;

    @Schema(title = "电话(可选)")
    private String phone;
}
上一篇 下一篇

猜你喜欢

热点阅读