Dynamically generate swagger documents based on OpenAPI and freemarker

Foreword

In spring projects, you can use springfox or springdoc to generate swagger documents by writing annotations. The following introduces a way to dynamically generate swagger documents without writing annotations. This will be applicable in certain scenarios. For example, the interface is dynamically generated, and swagger documents are generated dynamically. It cannot be generated through annotations.

1. Define swagger template

By observing the openapi structure of a swagger document, write the parts that need to be dynamically replaced as variables to generate freemaker’s ftl template.
You can view the json structure of openapi by clicking the swagger icon link.

Modify a json structure, generate a ftl template, and place the template under resources/static/data-service-swagger-templates of the springboot project.

{<!-- -->
  "openapi": "3.0.3",
  "info": {<!-- -->
    "title": "Universal Query-[${interfaceName}]Interface",
    "description": "Universal query interface",
    "version": "0.0.1"
  },
  "servers": [{<!-- -->
    "url": "${dataServicePrefix}",
    "description": "Generated server url"
  }],
  "security": [{<!-- -->
    "secretHeader": []
  }],
  "paths": {<!-- -->
    "${url}": {<!-- -->
      "post": {<!-- -->
        "tags": ["Data Service-General Query Interface"],
        "summary": "Universal query interface",
        "description": "Universal query interface, the request body adopts a unified data structure",
        "operationId": "getData2UsingPOST",
        "requestBody": {<!-- -->
          "content": {<!-- -->
            "application/json": {<!-- -->
              "schema": {<!-- -->
                "$ref": "#/components/schemas/DmoRequest"
              }
            }
          }
        },
        "responses": {<!-- -->
          "200": {<!-- -->
            "description": "OK",
            "content": {<!-- -->
              "*/*": {<!-- -->
                "schema": {<!-- -->
                  "$ref": "#/components/schemas/ResponseEntity"
                }
              }
            }
          },
          "201": {<!-- -->
            "description": "Created"
          },
          "401": {<!-- -->
            "description": "Unauthorized"
          },
          "403": {<!-- -->
            "description": "Forbidden"
          },
          "404": {<!-- -->
            "description": "Not Found"
          }
        }
      }
    }


  },
  "components": {<!-- -->
    "schemas": {<!-- -->
      "ResponseEntity": {<!-- -->
        "title": "ResponseEntity",
        "type": "object",
        "properties": {<!-- -->
          "desc": {<!-- -->
            "type": "string",
            "description": "Error detailed description"
          },
          "message": {<!-- -->
            "type": "string",
            "description": "If the return value is not 200, you can prompt this message to the user"
          },
          "requestURL": {<!-- -->
            "type": "string"
          },
          "stackTrace": {<!-- -->
            "type": "string",
            "description": "Backend exception stack information, if it is too long, only the first part will be intercepted"
          },
          "status": {<!-- -->
            "type": "integer",
            "description": "200: Normal; 401: Not logged in; 403: No permission; 400: Request parameter verification failed; 500: Server internal error",
            "format": "int32"
          },
          "tookInMillis": {<!-- -->
            "type": "integer",
            "description": "Request time taken",
            "format": "int64"
          },
          "value": {<!-- -->
            "type": "object"
          }
        }
      },
      "DmoRequest": {<!-- -->
        "title": "DmoRequest",
        "type": "object",
        "properties": {<!-- -->
          "fulltextNode": {<!-- -->
            "$ref": "#/components/schemas/QueryNode"
          },
          "node": {<!-- -->
            "$ref": "#/components/schemas/QueryNode"
          },
          "pageNumber": {<!-- -->
            "type": "integer",
            "description": "page number",
            "format": "int32"
          },
          "pageSize": {<!-- -->
            "type": "integer",
            "description": "Number of items per page",
            "format": "int32"
          },
          "showColumns": {<!-- -->
            "uniqueItems": true,
            "type": "array",
            "items": {<!-- -->
              "type": "string",
              "description": "Displayed columns"
            }
          },
          "sorts": {<!-- -->
            "type": "array",
            "items": {<!-- -->
              "$ref": "#/components/schemas/DmoSort"
            }
          }
        }
      },
      "QueryNode": {<!-- -->
        "title": "QueryNode",
        "type": "object",
        "description": "query conditions",
        "properties": {<!-- -->
          "children": {<!-- -->
            "type": "array",
            "items": {<!-- -->
              "$ref": "#/components/schemas/QueryNode"
            }
          },
          "data": {<!-- -->
            "$ref": "#/components/schemas/NodeData"
          },
          "type": {<!-- -->
            "type": "string",
            "description": "Node type",
            "enum": ["AND", "LEAF", "OR", "ROOT"]
          }
        }
      },
      "NodeData": {<!-- -->
        "title": "NodeData",
        "type": "object",
        "description": "node data",
        "properties": {<!-- -->
          "operator": {<!-- -->
            "type": "string",
            "description": "operator",
            "enum": ["BETWEEN", "EQ", "EXISTS", "GE", "GT", "IN", "IS_NOT_NULL", \ "IS_NULL", "LE", "LIKE", "LT", "NE", "NOT_BETWEEN", "NOT_EXISTS", "NOT_IN", "NOT_LIKE ", "PREFIX", "REGEXP"]
          },
          "param": {<!-- -->
            "type": "string",
            "description": "Parameter name, generally corresponding to the column name of the table"
          },
          "value": {<!-- -->
            "type": "array",
            "description": "Parameter value",
            "items": {<!-- -->
              "type": "object"
            }
          }
        }
      },
      "DmoSort": {<!-- -->
        "title": "DmoSort",
        "type": "object",
        "description": "sort",
        "properties": {<!-- -->
          "column": {<!-- -->
            "type": "string",
            "description": "column name"
          },
          "sortOrder": {<!-- -->
            "type": "string",
            "description": "Sort by",
            "enum": ["ASC", "DESC"]
          }
        }
      }
    },
    "securitySchemes": {<!-- -->
        "secretHeader": {<!-- -->
            "type": "apiKey",
            "name": "Authorization",
            "in": "header"
        }
    }
  }
}

2. Use freemarker to generate the JSON structure of openapi

1. Import library

The code is as follows (example):

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

2. Generate json

The serviceInterface below is an entity and can be defined by yourself

 @ApiOperation(value = "Get the JSON of openapi", notes = "Get the JSON of openapi")
    @GetMapping("/swagger-json/{id}")
    public String getSwaggerJson(@ApiParam(value = "id") @PathVariable Integer id) throws BaseException {<!-- -->
   ServiceInterface serviceInterface = getServiceInterface(id);
        return getOpenApiJson(ServiceInterface serviceInterface, "test.ftl");
    }

 private String getOpenApiJson(ServiceInterface serviceInterface, String ftl) throws BaseException {<!-- -->
 freemarker.template.Configuration configuration = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_0);
                    //Set default encoding
                    configuration.setDefaultEncoding("utf-8");
                    //Set the class loader
                    configuration.setClassLoaderForTemplateLoading(this.getClass().getClassLoader(), "data-service-swagger-templates");
                    try {<!-- -->
                        // Generate template object
                        Template template = configuration.getTemplate(fileName);
                        TEMPLATE_CACHE.put(fileName, template);
                    } catch (Exception e) {<!-- -->
                        throw new BaseException(String.format("Getting template file: [%s] error", fileName), e);
                    }
        Template template = getFltTemplate(ftl);
        String dataServicePrefix = dataServiceProtocol + dataServiceUpstream;
        Map<String, String> dataMap = new HashMap<>();
        dataMap.put("interfaceName", serviceInterface.getServiceName());
        dataMap.put("dataServicePrefix", dataServicePrefix);
        dataMap.put("url", serviceInterface.getUrl());
        StringWriter sw = new StringWriter();
        try {<!-- -->
            template.process(dataMap, sw);
            return sw.toString();
        } catch (Exception e) {<!-- -->
            throw new BaseException("Template conversion error:" + e.getMessage(), e);
        }
    }

3. Front-end generated swagger example

<!DOCTYPE html>
<html>
  <head>
    <title>Data service interface document</title>
    <link rel="stylesheet" type="text/css" href="swagger-ui.css"/>
  </head>
  <body>
    <div id="swagger-ui"></div>
    <script src="swagger-ui-bundle.js"></script>
    <script>
      window.onload = function () {<!-- -->
        SwaggerUIBundle({<!-- -->
          // url: "http://localhost:14500/v3/api-docs", // Replace with the URL or file path of your OpenAPI specification
          // url: "swagger-custom-select.json", // Replace with the URL or file path of your OpenAPI specification
          url: "http://192.168.33.22:3282/dmo/service-interface/swagger-json/226", // Replace with the URL or file path of your OpenAPI specification
          dom_id: "#swagger-ui",
          deepLinking: true,
        });
      };
    </script>
  </body>
</html>

where url is the interface of the second step

The css and js download address used: https://blog.csdn.net/weixin_41085315/article/details/124965953

4. Test