当前位置:Java -> 扩展Swagger和Springdoc Open API

扩展Swagger和Springdoc Open API

Java的应用已经从1.8版本转移到至少Java 17。与此同时,Spring Boot已经从2.x版本进化到3.2.2版本。springdoc项目已经从较旧的库“springdoc-openapi-ui”转换到“springdoc-openapi-starter-webmvc-ui”以实现其功能。这些更新意味着依赖于较旧文章的读者可能会发现自己在这些技术方面已经滞后多年。作者已更新本文以确保读者使用最新版本,不会在迁移期间受到过时信息的困扰。


在我的最近的文章——使用Spring Boot进行OpenAPI 3文档编写使用springdoc-openapi进行更多操作 中,我们尝试了一个Spring Boot Open API 3启用的REST项目,并探讨了一些其功能,包括:

  • 自动JSR-303相关的Swagger文档
  • Maven构建属性如何在Swagger文档中显示为项目信息
  • 在生成的Swagger文档中呈现完全合格的名称
  • 使用控制器建议全局异常处理及其相关的Swagger文档

我们还讨论了在未来的springdoc-openapi版本中:

  • 实现FQN可能会更加容易(已实现)。
  • springdoc-openapi将更灵活地处理@ControlerAdvice相关的文档(已实现)。

以前,我们在其他细节中看到了如何利用了一些JSR 303注释。我们还注意到有些注解被忽略了,例如:javax.validation.constraints.Emailorg.hibernate.validator.constraints.CreditCardNumber

新的目标

为输入和输出指定详细约定对于任何API都非常重要。如果我们能扩展Swagger的行为并通过其自动化文档传达有关这些附加注释和自定义验证注释的信息,那不是很好吗?

让我们来探索一下。让我们尽可能简化代码。我们将从零开始编写足够的代码来实现我们的目标。

我们不会重复上次已经详细讲解的异常处理和控制器建议概念(只是为了尽可能简化本文的代码)。

与以前一样,我们将参考构建RESTful Web服务springdoc-openapi v2.5.0

我还要感谢Springdoc的Badr Nass Lahsen对本文和代码进行审查。

先决条件

  • Java 17.x
  • Maven 3.x
  • 在IDE中安装Lombok(如果使用IDE的话)

步骤

首先创建一个Maven JAR项目。下面是要使用的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xmlns="http://maven.apache.org/POM/4.0.0"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.2</version>
		<relativePath ></relativePath> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sample</artifactId>
	<version>0.0.1</version>
	<name>sample</name>
	<description>Demo project for Spring Boot with openapi 3 documentation</description>

	<properties>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.5.0</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>


springdoc-openapi-starter-webmvc-ui" 依赖。

如果使用Eclipse IDE,在使用上述内容创建了pom.xml后,可能需要对项目进行Maven更新(右键单击 项目  - Maven > 更新项目)。

现在,让我们创建一个与以前文章类似的小Java bean类。

package sample.model;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;

import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import sample.customvalidations.DateTimeType;
import sample.customvalidations.LocalDateTimeFormat;

@Data
@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {

	private long id;

	@Size(min = 2)
	private String firstName;

	@NotNull
	@NotBlank
	private String lastName;

	@Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
	private String email;

	@Email()
	private String email1;

	@Min(18)
	@Max(30)
	private int age;

	@CreditCardNumber
	private String creditCardNumber;

	@LocalDateTimeFormat(pattern = "yyyyMMdd", dateTimeType = DateTimeType.Date, message = "Invalid dateTimeField Format. It Should be in yyyyMMdd format")
	private String registrationDate;

}


Person.java 可能会提及 sample.customvalidations.DateTimeTypesample.customvalidations.LocalDateTimeFormat。随着我们进行步骤,我们将稍后添加这些类。

registrationDate" 字段,只是为了演示自定义验证器。

现在,让我们创建一个控制器。

package sample.controller;

import jakarta.validation.Valid;

import sample.model.Person;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;

@RestController 
public class PersonController {

	@PostMapping(path = "/person", consumes = { MediaType.APPLICATION_JSON_VALUE, 
			MediaType.APPLICATION_XML_VALUE })
	@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(examples = {
			@ExampleObject(value = INVALID_REQUEST, name = "invalidRequest", description = "Invalid Request"),
			@ExampleObject(value = VALID_REQUEST, name = "validRequest", description = "Valid Request") }))
	public Person person(@Valid @RequestBody Person person) {
		return person;
	}
	
	private static final String VALID_REQUEST = """
			{
			  "id": 0,
			  "firstName": "string",
			  "lastName": "string",
			  "email": "abc@abc.com",
			  "email1": "abc@abc.com",
			  "age": 20,
			  "creditCardNumber": "4111111111111111",
			  "registrationDate": "20211231"
			}""";

	private static final String INVALID_REQUEST = """
			{
			  "id": 0,
			  "firstName": "string",
			  "lastName": "string",
			  "email": "abcabc.com",
			  "email1": "abcabc.com",
			  "age": 17,
			  "creditCardNumber": "411111111111111",
			  "registrationDate": "string"
			}""";
}


让我们在src\main\resources\application.properties中做一些条目。请相应地创建文件。

application-description=@project.description@
application-version=@project.version@
springdoc.swagger-ui.show-extensions=true
springdoc.swagger-ui.show-common-extensions=true
server.error.include-message=always
server.error.include-binding-errors=always
springdoc.use-fqn=true


application-description 和 application-version 条目将将Maven构建相关的信息传递到OpenAPI文档。

package sample.config;

import io.swagger.v3.oas.models.OpenAPI;
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
class OpenApiConfig {

	@Bean
	public OpenAPI customOpenAPI(@Value("${application-description}") String appDesciption, 
			@Value("${application-version}") String appVersion) {
		return new OpenAPI()
				.info(new Info()
						.title("sample application API")
						.version(appVersion)
						.description(appDesciption)
						.termsOfService("http://swagger.io/terms/")
						.license(new License().name("Apache 2.0")
								.url("http://springdoc.org")));
	}

}


让我们编写Spring Boot应用程序类。

package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}

}


让我们添加一些额外的代码来帮助演示自定义验证器。

package sample.customvalidations;

public enum DateTimeType {
	DateTime,
	Date,
	Time
}


package sample.customvalidations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
		ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = LocalDateTimeValidator.class)
@Documented
public @interface LocalDateTimeFormat {

	String message() default "{message.key}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String pattern();

	DateTimeType dateTimeType() default DateTimeType.DateTime;

}


package sample.customvalidations;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class LocalDateTimeValidator implements ConstraintValidator<LocalDateTimeFormat, String> {

	private String pattern;

	private DateTimeType dateTimeType;

	@Override
	public void initialize(LocalDateTimeFormat constraintAnnotation) {
		this.pattern = constraintAnnotation.pattern();
		this.dateTimeType = constraintAnnotation.dateTimeType();
	}

	@Override
	public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
		if (object == null || "".equals(object)) {
			return true;
		}

		try {
			DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
			if (DateTimeType.Time.equals(dateTimeType)) {
				LocalTime.parse(object, dateFormatter);
			}
			else if (DateTimeType.Date.equals(dateTimeType)) {
				LocalDate.parse(object, dateFormatter);
			}
			else {
				LocalDateTime.parse(object, dateFormatter);
			}
			return true;
		}
		catch (Exception e) {
			// e.printStackTrace();
			return false;
		}
	}
}


此时项目在Eclipse中的情况如下:

Project in Eclipse

以上是项目内容。接下来,从命令提示符或终端中执行mvn clean package。然后,执行java -jar target\sample-0.0.1.jar

您还可以通过在IDE中运行SampleApplication.java类来启动应用程序。

现在,让我们访问Swagger UI — http://localhost:8080/swagger-ui.html:

Swagger UI

展开右侧>符号下的Person,然后也展开各种属性。

展开Schemas下Person右侧的>符号和各个属性

精彩之处在于合同如何自动详细地利用模型上的JSR-303注解。开箱即用,它涵盖了许多重要的注解并对其进行了文档处理。然而,目前它不支持@javax.validation.constraints.Email@org.hibernate.validator.constraints.CreditCardNumber的开箱即用。

它也不支持我们应用于Person Java bean中的registrationDate字段的自定义注解。

@LocalDateTimeFormat(pattern = "yyyyMMdd",   dateTimeType=DateTimeType.Date, 
                     message = "Invalid dateTimeField Format. It Should be in yyyyMMdd format")
private String registrationDate;


然而,这些约束是由后端应用的。

为了完整起见,让我们发起一个请求。点击POST按钮。然后点击出现的Try it out按钮。这将带您到下面的屏幕。

发布请求

点击蓝色的Execute按钮。

执行请求

{
  "timestamp": "2024-04-07T12:49:49.106+00:00",
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed for object='person'. Error count: 5",
  "errors": [
    {
      "codes": [
        "CreditCardNumber.person.creditCardNumber",
        "CreditCardNumber.creditCardNumber",
        "CreditCardNumber.java.lang.String",
        "CreditCardNumber"
      ],
      "arguments": [
        {
          "codes": [
            "person.creditCardNumber",
            "creditCardNumber"
          ],
          "arguments": null,
          "defaultMessage": "creditCardNumber",
          "code": "creditCardNumber"
        },
        false
      ],
      "defaultMessage": "invalid credit card number",
      "objectName": "person",
      "field": "creditCardNumber",
      "rejectedValue": "411111111111111",
      "bindingFailure": false,
      "code": "CreditCardNumber"
    },
    {
      "codes": [
        "LocalDateTimeFormat.person.registrationDate",
        "LocalDateTimeFormat.registrationDate",
        "LocalDateTimeFormat.java.lang.String",
        "LocalDateTimeFormat"
      ],
      "arguments": [
        {
          "codes": [
            "person.registrationDate",
            "registrationDate"
          ],
          "arguments": null,
          "defaultMessage": "registrationDate",
          "code": "registrationDate"
        },
        "Date",
        {
          "arguments": null,
          "codes": [
            "yyyyMMdd"
          ],
          "defaultMessage": "yyyyMMdd"
        }
      ],
      "defaultMessage": "Invalid dateTimeField Format. It Should be in yyyyMMdd format",
      "objectName": "person",
      "field": "registrationDate",
      "rejectedValue": "string",
      "bindingFailure": false,
      "code": "LocalDateTimeFormat"
    },
    {
      "codes": [
        "Min.person.age",
        "Min.age",
        "Min.int",
        "Min"
      ],
      "arguments": [
        {
          "codes": [
            "person.age",
            "age"
          ],
          "arguments": null,
          "defaultMessage": "age",
          "code": "age"
        },
        18
      ],
      "defaultMessage": "must be greater than or equal to 18",
      "objectName": "person",
      "field": "age",
      "rejectedValue": 17,
      "bindingFailure": false,
      "code": "Min"
    },
    {
      "codes": [
        "Email.person.email1",
        "Email.email1",
        "Email.java.lang.String",
        "Email"
      ],
      "arguments": [
        {
          "codes": [
            "person.email1",
            "email1"
          ],
          "arguments": null,
          "defaultMessage": "email1",
          "code": "email1"
        },
        [],
        {
          "arguments": null,
          "codes": [
            ".*"
          ],
          "defaultMessage": ".*"
        }
      ],
      "defaultMessage": "must be a well-formed email address",
      "objectName": "person",
      "field": "email1",
      "rejectedValue": "abcabc.com",
      "bindingFailure": false,
      "code": "Email"
    },
    {
      "codes": [
        "Pattern.person.email",
        "Pattern.email",
        "Pattern.java.lang.String",
        "Pattern"
      ],
      "arguments": [
        {
          "codes": [
            "person.email",
            "email"
          ],
          "arguments": null,
          "defaultMessage": "email",
          "code": "email"
        },
        [],
        {
          "arguments": null,
          "codes": [
            ".+@.+\\..+"
          ],
          "defaultMessage": ".+@.+\\..+"
        }
      ],
      "defaultMessage": "Please provide a valid email address",
      "objectName": "person",
      "field": "email",
      "rejectedValue": "abcabc.com",
      "bindingFailure": false,
      "code": "Pattern"
    }
  ],
  "path": "/person"
}


很明显,从上述JSON中可以看出这些注解确实由后端应用:

  • @javax.validation.constraints.Email
  • @org.hibernate.validator.constraints.CreditCardNumber
  • @sample.customvalidationsLocalDateTimeFormat

让我们输入一个有效的输入:

{
  "id": 0,
  "firstName": "string",
  "lastName": "string",
  "email": "abc@abc.com",
  "email1": "abc@abc.com",
  "age": 20,
  "creditCardNumber": "4111111111111111",
  "registrationDate": "20211231"
}


将该有效输入放入Request body部分。(我们也可以从下拉列表中选择"validRequest",如下所示。)

将有效输入放入请求体部分

点击蓝色的Execute按钮,我们看到如下:

执行输出

到目前为止的代码可以在这里找到:

让我们重新审视我们的目标:

新目标:如果我们能够扩展Swagger的行为,并通过其自动化文档传达有关这些附加注释和自定义验证注释的信息,那不是件很好的事吗?

现在让我们来做这件事。

让我们添加两个新类。

package sample.config;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
class DateTimeFormatData {

	private String pattern;

	private String dateTimeType;

}


package sample.config;

import java.lang.annotation.Annotation;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import sample.customvalidations.LocalDateTimeFormat;

import org.springframework.context.annotation.Configuration;

@Configuration
class CustomOpenApiValidator extends ModelResolver {

	private final Class[] handledValidations = { jakarta.validation.constraints.NotNull.class,
			jakarta.validation.constraints.NotBlank.class,
			jakarta.validation.constraints.NotEmpty.class,
			jakarta.validation.constraints.Min.class,
			jakarta.validation.constraints.Max.class,
			jakarta.validation.constraints.DecimalMin.class,
			jakarta.validation.constraints.DecimalMax.class,
			jakarta.validation.constraints.Pattern.class,
			jakarta.validation.constraints.Size.class };

	private final Package[] allowedPackages = { handledValidations[0].getPackage(),
			org.hibernate.validator.constraints.CreditCardNumber.class.getPackage(),
			LocalDateTimeFormat.class.getPackage() };

	public CustomOpenApiValidator(ObjectMapper mapper) {
		super(mapper);
	}

	@Override
	protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
		super.applyBeanValidatorAnnotations(property, annotations, parent, applyNotNullAnnotations);
		if (annotations != null) {
			for (Annotation annotation : annotations) {
				Class<? extends Annotation> annotationType = annotation.annotationType();
				boolean handled = false;
				for (Class check : handledValidations) {
					if (annotationType == check) {
						handled = true;
						break;
					}
				}
				if (!handled) {
					Package annotationPackage = annotationType.getPackage();
					boolean allowed = false;
					for (Package allowedPackage : allowedPackages) {
						if (allowedPackage == annotationPackage) {
							allowed = true;
							break;
						}
					}
					if (allowed) {
						Map extensions = property.getExtensions();
						String extensionKey = "x-" + annotationType.getSimpleName();
						if (!(extensions != null && extensions.containsKey(extensionKey))) {
							Object value = describeAnnotation(annotation, annotationType);
							property.addExtension(extensionKey, value);

						}
					}
				}
			}
		}

	}

	private Object describeAnnotation(Annotation annotation, Class<? extends Annotation> annotationType) {
		Object ret = true;
		if (annotationType == LocalDateTimeFormat.class) {
			LocalDateTimeFormat format = (LocalDateTimeFormat) annotation;
			ret = new DateTimeFormatData(format.pattern(), format.dateTimeType().name());

		}
		return ret;
	}
}




请停止应用程序,然后构建并重新启动应用程序。访问http://localhost:8080/swagger-ui.html查看Swagger UI。如果您检查,您会发现模式现在显示了突出显示的扩展,用以传达额外的约束。

模式现在显示了突出显示的扩展,用以传达额外的约束

如下图所示:

模式输出

结论

我们已经展示了如何实现新目标,演示了如何使用Swagger模式扩展,并记录了本来可能未被记录的附加约束。 这包括自定义验证器。在下一部分,我们将尝试沿着同样的方向进行尝试,但会有所突破。

故障排除提示

  • 确保满足先决条件。
  • 如果使用Eclipse IDE,在创建所有文件后可能需要对项目进行Maven更新(右键单击项目 - Maven > 更新项目)。
  • 在Swagger UI中,如果无法访问“模式”定义链接,可能是因为您需要退出“试一下”模式。单击可能可见的一个或两个取消按钮。
  • 确保您对本教程使用http://localhost:8080/swagger-ui.html。
  • 另请参阅使用Lombok 在IDE中设置Lombok。

推荐阅读: 阿里巴巴面经(76)

本文链接: 扩展Swagger和Springdoc Open API