Swagger extension - generate multiple Swagger API documents for the same interface

@ApiOperationGenerate multiple different Swagger API documents for the same one .

1. Background

In the formal software development process, the requirement is to do the design first, then develop; write the document first, and then write the implementation. It's just that when facing the reality, the code is often the first, and the documentation is the need to talk about it later.

In this context, tools such as Swagger that generate documents based on code have endless vitality.

Also in actual scenarios, we often allow the same interface to implement multiple functions for some reasons, such as "following the existing standards in the industry", or "lowering the threshold for interface users to use and reducing the mental burden". To give a specific example, for some publishing services, the interfaces for initiating processes often provide the same URL entry address, and then use different parameters to initiate operations of different services or processes.

But there is no perfect thing in the world. By default, Swagger @ApiOperationgenerates a corresponding API document description, but in the above example of "publishing services, initiating processes", it is necessary to distinguish through different parameters, so that contradictions arise——Swagger's default implementation cannot allow users to distinguish which parameters need to be passed by different services/processes, and the basic verification rules for which parameters without additional explanations. (Of course, you can forcibly put all parameters in a parameter entity class, and then create different types for different service/process types as the fields of the previous parameter entity class. Composing such a hierarchical structure can indeed alleviate the above contradiction, but I believe that under this implementation, when the Swagger front-end is displayed, the scroll bar of the browser must be quite deep).

Under the above contradictions, when using this type of interface, users need to consume a lot of cost in repeated communication and confirmation. After personnel changes or after a long time, the same level of communication has to start all over again. Both users and providers are exhausted by this.

In the past, we tried to alleviate the above problems by writing documents, but the result of the separation of codes and documents is the lack of timeliness and the lack of quick verification methods.

In this paper we try to substantially alleviate this problem. With the help of Swagger extension, under the same interface, different services/processes can generate different swagger documents, in order to improve the timeliness and verifiability of interface documents, thereby greatly reducing communication costs.

2. Effect display

The following are two Swagger API documents generated by the same interface (you can see that they are the same url address, different request parameters ):

  1. Process service 1
    Process service 1
  2. Process service 2
    Process service 2

3. Realize

First, let us list some difficulties that may be encountered in the process of realizing this requirement, and then introduce the solution in a targeted manner.

  1. How to make the target interface self-explanatory "This interface needs to be generated with multiple API documents".
  2. On the basis of the first step, how to generate the Swagger internal data structure corresponding to the corresponding API document, participate in the Swagger life cycle, and minimize our workload.
  3. How to assign values ​​to the Swagger internal data structure generated in the second step. Note that the assignment here is divided into parsing assignment of input and output parameters, and other assignments such as http method. We will introduce this part in a special subsection below.
  4. How to make the finally generated and populated internal data structure of Swagger displayed normally on the front-end page. In Swagger's restful interface return value (accurately, the openapi return value specification), the uniqueness of the interface is realized by uri, but our requirement in this article is precisely to require that multiple interface documents generated have the same uri. How should this contradiction be alleviated?

In view of the above problems, let us solve them respectively.

3.1 Key logic - let the interface explain itself

Here we use the custom annotation method. Then declare that the interface needs to generate a corresponding number of API documents by marking the corresponding number of annotations on an interface, and at the same time, balance the individual needs and general processing by declaring the corresponding annotation attributes.

The relevant code is as follows:

// ============================================================ 自定义注解
// 使用Java8的"重复性注解"特性, 优化使用体验.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface FlowTypes {
    
    
	FlowType[] value();
}

// 代表一个被支持的流程类型
@Repeatable(FlowTypes.class)
@Target({
    
     ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowType {
    
    
	// 流程类型的名称
	String name();
	// 发起该流程时候, 需要传递的参数类型. 很明显, 我们建议使用自定义类型封装所有的请求参数
	Class<?> paramType();
}

// ============================================================
// 为了后面解析的方便, 这里我们还是使用了java8以前的使用方式.
// 以下就是代表该接口支持发送两类流程服务
@FlowTypes({
    
     //
		@FlowType(name = "流程服务1", paramType = FlowServiceParams1.class), //
		@FlowType(name = "流程服务2", paramType = FlowServiceParams2.class) //
})
@ApiOperation(value="createFlowService")
@ResponseBody
@PostMapping("/createFlowService")
public Result createFlowService() {
    
    
	return new Result();
}

3.2 Key logic - how to generate the corresponding ApiDescription

Here we take the implementation of the external extension interface provided by Swagger ApiListingScannerPlugin.

Go directly to the code.

@Component
public class SwaggerAddApiDescriptionPlugin implements ApiListingScannerPlugin {
    
    

	private final TypeResolver typeResolver;

  /**
   * 参考 ApiDescriptionLookup.java
   */
	private Map<RequestHandler, GeneratedApis> needDealed = new HashMap<>();
	
	public Map<RequestHandler, GeneratedApis> getNeedDealed() {
    
    
		return Collections.unmodifiableMap(needDealed);
	}	

	@Autowired
	public SwaggerAddApiDescriptionPlugin(TypeResolver typeResolver) {
    
    
		this.typeResolver = typeResolver;
	}

	@Override
	public List<ApiDescription> apply(DocumentationContext documentationContext) {
    
    
		return generateApiDesc(documentationContext);
	}

	private List<ApiDescription> generateApiDesc(final DocumentationContext documentationContext) {
    
    
		List<RequestHandler> requestHandlers = documentationContext.getRequestHandlers();
		List<ApiDescription> newArrayList = new ArrayList<>();
		requestHandlers.stream().filter(s -> s.findAnnotation(FlowTypes.class).isPresent())
				.forEach(handler -> {
    
    
					List<ApiDescription> apiDescriptions = addApiDescriptions(documentationContext, handler);
					newArrayList.addAll(apiDescriptions);
					if (!apiDescriptions.isEmpty()) {
    
    
						needDealed.put(handler, GeneratedApis.builder().ads(apiDescriptions).build());
					}					
				});
		
		return newArrayList;
	}

	private List<ApiDescription> addApiDescriptions(DocumentationContext documentationContext,
			RequestHandler handler) {
    
    
		Optional<FlowTypes> annotation = handler.findAnnotation(FlowTypes.class);
		List<ApiDescription> apiDescriptionList = new ArrayList<>();
		if (annotation.isPresent()) {
    
    
			FlowTypes FlowTypes = annotation.get();
			String tagName = FlowTypes.name();
			// 确保归类在不同的group下, 以实现相同path的共存
			Arrays.stream(FlowTypes.value()).filter(FlowType -> FlowType.name()
					.equalsIgnoreCase(documentationContext.getGroupName()))
					.forEach(FlowType -> apiDescriptionList
							.addAll(addApiDescription(handler, documentationContext, FlowType, tagName)));
		}
		return apiDescriptionList;
	}

	private List<ApiDescription> addApiDescription(RequestHandler handler,
			DocumentationContext documentationContext,
			FlowType FlowType, String tagName) {
    
    
		RequestHandlerKey requestHandlerKey = handler.key();
		final String value = FlowType.value();
		OperationBuilder operationBuilder = new OperationBuilder(new CachingOperationNameGenerator())
				.summary(value)
				.notes(value)
				.tags(CollUtil.newHashSet(tagName + "-" + value));

		final ApiDescriptionBuilder builder = new ApiDescriptionBuilder(
				documentationContext.operationOrdering());
		builder.description(value)
				.groupName(documentationContext.getGroupName())
				.hidden(false);
		List<ApiDescription> apiDescriptionList = new ArrayList<>();
		Iterator<RequestMethod> methodIterator = requestHandlerKey.getSupportedMethods().iterator();
		Iterator<String> pathIterator = requestHandlerKey.getPathMappings().iterator();
		while (methodIterator.hasNext()) {
    
    
			List<Parameter> parameters = createParameter(FlowType,
					requestHandlerKey.getSupportedMediaTypes(), operationBuilder.build().getMethod());
			// 设置参数
			operationBuilder.parameters(parameters);
      operationBuilder.uniqueId(value + IdUtil.fastUUID());
			while (pathIterator.hasNext()) {
    
    
				// 设置请求路径
				builder.path(pathIterator.next());
				List<Operation> operations = Arrays.asList(operationBuilder.build());
				apiDescriptionList.add(builder.operations(operations).build());
			}
		}
		return apiDescriptionList;
	}


	/**
	 * 解析参数
	 * @param FlowType
	 * @param consumes
	 * @param method
	 * @return
	 */
	private List<Parameter> createParameter(FlowType FlowType,
			Set<? extends MediaType> consumes, HttpMethod method) {
    
    
		final Class<?> paramType = FlowType.dataTypeClass();
		final Map<String, Field> fieldMap = ReflectUtil.getFieldMap(paramType);
		return fieldMap.entrySet().stream().map(kv -> {
    
    
			Field field = kv.getValue();
			ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
			ParameterBuilder parameterBuilder = new ParameterBuilder();
			ResolvedType resolve = typeResolver.resolve(field.getType());
			return parameterBuilder.description(annotation.value())
					//参数数据类型
					.type(resolve)
					//参数名称
					.name(field.getName())
					//参数默认值
					.defaultValue(annotation.name())
					//参数类型 query、form、formdata
					.parameterType(findParameterType(resolve, consumes, method))
					.parameterAccess(annotation.access())
					//是否必填
					.required(annotation.required())
					//参数数据类型
					.modelRef(modelReference(resolve)).build();
		}).collect(Collectors.toList());
	}

	/**
	 * 设置返回值model
	 * @param type
	 * @return
	 */
	private ModelReference modelReference(ResolvedType type) {
    
    
		if (Void.class.equals(type.getErasedType()) || Void.TYPE.equals(type.getErasedType())) {
    
    
			return new ModelRef("void");
		}
		if (MultipartFile.class.isAssignableFrom(type.getErasedType())|| isListOfFiles(type)) {
    
    
			return new ModelRef("__file");
		}
		return new ModelRef(
				type.getTypeName(),
				type.getBriefDescription(),
				null,
				allowableValues(type),
				type.getBriefDescription());
	}

	private static String findParameterType(ResolvedType resolvedType,
			Set<? extends MediaType> consumes, HttpMethod method) {
    
    
		//Multi-part file trumps any other annotations
		if (isFileType(resolvedType) || isListOfFiles(resolvedType)) {
    
    
			return "form";
		} else {
    
    
			return determineScalarParameterType(consumes, method);
		}
	}

	private static String determineScalarParameterType(Set<? extends MediaType> consumes,
			HttpMethod method) {
    
    
		String parameterType = "query";
		if (consumes.contains(MediaType.APPLICATION_FORM_URLENCODED)
				&& method == HttpMethod.POST) {
    
    
			parameterType = "form";
		} else if (consumes.contains(MediaType.MULTIPART_FORM_DATA)
				&& method == HttpMethod.POST) {
    
    
			parameterType = "formData";
		}
		return parameterType;
	}

	private static boolean isListOfFiles(ResolvedType parameterType) {
    
    
		return isContainerType(parameterType) && isFileType(collectionElementType(parameterType));
	}

	private static boolean isFileType(ResolvedType parameterType) {
    
    
		return MultipartFile.class.isAssignableFrom(parameterType.getErasedType());
	}
	
	@Override
	public boolean supports(DocumentationType documentationType) {
    
    
		return DocumentationType.SWAGGER_2.equals(documentationType);
	}
	
	@Builder(toBuilder = true)
	@Data
	public static class GeneratedApis {
    
    
		List<ApiDescription> ads;
		// 来源于哪个group
		//String groupNameOfSource;
	}	

3.3 Key logic - how to assign a value to the generated ApiDescription

Regarding this step, under the subdivision, there are actually three dimensions:

  1. Such as request method, request data type and so on.
  2. Request entry. This step has been completed in step 2 above, it is not perfect, but let’s make use of it first.
  3. request return value.

Continue to code. Address the first and third of the three dimensions above.

/**
 * <p> 搭配 {@code SwaggerAddApiDescriptionPlugin } 实现新增的 ApiDescription属性填充
 * <p> 需要确保执行时机低于 {@code DocumentationPluginsBootstrapper}
 * <p> 但{@code DocumentationPluginsBootstrapper} 这个玩意的执行时机为最低
 * <p> 所以我们转而实现 ApplicationListener<ContextRefreshedEvent>
 * @author fulizhe
 *
 */
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class SwaggerAddAddtionApiDescriptionWithDeferPushValue implements ApplicationListener<ContextRefreshedEvent> {
    
    

	private AtomicBoolean initialized = new AtomicBoolean(false);

	private final ApiDescriptionLookup lookup;

	private final SwaggerAddApiDescriptionPlugin swaggerAddApiDescriptionPlugin;

	@Autowired
	private DocumentationCache cocumentationCache;

	public SwaggerAddAddtionApiDescriptionWithDeferPushValue(ApiDescriptionLookup lookup,
			SwaggerAddApiDescriptionPlugin swaggerAddApiDescriptionPlugin) {
    
    
		super();
		this.lookup = lookup;
		this.swaggerAddApiDescriptionPlugin = swaggerAddApiDescriptionPlugin;
	}

	void start() {
    
    
		if (initialized.compareAndSet(false, true)) {
    
    
			if (swaggerAddApiDescriptionPlugin.getNeedDealed().isEmpty()) {
    
    
				initialized.compareAndSet(true, false);
				return;
			}
			swaggerAddApiDescriptionPlugin.getNeedDealed().forEach((k, v) -> {
    
    
				if (v.ads.isEmpty()) {
    
    
					return;
				}

				ApiDescription sourceDescription = lookup.description(k);
				if (!Objects.isNull(sourceDescription)) {
    
     // 如果将 OneInterfaceMultiApiDescriptionController.createFlowService() 设置为hidden, 则这里判断失败
					List<ApiDescription> ads = v.ads;
					ApiDescription first = ads.get(0);
					
					// 这里所复制的就是请求方式,请求数据类型等等这些信息
					copyProperties(sourceDescription.getOperations().get(0), first.getOperations().get(0));

					// ============================== 设置返回值
					// 这里的思路是这样的:
					// 1. swagger中对于自定义类型的返回值显示采取的是 ref 引用的方式. (这一点可以随便找个swagger文档F12看一下), 同时将ref所引用的model定义放在整个接口最外层的definitions字段中
					// 2. 在上面的copyProperties(...)中我们已经复制response相关信息, 接下来我们就只需要将definitions相关信息拷贝到当前document之下就大功告成了
					Documentation matchedSourceDocumentationByGroup = matchedSourceDocumentationByGroup(
							sourceDescription);

					Documentation targetDocumentationByGroup = cocumentationCache
							.documentationByGroup(first.getGroupName().get());

					Map<String, List<ApiListing>> tartgetApiListings = targetDocumentationByGroup.getApiListings();

					String srouceGroupName = sourceDescription.getGroupName().get();
					List<ApiListing> list = matchedSourceDocumentationByGroup.getApiListings().get(srouceGroupName);

					// 确保返回值正常显示
					list.forEach(xv -> {
    
    
						tartgetApiListings.forEach((yk, yv) -> {
    
    
							yv.forEach(m -> ReflectUtil.setFieldValue(m, "models", xv.getModels()));
						});
					});
				}

			});
		}

	}

	private Documentation matchedSourceDocumentationByGroup(ApiDescription sourceDescription) {
    
    
		String srouceGroupName = sourceDescription.getGroupName().get();

		Optional<Documentation> findFirst = cocumentationCache.all().values().stream()
				.filter(v -> v.getApiListings().keySet().contains(srouceGroupName)).findFirst();

		return findFirst.get();
	}

	private void copyProperties(Operation src, Operation dest) {
    
    
		final HttpMethod method = src.getMethod();
		ReflectUtil.setFieldValue(dest, "method", method);

		final ModelReference responseModelOfSource = src.getResponseModel();
		ReflectUtil.setFieldValue(dest, "responseModel", responseModelOfSource);

		final int position = src.getPosition();
		ReflectUtil.setFieldValue(dest, "position", position);

		final Set<String> produces = src.getProduces();
		ReflectUtil.setFieldValue(dest, "produces", produces);

		final Set<String> consumes = src.getConsumes();
		ReflectUtil.setFieldValue(dest, "consumes", consumes);

		final Set<String> protocol = src.getProtocol();
		ReflectUtil.setFieldValue(dest, "protocol", protocol);

		ReflectUtil.setFieldValue(dest, "isHidden", src.isHidden());

		ReflectUtil.setFieldValue(dest, "securityReferences", src.getSecurityReferences());

		ReflectUtil.setFieldValue(dest, "responseMessages", src.getResponseMessages());

		ReflectUtil.setFieldValue(dest, "deprecated", src.getDeprecated());

		ReflectUtil.setFieldValue(dest, "vendorExtensions", src.getVendorExtensions());

		// 不拷貝以下屬性
		//	summary, notes, uniqueId, tags, parameters

		// 無效, 这个拷贝需要目标属性有setXX方法
		//	BeanUtil.copyProperties(src, dest, "parameters", "uniqueId", "summary", "notes", "tags");
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
    
    
		start();
	}
}

3.4 Key logic - how to dynamically generate Docket

After the Swagger internal data type instance is filled, there is only one last question: how to make the finally generated and filled Swagger internal data structure displayed normally on the front-end page?

In Swagger's restful interface return value (accurately speaking, the openapi return value specification), the uniqueness of the interface is realized by uri, but our requirement in this article is precisely to require that multiple interface documents generated have the same uri.

What we are currently doing is to allow different process services to appear under different groups.
insert image description here
The relevant code is as follows:

@Configuration
@EnableKnife4j
@EnableSwagger2WebMvc
public class SwaggerConfig {
    
    

  private DefaultListableBeanFactory context;
  private RequestMappingHandlerMapping handlerMapping;

  public SwaggerConfig(DefaultListableBeanFactory context,
      RequestMappingHandlerMapping handlerMapping) {
    
    
    this.context = context;
    this.handlerMapping = handlerMapping;
    dynamicCreate();
  }

  private void dynamicCreate() {
    
    
    // 分组
    Set<String> groupNames = getGroupName();
    // 根据分好的组,循环创建配置类并添加到容器中
    groupNames.forEach(item -> {
    
    
      Docket docket = new Docket(DocumentationType.SWAGGER_2)
          .groupName(item)
          .select()
          .apis(RequestHandlerSelectors.basePackage("cn.com.kanq.dynamic")) // 确保生成的Docket扫不到任何可以生成API文档的注解
          .paths(PathSelectors.any())
          .build();
      // 手动将配置类注入到spring bean容器中
      context.registerSingleton("dynamicDocket" + item, docket);
    });
  }

  private Set<String> getGroupName() {
    
    
    HashSet<String> set = new HashSet<>();
    Map<RequestMappingInfo, HandlerMethod> mappingHandlerMethods = handlerMapping
        .getHandlerMethods();
    for (Map.Entry<RequestMappingInfo, HandlerMethod> map : mappingHandlerMethods.entrySet()) {
    
    
      HandlerMethod method = map.getValue();
      GisServiceTypes gisServiceTypes = method.getMethod().getAnnotation(GisServiceTypes.class);
      if (null != gisServiceTypes) {
    
    
        GisServiceType[] value = gisServiceTypes.value();
        for (GisServiceType gisServiceType : value) {
    
    
          set.add(gisServiceType.name());
        }
      }
    }
    return set;
  }
}

4. Continue to optimize

  1. Regarding the analysis of request parameters, it is best to reuse the analysis of swagger. ( 2023-03-06 has been implemented , the idea is to refer to OperationParameterReader, expand it yourself OperationBuilderPlugin, let Swagger help to parse the parameters and record the corresponding relationship; finally, the corresponding ones mentioned in the above section SwaggerAddAddtionApiDescriptionWithDeferPushValueare injected into the generated one ApiDescription)
  2. Let different services appear under the same page.

5. Reference

  1. Gitee - easyopen . The method introduced in this article is based on the extension of swagger. The main purpose is to stand on the shoulders of giants and maximize the reuse of the achievements of predecessors. You said that if I write one from scratch, I don’t need to deduct so many details, then you can refer to this library.
  2. In addition to the annotation method, SWAGGER can customize and add interfaces, and define additional interfaces

Guess you like

Origin blog.csdn.net/lqzkcx3/article/details/129278580