02-OpenApi

0. summary

  • Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务
  • 它使得前后端分离开发更加方便,有利于团队协作

1. 发展

1. Swagger 1.x(2011-2014年)

  • 起始与定位:Swagger最初由Tony Tam在2011年创建,旨在作为一个简单的API文档生成工具
  • 核心功能:通过对JAX-RS和Jersey注解的支持,Swagger 1.x能够自动生成API文档,使得API文档的维护变得更加容易。在这个阶段,Swagger还没有完全成熟,主要支持基本的API描述和文档生成

2. Swagger 2.x(2014-2017年)

  • 重大变革:Swagger 2.x发生了重大变化,从单一的文档生成工具演变为一个完整的API开发和管理平台
    • 引入了强大的注解支持,可以描述API的细节信息。eg:请求参数、返回类型等
    • 定义了RESTful API的元数据。eg:API描述、标签等
    • 引入了OpenAPI规范(原名Swagger规范),为API定义提供了更严格的标准和规则

3. OpenAPI(2017年至今)

  • 被称为Swagger 3.x
  • 规范更名:在2017年,Swagger 2.x的规范被捐赠给Linux基金会,并正式更名为OpenAPI规范
  • 发展与普及:OpenAPI规范不仅继承了Swagger 2.x的特性,还提供了更加全面和严格的API定义规范,并且扩展了对非RESTful API的支持。随着OpenAPI规范的普及,越来越多的API开发者开始使用Swagger/OpenAPI来开发、测试和文档化他们的RESTful API
  • 工具与服务:OpenAPI规范采用JSON或YAML格式编写,并支持多种数据类型。基于OpenAPI规范,开发了许多工具和服务
    • eg:Swagger UISwagger CodegenSwaggerHub等,进一步扩展了Swagger的功能,使其成为了一个更加完整、强大和易于使用的API定义和管理平台

2. 版本介绍

  • Springdoc OpenAPI 1.x
    • JDK支持:支持JDK 8及以上版本
    • SpringBoot支持:适用于Spring Boot 2.x及更早版本
  • Springdoc OpenAPI 2.x
    • JDK支持:最新版本要求JDK 11及以上
    • SpringBoot支持:专为Spring Boot 3.x设计

1. pom.xml

  • 注释掉
<!-- swagger3-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.6.2</version>
</dependency>
  • 新增
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.7.0</version>
</dependency>

2. config

@Configuration
public class OpenApiConfig extends WebMvcConfigurerAdapter {

    /**
     * 添加类型转换器。处理 application/x-www-form-urlencoded 转化 LocalDate, LocalDateTime
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // yyyy-MM-dd
        registry.addConverter(new LocalDateConverter(DatePattern.NORM_DATE_PATTERN));
        // yyyy-MM-dd'T'HH:mm:ss.SSS'Z' =>(OpenApi默认example格式)
        registry.addConverter(new LocalDateTimeConverter(DatePattern.UTC_MS_PATTERN));
        // yyyy-MM-dd HH:mm:ss
        // registry.addConverter(new LocalDateTimeConverter(DatePattern.NORM_DATETIME_PATTERN));
    }

    /**
     * 对接口进行分组。指定包、注解筛选方法
     */
    @Bean
    public GroupedOpenApi userApi() {
        return GroupedOpenApi.builder()
                .group("UserApi")
                .packagesToScan("com.ruoyi.open_api")
                // 注解在接口上不生效
                .addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
                .build();
    }

    @Bean
    public GroupedOpenApi courseApi() {
        return GroupedOpenApi.builder()
                .group("CourseApi")
                .packagesToScan("com.ruoyi.course.controller")
                // .addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
                .build();
    }

    @Bean
    public OpenAPI openApi() {
        return new OpenAPI()
                .info(new Info()
                        .title("Swagger Petstore - OpenAPI 3.0")
                        .description("This is a sample Pet Store Server based on the OpenAPI 3.0 specification.  You can find out more about Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! You can now help us improve the API whether it's by making changes to the definition itself or to the code. That way, with time, we can improve the API in general, and expose some of the new features in OAS3.  _If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_  Some useful links: - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)")
                        .termsOfService("")
                        .version("1.0.11")
                        .license(new License()
                                .name("Apache 2.0")
                                .url("http://www.apache.org/licenses/LICENSE-2.0.html"))
                        .contact(new Contact()
                                .email("apiteam@swagger.io"))
                )
                // 服务器配置
                .servers(Arrays.asList(
                        new Server().url("http://localhost:7071").description("本地环境"),
                        new Server().url("http://listao.cn:20015/prod-api").description("生产环境")
                ));
    }

}

/**
 * application/x-www-form-urlencoded方式,String转LocalDate
 */
class LocalDateConverter implements Converter<String, LocalDate> {
    private final DateTimeFormatter formatter;

    public LocalDateConverter(String dateFormat) {
        this.formatter = DateTimeFormatter.ofPattern(dateFormat);
    }

    @Override
    public LocalDate convert(String source) {
        if (source.isEmpty()) {
            return null;
        }
        return LocalDate.parse(source, this.formatter);
    }
}

/**
 * application/x-www-form-urlencoded方式,String转LocalDateTime
 */
class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
    private final DateTimeFormatter formatter;

    public LocalDateTimeConverter(String dateFormat) {
        this.formatter = DateTimeFormatter.ofPattern(dateFormat);
    }

    @Override
    public LocalDateTime convert(String source) {
        if (source.isEmpty()) {
            return null;
        }
        return LocalDateTime.parse(source, this.formatter);
    }
}











 










 
 
 
 



























 
 
 
 







 


















 














image-20240810235849309

3. yml

springdoc:
  api-docs:
    path: /api-docs
  swagger-ui:
    # 本地ui无法访问,默认的访问地址
    disable-swagger-default-url: true

  # 可代替config中GroupedOpenApi,不能通过方法注解进行筛选
  group-configs:
    - group: UserApi
      packages-to-scan: com.ruoyi.open_api
      paths-to-match: /api/user/*
    - group: CourseApi
      paths-to-match: /api/course/*
      packages-to-scan: com.ruoyi.course.controller


 


 









image-20240810194136547

4. 页面集成

  • 直接带端口访问后端:http://localhost:7071/swagger-ui/index.html
image-20240810195525085
image-20240810194712701
image-20240810194922045
  • 页面进行了重定向,不再请求本地后端
image-20240810194323555

1. 本地前端代理服务器

  • 修改本地前端,代理服务器配置
// vite 相关配置
server: {
  port: 80,
  host: true,
  open: true,
  proxy: {
    // https://cn.vitejs.dev/config/#server-proxy
    '/dev-api': {
      target: 'http://localhost:7071',
      // target: 'http://listao.cn:20015/prod-api',
      changeOrigin: true,
      rewrite: (p) => p.replace(/^\/dev-api/, '')
    },
    '/api-docs': {
      target: 'http://localhost:7071',
      // target: 'http://listao.cn:20015/prod-api',
      changeOrigin: true,
      // rewrite: (p) => p.replace(/^\/dev-api/, '')
    }
  }
},













 
 
 
 
 
 


2. 正式环境nginx

# RuoYi官网的配置
server {
    listen 18082;
    # 指定前端项目所在的位置
    location / {
        root /etc/nginx/ruoyi;
        try_files $uri $uri/ /index.html;
        index index.html index.htm;
    }

    error_page 500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }

    location /prod-api/ {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://ruoyi:7071/;
    }

    location /api-docs/ {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://ruoyi:7071;
    }

}























 
 
 
 
 
 
 


5. 规避SwaggerUI问题

1. SwaggerUI传null

  • 删除example即向后端传递空String
  • 再取消 Send empty value 勾选,即null
image-20240810171431782

2. 和校验冲突

  • 插入、更新id处理
    • 关闭required
    • 进行分组校验
    • 接口描述:新增不为空, 更新为空
@NotBlank(message = "id为空", groups = Validd.upd.class)
@Null(message = "id不为空", groups = Validd.ins.class)
@Schema(example = "10", description = "主键, 新增不为空, 更新为空",
        minimum = "1", maximum = "33",
        // 为了区分新增、更新,关闭`required`
        requiredMode = Schema.RequiredMode.NOT_REQUIRED,
        nullable = true
)
@JsonProperty("id")
private String id;
 
 



 




3. 日期时间处理

1. @RequestBody

  • application/json:入参、出参,都进行控制
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

2. @RequestParam

  • application/x-www-form-urlencoded:入参进行控制,LocalDate, LocalDateTime
@Configuration
public class OpenApiConfig extends WebMvcConfigurerAdapter {

    /**
     * 添加类型转换器。处理 application/x-www-form-urlencoded 转化 LocalDate, LocalDateTime
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // yyyy-MM-dd
        registry.addConverter(new LocalDateConverter(DatePattern.NORM_DATE_PATTERN));
        // yyyy-MM-dd'T'HH:mm:ss.SSS'Z' =>(OpenApi默认example格式)
        registry.addConverter(new LocalDateTimeConverter(DatePattern.UTC_MS_PATTERN));
        // yyyy-MM-dd HH:mm:ss
        // registry.addConverter(new LocalDateTimeConverter(DatePattern.NORM_DATETIME_PATTERN));
    }

}

/**
 * application/x-www-form-urlencoded方式,String转LocalDate
 */
class LocalDateConverter implements Converter<String, LocalDate> {
    private final DateTimeFormatter formatter;

    public LocalDateConverter(String dateFormat) {
        this.formatter = DateTimeFormatter.ofPattern(dateFormat);
    }

    @Override
    public LocalDate convert(String source) {
        if (source.isEmpty()) {
            return null;
        }
        return LocalDate.parse(source, this.formatter);
    }
}

/**
 * application/x-www-form-urlencoded方式,String转LocalDateTime
 */
class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
    private final DateTimeFormatter formatter;

    public LocalDateTimeConverter(String dateFormat) {
        this.formatter = DateTimeFormatter.ofPattern(dateFormat);
    }

    @Override
    public LocalDateTime convert(String source) {
        if (source.isEmpty()) {
            return null;
        }
        return LocalDateTime.parse(source, this.formatter);
    }
}









 

 









 


















 














3. example

  • 只能以这种格式进行示例回显
@Future(message = "日期必须为将来日期")
// 只为进行example回显
@Schema(example = "2099-09-09T06:32:52.415Z", description = "将来日期")
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime futureDate;


 


image-20240811091416004

4. 嵌套对象

// 嵌套对象必须`@Valid`修饰
@Valid
@Schema(description = "嵌套对象", nullable = true)
private InObj inObj;

 


6. ctl

@Tag(name = "CRUD")
@RestController
@RequestMapping("/openApiCtl")
public class OpenApiCtl {
    private final static Map<String, OpenApi> users = new LinkedHashMap<>();

    {
        users.put("1", new OpenApi("1", "name1"));
        users.put("2", new OpenApi("2", "name2"));
    }

    // @Operation(summary = "查询user集合", tags = {"ooxx"})
    @Operation(summary = "查询user集合")
    @GetMapping("/list")
    public R<List<OpenApi>> userList() {
        List<OpenApi> userList = new ArrayList<>(users.values());
        return R.ok(userList);
    }

    @Operation(summary = "userId查询user")
    @GetMapping("/{userId}")
    public R<OpenApi> getUser(@PathVariable Integer userId) {
        return R.ok(users.get(userId));
    }

    /**
     * 分组校验 = ins分组 + default分组
     */
    @Operation(summary = "新增user", description = "inObj置null")
    @PostMapping(value = "/save", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public R<OpenApi> save(@Parameter @Validated({Validd.ins.class, Default.class}) OpenApi user) {
        users.put(user.getId(), user);
        return R.ok(user);
    }

    /**
     * 分组校验 = upd分组 + default分组
     */
    @Operation(summary = "更新user", description = "id不为空,其他属性非null更新")
    @PutMapping("/update")
    public R<OpenApi> update(@RequestBody @Validated({Validd.upd.class, Default.class}) OpenApi user) {
        users.remove(user.getId());
        users.put(user.getId(), user);
        return R.ok(user);
    }

    @Operation(summary = "删除user")
    @DeleteMapping("/{userId}")
    public R<String> delete(@PathVariable Integer userId) {
        users.remove(userId);
        return R.ok();
    }

}

7. pojo

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpenApi {

    @NotBlank(message = "id为空", groups = Validd.upd.class)
    @Null(message = "id不为空", groups = Validd.ins.class)
    @Schema(example = "10", description = "主键, 新增不为空, 更新为空",
            minimum = "1", maximum = "33",
            // 为了区分新增、更新
            requiredMode = Schema.RequiredMode.NOT_REQUIRED,
            nullable = true
    )
    @JsonProperty("id")
    private String id;

    @NotBlank(message = "username为空", groups = Validd.ins.class)
    @Schema(example = "Danny", description = "user名称",
            requiredMode = Schema.RequiredMode.REQUIRED,
            minLength = 2, maxLength = 33
    )
    @JsonProperty("username")
    private String username;

    @Schema(example = "John", description = "名")
    @JsonProperty("firstName")
    private String firstName;

    @Schema(example = "James", description = "姓")
    @JsonProperty("lastName")
    private String lastName;

    @NotBlank(message = "邮箱不能为空", groups = Validd.ins.class)
    @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确")
    @Schema(example = "john@email.com", description = "邮箱")
    @JsonProperty("email")
    private String email;

    @NotBlank(message = "密码不能为空", groups = Validd.ins.class)
    @Size(min = 6, max = 16, message = "密码长度必须在6到16个字符之间")
    @Schema(example = "******", description = "密码")
    @JsonProperty("password")
    private String password;

    @NotBlank(message = "mobile不能为空", groups = Validd.ins.class)
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "无效的手机号码格式")
    @Schema(example = "19910769053", description = "手机号")
    @JsonProperty("mobile")
    private String mobile;

    @Schema(example = "1", description = "用户状态")
    @JsonProperty("userStatus")
    private Integer userStatus;

    @URL(message = "url格式错误")
    @Schema(example = "http://listao.site", description = "网站地址")
    private String url;

    @Digits(integer = 4, fraction = 2, message = "整数位数必须在4位以内小数位数必须在2位以内")
    @Schema(description = "小数", example = "1.3")
    private Double num;

    @Past(message = "日期必须为过去日期")
    @Schema(example = "2023-12-12", description = "过去日期")
    private LocalDate pastDate;

    @Future(message = "日期必须为将来日期")
    // 只为进行example回显
    @Schema(example = "2099-09-09T06:32:52.415Z", description = "将来日期")
    // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime futureDate;

    // 嵌套对象必须`@Valid`修饰
    @Valid
    @Schema(description = "嵌套对象", nullable = true)
    private InObj inObj;

    public OpenApi(String id, String username) {
        this.id = id;
        this.username = username;
    }
}

@Data
class InObj {
    @NotBlank(message = "oxId不能为空")
    @Schema(description = "oxId主键", example = "ooxxId")
    private String id;
}