Introduction to Swagger 1, Springfox-SWA……

(1) The partner of this article

Please note that this article is aimed at those who have a certain foundation for Swagger.

What you should have or already have:

  • 1. Have the experience of Swagger;
  • 2, know Swagger, start to integrate SpringBoot;
  • 3. Keen interest in customizing annotations to address business needs.
  • 4,… .

(2) What can we learn from this article

This article is based on a large number of practices, groping, looking up information, Debug source code step by step, sorting out a more detailed information, in order to help others, less detours.

Through this article you will:

  • 1. Learn how to customize annotations and use them in the SpringBoot project;
  • 2. Master how to expand Swagger’s function and successfully apply it to projects;
  • 3. Understand the process of customizing annotations and how to apply them;
  • 4. Walk less pits.

1. Introduction to Swagger

Swagger is a good thing.

Why is it a good thing?

Because:

  • 1, can automatically generate relevant API interface documents according to the business code, especially for restful style projects;
  • 2. The framework automatically generates restfut-style apis for your business code without requiring developers to maintain rest apis;
  • It also provides a test interface that automatically displays the response in JSON format.
  • 4, greatly convenient background developers and front-end communication and joint adjustment costs.

1, Springfox-Swagger introduction

In view of Swagger’s powerful capabilities, the Spring framework, the leading Java open source framework, quickly followed. It took full advantage of its advantages and integrated Swagger into its own projects, creating a Spring-Swagger, which later evolved into SpringFox. Springfox itself only uses its own AOP features, through the plug way to integrate Swagger, its own business API generation, or swagger to achieve.

Documentation for this framework is sparse on the web, and is mostly simple to use at the entry level. I in the process of integrating this framework into their own projects, encountered many pits, in order to solve these pits, I had to open its source code to see what it is. This article is to describe my understanding of SpringFox in the process of using springFox and what needs to be paid attention to.

2. General principles of SpringFox

The general principle of SpringFox is that during the initialization of the Spring context during project startup, the framework automatically loads some Swagger related beans into the current context according to the configuration, and automatically scans the system for classes that might need to generate API documentation. And generate the corresponding information to cache. If the project MVC control layer uses springMvc, it will automatically scan all Controller classes and generate API documentation based on the methods in those Controller classes.

SpringBoot integrates Swagger

Springfox-swagger-ui dependencies are not required. You can use a third-party UI or write your own front-end UI.

We can use a UI written based on Bootstrap.

1. Introduce dependencies

< the dependency > < groupId > IO. Springfox < / groupId > < artifactId > springfox - swagger2 < / artifactId > < version > 2.9.2 < / version > </dependency>Copy the code

The release of Springfox-Swagger2 :2.9.2 as of September 2020.9 introduces the following dependencies:

  • IO. Swagger: swagger – annotations: 1.5.20
  • IO. Swagger: swagger – models: 1.5.20
  • IO. Springfox: springfox – spi: 2.9.2
  • IO. Springfox: springfox – schema: 2.9.2
  • IO. Springfox: springfox swagger – common: 2.9.2
  • IO. Springfox: springfox – spring – web: 2.9.2
  • Com. Google. : guava guava: 20.0
  • Com. Fasterxml: classmate: 1.3.3
  • Org. Slf4j: slf4j – API: 1.7.24
  • Org. Springframework. Plugin: spring – the plugin – core: 1.2.0. RELEASE
  • Org. Springframework. Plugin: spring – the plugin – metadata: 1.2.0. RELEASE
  • Org. Mapstruct: mapstruct: 1.2.0) Final
  • IO. Springfox: springfox – core: 2.9.2
  • Net. Bytebuddy: byte – buddy: 1.8.12

To make the page look better, we can also introduce this front-end UI based on Bootstrap:

<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> The < version > 2.0.4 < / version > < / dependency >Copy the code

Default Swagger interface:

Introducing third-party Bootstrap written UI:

2. Configure related configuration files

@Configuration @EnableSwagger2 @EnableKnife4j public class Swagger2Config { @Bean public Docket appApi() { return new Docket (DocumentationType SWAGGER_2). UseDefaultResponseMessages (false). GroupName (" knowledge base "). ApiInfo (apiInfo ()). The select () .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo ApiInfo () {return new ApiInfoBuilder().title(" API ").description(" API ").r \n\n") Contact (new springfox. Documentation. Service. Contact (" we are robots < - q '(^_^)' p -- -- -- - > business background team ", "https://www.glodon.com/", Null)). The version (" 0.0.1 "). The build (); }}Copy the code

Note here where to scan:

There are two ways

.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
Copy the code

If the first method is used, all Controller classes in a fixed package will be scanned and the corresponding API samples will be automatically generated, as shown below:

The nice thing is that if you define an interface in your Controller class, or if you define more than one interface, it will be scanned directly. Simple, convenient and fast.

The good comes with the bad, which is: it creates a mess, and all the generated apis will be messy.

This is not enough. You also need to configure the MVC pattern to display web pages:

@Configuration public class SwaggerWebMvcConfigurer implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("doc.html"). addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**"). addResourceLocations("classpath:/META-INF/resources/webjars/"); }}Copy the code

(3) Simple induction of the use of Swagger commonly used annotations

  • 1. @ Api
  • 2, @ ApiOperation
  • 3, @ ApiOperation
  • 4. @ApiImplicitParams, @ApiImplicitParam
  • 5, @apiresponses, @apiResponse
  • 6, @apiModel, @apiModelProperty
  • 7, @ PathVariable
  • 8, @ RequestParam

1. @ APi

Description:

The @Api annotation annotates a Controller (Class).

Main attributes:

attribute describe
value Path value of the URL
tags If you set this value, the value of value will be overwritten
description Description of API resources
basePath Basic paths do not need to be configured
position If you configure multiple apis and want to change the order of the display position
produces For example, “application/json, application/XML”
consumes For example, “application/json, application/XML”
protocols Possible values: http, https, ws, wss.
authorizations This parameter is configured for advanced feature authentication
hidden Setting it to true will be hidden in the document

Example:

@controller@api (tags = "request test Api ",position = 1) public class TestController {}Copy the code

Effect:

The @Api annotation means that your Controller is allowed to be scanned by Swagger related components.

2, @ ApiOperation

Description:

The @apiOperation annotation is used to describe an operation or HTTP method. Different operations with the same path are grouped into the same action object. Different HTTP request methods and paths are combined to form a unique operation.

Main attributes:

attribute describe
value Path value of the URL
tags If you set this value, the value of value will be overwritten
description Description of API resources
basePath Basic paths do not need to be configured
position If you configure multiple apis and want to change the order of the display position
produces For example, “application/json, application/XML”
consumes For example, “application/json, application/XML”
protocols Possible values: http, https, ws, wss.
authorizations This parameter is configured for advanced feature authentication
hidden Setting it to true will be hidden in the document
response Object returned
responseContainer These objects are valid “List”, “Set” or “Map”, and others are invalid
httpMethod “GET”, “the HEAD”, “POST”, “PUT”, “DELETE”, “OPTIONS” and the “PATCH”
code The default HTTP status code is 200
extensions Extended attributes

Example:

@getMapping ("/get") @responseBody @apiOperation (value = "Get request test ", Notes = "get request test ",position = 1) public String Get (String)  name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); return json.toString(); }Copy the code

Effect:

3, @ ApiParam

Description:

@apiParam acts on the request method, defining annotations for API parameters.

Main attributes:

attribute describe
name The attribute name
value Attribute values
defaultValue Default property value
allowableValues Do not configure
required This parameter is mandatory
access But too much description
allowMultiple The default is false
hidden Hide the property
example For example,

Example:

@getMapping ("/get") @responseBody @apiOperation (value = "get request test ", Notes = "get request test ",position = 1) public String Get (@apiParam (Required = true,value = "name",example = "三",name = "name") String name){JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); return json.toString(); }Copy the code

Effect:

4. @ApiImplicitParams, @ApiImplicitParam

Description:

@APIIMPLICITParams: a description of a single parameter used in a request method. @APIIMPLICITParam: A description of a single parameter

Main attributes:

attribute describe
name Parameter names
value Description of parameters
required Parameter Specifies whether this parameter is mandatory
paramType Query –> Retrieve request parameters: @requestParam header –> retrieve request parameters: @requestheader path (used for restful interfaces) –> Retrieve request parameters: @pathVariable Body — > @requestBody User User form
dataType Parameter type, default String, other values dataType= “Integer”
defaultValue The default value of the parameter

Example:

@apiIMPLICITParams ({@apiIMPLICITParam (name="mobile",value=" mobile number ", Required =true,paramType="form"), @ ApiImplicitParam (name = "password", value = "password", the required = true, paramType = "form"). @APIIMPLICITParam (name="age",value=" age", Required =true,paramType="form",dataType="Integer")}) @postMapping ("/login") public JsonResult login(@RequestParam String mobile, @RequestParam String password, @RequestParam Integer age){ return JsonResult.ok(map); }Copy the code

Effect:

It’s the same as the last one, just in a different position.

5, @apiresponses, @apiResponse

Description:

@apiresponses, @apiResponse for method return object description.

Main attributes:

attribute describe
code A number, such as 400
message Information, such as “Request parameters not filled in”
response The entity class of the custom schema

Example:

@RequestMapping(value = "/ceshia", Method = requestmethod. POST) @responseBody @apiOperation (value = "POST request test ",notes =" test 2",position = 2) @apiresponses ({ @ApiResponse(code = 200,message = "success",response = RequestCode200.class), @apiResponse (code = 509,message = "error ",response = requestCode509.class), @apiResponse (code = 410,message = "error ",response = requestCode410.class), @apiResponse (code = 510,message = "error ",response = requestCode50.class)}) public String ceshia(@requestBody String str){ JSONObject json = new JSONObject(); json.put("requestType","postType"); json.put("body",str); return json.toString(); }Copy the code

Effect:

6, @apiModel, @apiModelProperty

Description:

  • @APIModel is used to describe information about a Model (this is typically used during POST creation, in scenarios such as @RequestBody, where request parameters cannot be described using the @APIIMPLicitParam annotation).
  • The @apiModelProperty is used to describe the properties of a Model.

Main attributes:

Example:

package com.github.swaggerplugin.codes; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @apiModel ("RequestCode200") public class RequestCode200 {@apiModelProperty (value = "response code ",name = "messageCode",example =  "200") private Integer messageCode; @apiModelProperty (value = "return message", name = "message",example = "success") private String message; public Integer getMessageCode() { return messageCode; } public void setMessageCode(Integer messageCode) { this.messageCode = messageCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }}Copy the code

Effect:

7, @ PathVariable

Description:

@pathvariable is used to obtain the parameters in the URL path of get request, that is, the function of parameter binding, commonly said as “?” in the URL. The previously bound parameter.

For example: www.baidu.com/name=zhengh…

Example:

@getMapping ("/get") @responseBody @apiOperation (value = "get request test ", Notes = "get request test ",position = 1) public String Get (@apiparam (Required = true,value = "name",example = "zhang 三",name = "name") @pathVariable ("name") String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); json.put("id","123"); return json.toString(); }Copy the code

Note: If this is not added, the default is body, which is to receive json data.

8, @ RequestParam

Description:

RequestParam (@requestParam, @requestParam, @requestParam, @requestParam, @requestParam, @requestParam Each parameter that follows the concatenation.

Example:

@getMapping ("/get") @responseBody @apiOperation (value = "get request test ", Notes = "get request test ",position = 1) public String Get (@apiparam (required = true,value = "name",example = "name") @requestParam ("name") String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); json.put("id","123"); return json.toString(); }Copy the code

(a) What are annotations? How do I customize annotations?

For the explanation part of annotations, I have organized a more detailed article before, so as not to explain too much here, you can refer to my following article:

WX Public account: What are annotations? How to define annotations

CSDM: What did you say? Annotation you can’t?

(2) Why to expand Swagger function and the effect after expansion

A:

Of course, Swagger’s current capabilities are not enough for our current project.

In fact, Swagger’s existing features also meet our needs, but the code is too intrusive.

Understand intrusion in a word:

When your code introduces a component that leads to other code or design, make changes to accommodate the new component. In this case we consider the new component intrusive

At the same time, there is a design concept involved here, which is the problem of coupling.

Our code is designed with the idea of “high cohesion, low coupling” in mind, and in order to achieve this idea, we must make our code less intrusive.

From: One sentence to make you understand how intrusive code can be

Swagger has many advantages, there are advantages, there must be disadvantages, this is no one can change, what we can do is to reduce the disadvantages.

The advantages have already been mentioned, I summarize them as follows, and of course there are others:

  • 1, can automatically generate relevant API interface documents according to the business code, especially for restful style projects;
  • 2. The framework automatically generates restfut-style apis for your business code without requiring developers to maintain rest apis;
  • It also provides a test interface that automatically displays the response in JSON format.
  • 4, greatly convenient background developers and front-end communication and joint adjustment costs.

For disadvantages:

  • 1. Inconvenient maintenance (when the interface changes, the corresponding parameter configuration needs to be modified every time);
  • 2. Too much code about Swagger, seriously covering the original Java logic code (key point);
  • 3. If an interface has multiple response instances, only one response instance can be displayed (for example, the response code of the customized response parameter 401 includes incorrect password, incorrect parameter, and incorrect ID).
  • 4. When the interface receives a JSON string, the specific parameters in the JSON string cannot be displayed in Swagger’s UI (there will be problems with the front-end, the front-end will not know what it wants to pass to you);

The problems to be solved in this paper are also to make up for the shortcomings by expanding the functions of Swagger to solve these problems.

Example:

Oh, my God. That’s just one interface. That’s over 80 lines.

@responseBody @requestMapping (method = requestmethod.get) @apiOperation (position = 2,value = "9.2 ") Access to artificial regulation configuration, "notes =" < p > < strong > the requested url: < / strong > < / p > \ n "+" < p > / API/background/config/robotMonitorConfig < / p > \ n "+ "< p > < strong > to return to the success of data: < / strong > < / p > \ n" + "< the details > \ n" + "< summary > click on view < summary > \ n" + "< pre > < code class=\"language-json\">{\n" + "\n" + " message: "success",\n" + "\n" + " messageCode:"200",\n" + "\n" + " result: [\n" + "\n" + " {\n" + "\n" + " id: 1,\n" + "\n" + " robotId: 18,\n" + "\n" + " authStatus: 1,\n" + "\n" + " warnStatus: 1,\n" + "\n" + " warnRule: \n" + "\n" + " {\n" + "\n" + " "angry":1,\n" + "\n" + " "unknown":1,\n" + "\n" + " "down":1,\n" + "\n" + " "sameAnswer":1,\n" + "\n" + " "noClick":1,\n" + "\n" + " "keywords":1\n" + "\n" + " },\n" + "\n" + " warnKeywords: \ n \ n "+" "+"/" + "\ n \ n" + "" turn artificial", "+" \ n \ n "+" "artificial reply" \ n "+" \ n "+"], and \ n "+" \ n "+" operatorId: "qubb",\n" + "\n" + " lastModifyTime: 231431341343123\n" + "\n" + " }\n" + "\n" + " ]\n" + "\n" + "}\n" + "\n" + "</code></pre>\n" + "</details>\n" + "< p > < strong > return failure data: < / strong > < / p > \ n" + "< the details > \ n" + "< summary > click on view < summary > \ n" + "< pre > < code class=\"language-json\">{\n" + " message: "The current user has logged out, please try again after landing", \ n "+" messageCode: "509" \ n \ n "+" "+"} \ n "+" {\ n "+" message: "param robotId error",\n" + " messageCode:"410"\n" + "}\n" + "\n" + "{\n" + " message: "system error",\n" + " messageCode:"510"\n" + "}\n" + "\n" + "</code></pre>\n" + "</details>\n" + "\n" + "\n" + "\n") Public Object XXX (@apiParam (value = "roboID ",defaultValue = "7", Required = true) @requestParam (value = "robotId", required = false) String robotIdStr) { }Copy the code

After I extend the function:

One line of code is easy

@APiFileInfo("/xxx")
Copy the code

(3) Preparation of prelude

1. Three Spring annotations that you must know

  • (1) @component (instantiate regular POJOs into the Spring container, equivalent to the configuration file)
  • (2) @order (1) (adjust the Order in which this class is injected)
  • (3) @Configuration (used to define Configuration classes that can replace XML Configuration files, annotated classes that contain one or more methods annotated by @bean, These methods will be AnnotationConfigApplicationContext or AnnotationConfigWebApplicationContext scanned, and used to construct the bean definition, initialize the Spring container.

2. Swagger’s extensible components

In the source code: you can see the interface files at the end of the Plugin shown below, which we will work on.

For a detailed introduction, check out another article at the end of this article. A detailed analysis of these interfaces is not covered in this article.

(a) combat a: for the transfer of JSON string parameters, so that it has the function of the description of the relevant parameters

1. Sources of demand

Where there is demand, there is a source of demand or the generation of demand. Why is there a need in the first place? Let’s start by looking at why there is a need.

Let’s take three interfaces as examples:

@controller ("/studentController") @api (tags = "studentController",position = 1) public class studentController { @getMapping ("/getStudentById") @APIOperation (value = "Obtain student information based on student ID ",notes =" Obtain student information based on passed student ID ",position = 1) public Object getStudentById(String id){ return "id="+id; } @postmapping ("/addStudent") @apioperation (value = "addStudent info ",notes =" addStudent info ",position = 1) public Object addStudent(@RequestBody Student student){ return "student="+student.toString(); } @postmapping ("/addStudent2") @apiOperation (value = "add student information ",notes =" add student information ", ",position = 1) public Object addStudentStr(@requestBody String STR){return "STR ="+ STR; }}Copy the code

Analysis of 1:

GetStudentById (String ID) The interface passes only one ID.

The page looks like this:

The test function page is as follows:

Analysis of 2:

The addStudent(@requestBody Student Student) interface needs to pass an object of json data type.

The page looks like this:

The test function page is as follows:

Analysis 3 :(that’s where the problem lies. Watch out.)

AddStudentStr (@requestBody String STR) The interface needs to pass a JSON data type String.

The page looks like this:

The test function page is as follows:

2. Demand analysis

Through analysis 1, 2, and 3, the three examples show that when a JSON string is passed as an argument, no specific argument is displayed. This makes it impossible for the front end to know what is being delivered.

Our needs are simple, clear and direct. When the parameters passed are in JSON string format, the function of realizing the description of related parameters is realized.

3. Development ideas

(1) Detours

The first thing you might think of is: in a custom class, write the fields you need inside, so you don’t have it.

First of all, it’s ok.

But the problem is quite big, if I only have a few interfaces, ok. But a project with hundreds of interfaces needs to define hundreds of classes. After a team discussion, this approach was killed.

(2) The right way

You can customize an annotation to add the annotation to the desired parameter. The class is then automatically generated by passing values through annotations.

It works.

There are many ways to create classes on the fly, and I chose to do it myself: Javassist’s ClassPool mechanism.

If you’re interested in ClassPool, you can read up on it yourself, so I won’t go into too much detail here.

4. Key code

Custom annotations are described in more detail than necessary.

Definition of @APICP annotation:

package com.github.swaggerplugin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Apicp { Class<? > classPath(); String modelName(); String values()[]; String noValues()[] default {} ; String noValueTypes()[] default {}; String noVlaueExplains()[] default {}; }Copy the code

@apiigp Definition of annotation:

package com.github.swaggerplugin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIgp { Class<? > classPath(); String modelName(); String values()[]; String noValues()[] default {} ; String noValueTypes()[] default {}; String noVlaueExplains()[] default {}; }Copy the code

Custom finished annotation, but how to make it work? This is the most important part of the step.

The principle of automatic assembly in Spring, you can go to understand. In this project, we use Spring’s @Component or @Configuration annotation to implement automatic injection into poJOs. The utility of these two annotations has already been described.

Looking through the SpringFox (Swagger) and Knife4J-spring-boot source code, I found that if I wanted to customize the extension functionality, I only needed to implement the Apply method in an xxxPlugin interface. In the Apply method, we manually scan our custom annotations and add the implementation logic.

The code is not put in the whole, too long, only selected part to put. If you’re interested, you can go to my Github and pull it up, and THEN I’ll show you how to apply it directly.

@Configuration
@Order(-19999)   
public class SwaggerModelReader implements ParameterBuilderPlugin {

    @Autowired
    private TypeResolver typeResolver;

    static final Map<String,String> MAPS = new HashMap<>();
    static {
        MAPS.put("byte","java.lang.Byte");
        MAPS.put("short","java.lang.Short");
        MAPS.put("integer","java.lang.Integer");
        MAPS.put("long","java.lang.Long");
        MAPS.put("float","java.lang.Float");
        MAPS.put("double","java.lang.Double");
        MAPS.put("char","java.lang.Character");
        MAPS.put("string","java.lang.String");
        MAPS.put("boolean","java.lang.Boolean");
    }

    
    static public String getTypePath(String key){
        return key==null || !MAPS.containsKey(key.toLowerCase()) ? null :  MAPS.get(key.toLowerCase());
    }


    @Override
    public void apply(ParameterContext context) {
        ResolvedMethodParameter methodParameter = context.resolvedMethodParameter();

        
        Optional<ApiIgp> apiIgp = methodParameter.findAnnotation(ApiIgp.class);
        Optional<Apicp> apicp = methodParameter.findAnnotation(Apicp.class);



        if (apiIgp.isPresent() || apicp.isPresent()) {
            Class originClass = null;
            String[] properties = null; 
            Integer annoType = 0;
            String name = null + "Model" + 1;  

            String[] noValues = null;
            String[] noValueTypes = null;
            String[] noVlaueExplains = null;
            
            if (apiIgp.isPresent()){
                properties = apiIgp.get().values(); 
                originClass = apiIgp.get().classPath();
                name = apiIgp.get().modelName() ;  

                noValues = apiIgp.get().noValues();
                noValueTypes = apiIgp.get().noValueTypes();
                noVlaueExplains = apiIgp.get().noVlaueExplains();

            }else {
                properties = apicp.get().values(); 
                annoType = 1;
                originClass = apicp.get().classPath();
                name = apicp.get().modelName() ;
                noValues = apicp.get().noValues();
                noValueTypes = apicp.get().noValueTypes();
                noVlaueExplains = apicp.get().noVlaueExplains();
            }

            
            Class newClass = createRefModelIgp(properties, noValues, noValueTypes, noVlaueExplains, name, originClass, annoType);


            context.getDocumentationContext()
                    .getAdditionalModels()
                    .add(typeResolver.resolve(newClass));  


            context.parameterBuilder()  
                    .parameterType("body")
                    .modelRef(new ModelRef(name))
                    .name(name);

        }


    }

    
    private Class createRefModelIgp(String[] properties, String[] noValues, String[] noValueTypes, String[] noVlaueExplains, String name, Class origin, Integer annoType) {
        try {
            
            Field[] fields = origin.getDeclaredFields();
            
            List<Field> fieldList = Arrays.asList(fields);
            
            List<String> dealProperties = Arrays.asList(properties);
            
            List<Field> dealFileds = fieldList
                    .stream()
                    .filter(s ->
                            annoType==0 ? (!(dealProperties.contains(s.getName()))) 
                                        : dealProperties.contains(s.getName())
                            ).collect(Collectors.toList());

            
            List<String> noDealFileds = Arrays.asList(noValues);
            List<String> noDealFiledTypes = Arrays.asList(noValueTypes);
            List<String> noDealFiledExplains = Arrays.asList(noVlaueExplains);


            
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass(origin.getPackage().getName()+"."+name);

            
            createCtFileds(dealFileds,noDealFileds,noDealFiledTypes,noDealFiledExplains,ctClass,annoType);

            
            return ctClass.toClass();

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }

    
    public void createCtFileds(List<Field> dealFileds, List<String> noDealFileds, List<String> noDealFiledTypes,List<String> noDealFiledExplains, CtClass ctClass, Integer annoType) {
       

        for (Field field : dealFileds) {
            CtField ctField = null;
            try {
                ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
            } catch (CannotCompileException e) {
                System.out.println("找不到了1:"+e.getMessage());
            } catch (NotFoundException e) {
                System.out.println("找不到了2:"+e.getMessage());
            }
            ctField.setModifiers(Modifier.PUBLIC);
            ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
            String apiModelPropertyValue = java.util.Optional.ofNullable(annotation).map(s -> s.value()).orElse("");



            if (StringUtils.isNotBlank(apiModelPropertyValue)) { 
                ConstPool constPool = ctClass.getClassFile().getConstPool();

                AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
                Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
                ann.addMemberValue("value", new StringMemberValue(apiModelPropertyValue,constPool));
                attr.addAnnotation(ann);

                ctField.getFieldInfo().addAttribute(attr);
            }
            try {
                ctClass.addField(ctField);
            } catch (CannotCompileException e) {
                System.out.println("无法添加字段1:"+e.getMessage());
            }
        }

        
         for (int i = 0; i < noDealFileds.size(); i++) {
            String valueName = noDealFileds.get(i);
            String valueType = noDealFiledTypes.get(i);
            valueType=getTypePath(valueType);

            
             CtField ctField = null;
             try {
                 ctField = new CtField(ClassPool.getDefault().get(valueType), valueName, ctClass);
             } catch (CannotCompileException e) {
                 System.out.println("找不到了3:"+e.getMessage());
             } catch (NotFoundException e) {
                 System.out.println("找不到了4:"+e.getMessage());
             }
             ctField.setModifiers(Modifier.PUBLIC);

             if(noDealFiledExplains.size()!=0){
                 
                 String apiModelPropertyValue = (apiModelPropertyValue=noDealFiledExplains.get(i))==null?"无描述":apiModelPropertyValue;

                 System.out.println(apiModelPropertyValue);

                 if (StringUtils.isNotBlank(apiModelPropertyValue)) { 
                     ConstPool constPool = ctClass.getClassFile().getConstPool();
                     AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
                     Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
                     ann.addMemberValue("value", new StringMemberValue(apiModelPropertyValue,constPool));
                     attr.addAnnotation(ann);

                     ctField.getFieldInfo().addAttribute(attr);
                 }

             }

             
             try {
                 ctClass.addField(ctField);
             } catch (CannotCompileException e) {
                 System.out.println("无法添加字段2:"+e.getMessage());
             }

         }

    }
}
Copy the code

5. Actual combat results

We modify the interface as follows:

@postmapping ("/addStudent2") @apiOperation (value = "add student information for special fields ",notes =" Add student information for special fields, ",position = 1) public Object addStudentStr(@apICP (values = {"Id","name"}, modelName = "addStudent2", classPath = Student.class, noValues = {"lala","haha","xixi"}, noValueTypes = {"string","integer","double"}, }) {return "STR ="+ STR; }Copy the code

Effect:

As you can see, our custom annotations are in effect, and we have parameter names, parameter descriptions, and data types.

Note: If this option is required, the next upgrade will have it.

When debugging, you can also see that there are also:

So we don’t have to manually create hundreds of classes for hundreds of interfaces.

(2) Actual combat 2: Reduce Swagger code in Controller, so that it can read information from some files, automatic configuration of Swagge function

1. Sources of demand

We need to describe the return value of the interface, for example:

Return value with code 200: (source: I copied it from the API return in the briefbook)

[{"id":62564697, "slug":" 09C7DB472fa6 ", "title":"Java SPI mechanism ", "view_count":42, "user":{"id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } }, {"id":62564140, "slug":" ec3bd614dCB0 ", "title":"SLF4J log level and usage scenario ", "view_count":381, "user":{"id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } } ]Copy the code

Return value for code 410:

{"messageCode": 410, "message": "Name cannot be empty"}Copy the code

Return value for code 509:

{"messageCode": 509, "message": "Name length cannot exceed 15"} {"messageCode": 509, "message":" name length cannot exceed 15"} {"messageCode": 509, "message": "name length cannot exceed 15"} {"messageCode": {"messageCode": 509, "message": "name already exists"} {"messageCode": 509, "message": "name already exists"}Copy the code

Return value for code 510:

{
    "messageCode": 510,
    "message": "system error"
}
Copy the code

We can add it like this:

@getMapping ("/getStudentById") @apiOperation (value = "Obtain student information by student ID ",notes = "" + "[\n" +" {\n" + "\" ID \":62564697,\n" Slug + "\" \ ", \ "09 c7db472fa6 \", \ n "+" \ "title \ \" : \ "Java SPI mechanism," \ n "+" \ "view_count \" : 42, \ n "+" \ "user \" : {\ n "+" \"id\":12724216,\n" + " \"nickname\":\"bdqfork\",\n" + " \"slug\":\"a2329f464833\",\n" + " \"avatar\":\"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg\"\n" + " "+"}} \ n \ n "+" {\ n "+" \ \ "id" : 62564140, \ n "+" \ "slug \" : \ "ec3bd614dcb0 \", \ n "+" \ "title \" : \ SLF4J log level, and the use of scene \ ", \ n" + " \"view_count\":381,\n" + " \"user\":{\n" + " \"id\":12724216,\n" + " \"nickname\":\"bdqfork\",\n" + " \"slug\":\"a2329f464833\",\n" + " \"avatar\":\"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg\"\n" + " }\n" + " }\n" + "]",position = 1) public Object getStudentById(String id){ return "id="+id; }Copy the code

Page effect:

What we have to do is correct that.

And this 200 interface description takes up a screen:

What is good when a controller has multiple interfaces is always overwritten by the interface description. That’s where our demand comes from.

2. Demand analysis

See how the page looks

You may wonder why adding \ n does not return to display, I went to check how Swagger UI source code is displayed. The principle is made by makdown, by rendering. So we can convert makdown syntax to HTML syntax for implementation, after I wrote the conversion gadget, found that it is possible.

3. Development ideas

Check online to see if there is a conversion tool.

Let’s first introduce this, which is how to do the conversion:

< the dependency > < groupId > com. Vladsch. Flexmark < / groupId > < artifactId > flexmark -all < / artifactId > < version > 0.50.42 < / version > </dependency>Copy the code

The code is simple:

MutableDataSet options = new MutableDataSet();
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();


Node document = parser.parse(mdBody.toString());


String html = renderer.render(document);  
Copy the code

When run, you get the transformed HTML syntax:

We copy the transformed HTML code into the interface description:

@getMapping ("/getStudentById") @apiOperation (value = "Obtain student information by student ID ",notes =" + "<pre><code Class = \ "language - json \" > [\ n "+" {\ n "+" id ":" 62564697, \ n "+" "slug" : "09 c7db472fa6," \ n "+" title ":" "Java mechanism of SPI," \ n "+  " "view_count":42,\n" + " "user":{\n" + " "id":12724216,\n" + " "nickname":"bdqfork",\n" + " "slug":"a2329f464833",\n" + " "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg"\n" + " "+"}} \ n \ n "+" {\ n "+" id ":" 62564140, \ n "+" "slug" : "ec3bd614dcb0," \ n "+" title ":" SLF4J log level, and the use of scenario ", \ n "+" "view_count":381,\n" + " "user":{\n" + " "id":12724216,\n" + " "nickname":"bdqfork",\n" + " "slug":"a2329f464833",\n" + " "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg"\n" + " }\n" + " }\n" + "]\n" + "</code></pre>\n" + "\n" + "\n" + "",position = 1) public Object getStudentById(String id){ return "id="+id; }Copy the code

To see the effect again:

Sure enough, it can.

But if there are hundreds of interfaces, are you going to copy and paste them one by one?Copy the code

Here is how to solve the problem when there are a large number of interfaces.

4. Key code

The code for converting makdown to HTML syntax is as follows:

I upgraded it to fold when I encountered a block of code.

public class MdToHtml { public final static String makdownToHtml(String md) { StringBuilder mdBody = new StringBuilder(); for (int i = 0; i < md.length() ; i++) { StringBuilder newCodeBody = new StringBuilder(); newCodeBody.append("\n\n"); newCodeBody.append("<details> \n"); newCodeBody.append("\n"); Newcodebody.append ("<summary> click to expand to see </summary> \n"); if(md.charAt(i)=='`' && md.charAt(i+1)=='`' && md.charAt(i+2)=='`'){ String temp = md.substring(i+3); int strIndex = findStrIndex(temp); String codeType = md.substring(i + 3, i + 3 + strIndex); newCodeBody.append("\n```"+codeType+"\n"); for (int j = i+3+strIndex+1; j < md.length() ; j++) { if(md.charAt(j)=='`' && md.charAt(j+1)=='`' && md.charAt(j+2)=='`'){ i=j+3; newCodeBody.append("\n```\n"); newCodeBody.append("\n"); newCodeBody.append("</details>"); newCodeBody.append("\n"); break; }else{ newCodeBody.append(md.charAt(j)); } } mdBody.append(newCodeBody.toString()); }else{ mdBody.append(md.charAt(i)); } } MutableDataSet options = new MutableDataSet(); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); Node document = parser.parse(mdBody.toString()); String html = renderer.render(document); return html; } static private int findStrIndex(String str){ int sum = 0; for (int i = 0; i <str.length() ; i++) { if(str.charAt(i)=='\n') return sum++; else sum++; } return -1; }}Copy the code

If used, it would look something like this:

You can expand it, you can close it.

This is done using makdown’s syntax.

Define a custom annotation:

package com.github.swaggerplugin.annotation;


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

@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface APiFileInfo {

    String value() default "";
}
Copy the code

Implementation of this annotation:

The implementation is very simple, the hard part is how to parse it.Copy the code
@Component @Order(1) public class OperationPositionBulderPlugin implements OperationBuilderPlugin { @Autowired private TypeResolver typeResolver; static Map<String,Map<Integer, APiFileInfoBean>> apiFileInfoMaps = null; public static String swaggerMdPaths = "src/main/resources/md"; private static boolean flag = false; @Value("${swagger.md.paths}") private void setSwaggerMdPaths(String swaggerMdPaths){ OperationPositionBulderPlugin.swaggerMdPaths = swaggerMdPaths; } public OperationPositionBulderPlugin() { String[] paths = swaggerMdPaths.split(","); System.out.println(" start parsing file ------------>>>>"); System.out.println(" array: "+Arrays. ToString (paths)); if(apiFileInfoMaps==null) { apiFileInfoMaps = ReadFromFile.initFileOrDirectory(paths); flag=apiFileInfoMaps==null? false:true; }} @override public void apply(OperationContext context) {if(flag){system.out.println (" file, load: "+flag); Optional<APiFileInfo> apiFileInfoOptional = context.findAnnotation(APiFileInfo.class); if (apiFileInfoOptional.isPresent()) { String flag = null; System.out.println("apiFileInfoOptional--->"+apiFileInfoOptional.get().value()); flag = apiFileInfoOptional.get().value(); context.operationBuilder() .responseMessages(buildResponseMessage(flag, apiFileInfoMaps)); }}else {system.out.println (" no file, no load "); } } private Set<ResponseMessage> buildResponseMessage(String flag, Map<String, Map<Integer,APiFileInfoBean>> apiFileInfoMaps) { Map<Integer,APiFileInfoBean> aPiFileInfoBean = apiFileInfoMaps.get(flag); Set<ResponseMessage> set = new HashSet<>(); ResponseMessage responseMessage = null; if(aPiFileInfoBean! =null) for (Integer code : aPiFileInfoBean.keySet()) { APiFileInfoBean fileInfoBean = aPiFileInfoBean.get(code); responseMessage = new ResponseMessageBuilder() .code(code) .message(MdToHtml.makdownToHtml(fileInfoBean.getMessage())) .responseModel(new ModelRef("UpdateRobotModel")) .build(); set.add(responseMessage); } return set; } @Override public boolean supports(DocumentationType delimiter) { return true; }}Copy the code

Parsing file contents:

There are many legacy debug print statements in the code that can be ignored.

package com.github.swaggerplugin.util; import com.github.swaggerplugin.bean.APiFileInfoBean; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ReadFromFile { public static Map<String, Map<Integer, APiFileInfoBean>> initFileOrDirectory(String[] mdFileURIs) { Map<String, Map<Integer,APiFileInfoBean>> map = new HashMap<>(); for (String fileURIs : mdFileURIs) { File file = new File(fileURIs); if(file! =null && ! file.isFile()){ initDirectory(map,file); }else if(file! =null && file.isFile()){ initFile(map,file); } } return map.size()<=0? null:map; } private static void initFile(Map<String, Map<Integer,APiFileInfoBean>> map, File file) { List<String> list = ReadFileDataToList(file); int start = 0; int end = 0; for (int i = 0; i < list.size(); i++) { String dataLine = list.get(i); if(dataLine.startsWith("# URL:")){ start = i; for (int j = i+1; j<list.size(); j++){ String dataLine_start = list.get(j); If (dataLine_start. StartsWith (" # URL: ")) {System. Out. Println (" the current API starting position: "+ I +" -- -- > "+ (j - 1)); end = j-1; i=j-1; break; } } disposeInterval(start,end,list,map); }} System. Out. Println (" the current API starting position: "+ start +" -- -- > "+ (list. The size ())); disposeInterval(start,list.size(),list,map); } private static void disposeInterval(int start, int end, List<String> list, Map<String,Map<Integer,APiFileInfoBean>> map) { int code_start = 0; int code_end = 0; String flag = null; for (int index = start; index < end ; index++) { String s = list.get(index); if(s.startsWith("# URL:")){ flag = s.substring(6, s.length()).replace(" ",""); } if(s.replace(" ","").endsWith("---") && s.replace(" ","").equals("---")){ code_start = index+1+1; for (int i = index+1; i < end; i++) { s=list.get(i); if(s.replace(" ","").endsWith("---") && s.replace(" ","").equals("---")) { code_end = i-1; index = i-1; System. The out. Println (" code starting position: "+ code_start +" -- -- -- > "+ code_end); disposeCodeInterval(flag,code_start,code_end,list,map); break; } } } } } private static void disposeCodeInterval(String flag, int code_start, int code_end, List<String> list, Map<String, Map<Integer, APiFileInfoBean>> map) { APiFileInfoBean aPiFileInfoBean = new APiFileInfoBean(); Map<Integer,APiFileInfoBean> fileInfoBeanMap = new HashMap<>(); StringBuilder sb = new StringBuilder(); for (int index = code_start; index <= code_end ; index++) { String s = list.get(index); if(s.startsWith("code:")){ String code = s.substring(5, s.length()); try { aPiFileInfoBean.setCode(Integer.valueOf(code.replace(" ",""))); }catch (Exception e){ e.fillInStackTrace(); } }else if(s! =null && ! (s.replace(" ","")).equals("") ){ if(s.charAt(0)=='`' && s.charAt(1)=='`' && s.charAt(2)=='`'){ sb.append(s+"\n"); for (int i = index+1; i < list.size() ; i++) { s = list.get(i); sb.append(list.get(i)+"\n"); if(s! =null && ! (s.replace(" ","").equals("")) &&s.charAt(0)=='`' && s.charAt(1)=='`' && s.charAt(2)=='`'){ index=i; break; } } }else{ sb.append(s+"\n"); } }else{ sb.append(list.get(index)+"\n"); } } aPiFileInfoBean.setMessage(sb.toString()); if(flag! =null && ! flag.equals("") && aPiFileInfoBean! =null){ fileInfoBeanMap.put(aPiFileInfoBean.getCode(),aPiFileInfoBean); Map<Integer, APiFileInfoBean> fileInfoBeanMap1 = map.get(flag); if(fileInfoBeanMap1! =null) fileInfoBeanMap.putAll(fileInfoBeanMap1); map.put(flag,fileInfoBeanMap); } } private static void initDirectory(Map<String, Map<Integer,APiFileInfoBean>> map, File file) { File[] files = file.listFiles(); for (File fi : files) { if(! Fi. IsFile ()){system.out.println (fi+"--> is directory "); initDirectory(map,fi); }else{system.out.println (fi+"--> file "); initFile(map,fi); } } System.out.println("---"); } public static List<String> ReadFileDataToList( File file) { FileInputStream fis = null; InputStreamReader isr = null; BufferedReader br = null; List<String> list = new ArrayList<>(); try { fis = new FileInputStream(file); isr = new InputStreamReader(fis); br = new BufferedReader(isr); String dataLine = null; while((dataLine = br.readLine()) ! = null) { list.add(dataLine); }} catch (FileNotFoundException e) {system.out.println () + LLDB message (); } catch (IOException e) {system.out.println (" error: "+ LLDB message ()); } finally { try { if(br ! = null) br.close(); if(isr ! = null) isr.close(); if(fis ! = null) fis.close(); } catch (IOException e) { e.printStackTrace(); } } return list; }}Copy the code

5. Actual combat results

The result is a complete annotation.

Comments: APiFileInfo (” flag “)

The annotation is then parsed from the corresponding file according to the rules.

The xxxx.md file is stored under SRC /main/resources/md and contains the following contents:

Json [{"id":62564697, "slug":" 09C7DB472fa6 ", "slug":" 09C7DB472fa6 ", View_count :42, user :{"id":12724216, "nickname":"bdqfork", "slug":" a2329F464833 ", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } }, {"id":62564140, "slug":" ec3bd614dCB0 ", "title":"SLF4J log level and usage scenario ", "view_count":381, "user":{"id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } } ] ``` -- Code :410 ### 517 Json {"messageCode": 410, "message": "Name cannot be empty"} "-- code:509 #### 510 Test description" json {"messageCode": 509, "message": "Name length cannot exceed 15"} {"messageCode": 509, "message": "name cannot exceed 15"} {"messageCode": 509, "message": "name already exists"} {"messageCode": 509, "message": "Name exists, cannot be added"} ' '--Copy the code

The interface code is modified as follows:

This is not very convenient, there is no large volume of code, also will not appear particularly messy.

@getMapping ("/getStudentById") @apiOperation (value = "Obtain student information by student ID ",notes = "",position = 1) @APiFileInfo("/getStudentById") public Object getStudentById(String id){ return "id="+id; }Copy the code

The effect is as follows:

The default status code is 401,403, so we can turn it off:

 return new Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false)
Copy the code

It’s a little more succinct:

1. Keep an eye on this GitHub repository: github.com/8042965/swa…

2. Pull the warehouse code;

3. Find ways to incorporate it into your project;

4, the use of steps is very simple and the third part of the actual combat link in front of the annotations can be.

You can also add my wechat to communicate: Weiyi3700, or QQ: 8042965

You can also follow my wechat public account: TrueDei, reply swagger-Plugin can also get.

1. How can the @Order() annotation be used effectively when customizing annotations?

If you want to adjust the order in which this class is injected, you can also say priority.

So we can adjust the priority of the execution Order of the class by adjusting @order.

That’s what the @Order annotation does.

Default priority for this annotation:

If not specified, the default priority level is used.

As you can imagine, if you have something that needs to be loaded first, if you don’t specify it, or if you specify it at a low priority level, it probably won’t load. I had this problem.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
    int value() default 2147483647;
}
Copy the code

Take a look at the Ordered interface:

public interface Ordered {
    int HIGHEST_PRECEDENCE = -2147483648;
    int LOWEST_PRECEDENCE = 2147483647;

    int getOrder();
}
Copy the code

This class can put me in trouble, specific how to use, please see:

When I customize an annotation and want to inject it into a bean using Spring:

I checked online that @order (Ordered.HIGHEST_PRECEDENCE) is used to specify the Order, but some parameters cannot be loaded because they are specified without checking what they do.

@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class OperationPositionBulderPlugin implements OperationBuilderPlugin { ......... Ignore the code}Copy the code

You can see that I have specified the data for 200, but it doesn’t work.

Solution: Switch to a higher priority:

@Component

@Order(999999)
public class OperationPositionBulderPlugin implements OperationBuilderPlugin {
		........
}
Copy the code

Take a look again:

@order experiment, source:

Blog.csdn.net/yaomingyang…

@Component @Order(1) public class BlackPersion implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("----BlackPersion----"); } } @Component @Order(0) public class YellowPersion implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("----YellowPersion----"); }}Copy the code

Print result:

----YellowPersion----
----BlackPersion----
Copy the code

1, Bayi kitchen knife Springfox source code interpretation

2, Swagger

3. One sentence to let you know how intrusive code is

All code is placed in:

GitHub Repository: github.com/8042965/swa…