Gitee link

Spring Boot version: 2.3.4.RELEASE

Swagger can help us generate interface documents and reduce our workload. However, in most scenarios, the interface documents generated by Swagger can only serve as reference. We still need to communicate a lot with the front end on interface documents. I wanted to make the interface documentation as clear as possible.

directory

  • An introduction to Swagger
  • Swagger
  • Knife4j library UI enhancements and extensions
  • Global parameters for online debugging
  • The same class is compatible with mandatory properties of different interface parameters

An introduction to swagger

Create a dependency class for swagger

Pom. XML:

<!--Swagger UI API文档-->
<! -- http://localhost:8888/swagger-ui.html -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
Copy the code

Config class Swagger2Config:

package com.cc.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi(a) {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // Generate API documentation for the controller under the current package
                .apis(RequestHandlerSelectors.basePackage("com.cc"))
                // Generate Api documentation for controller annotated with @API
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                // Generate API documentation for methods annotated @apiOperation
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                .groupName("v1");
    }

    private ApiInfo apiInfo(a) {
        return new ApiInfoBuilder()
                .title("swaggerDemo")
                .description("Interface document generated by swaggerDemo")
                .contact("cc")
                .termsOfServiceUrl("http://www.xxxx.cc")// (not visible) clause address, not required for internal use
                .version("v1") .build(); }}Copy the code

Then let’s write two interfaces to test this:

UserController:

package com.cc.controller;

import com.cc.model.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    /** * Register interface *@author cc
     * @dateThe 2021-11-15 15:04 * /
    @PostMapping("/register")
    public String register(@RequestBody User user) {
        return "Registration successful";
    }

    /** * Login interface *@author cc
     * @dateThe 2021-11-15 days * /
    @PostMapping("/login")
    public String login(@RequestBody User user) {
        return "Login successful"; }}Copy the code

Entity class User:

package com.cc.model;

/** * User object **@author cc
 * @dateThe 2021-11-15 15:02 * /
public class User {

    / / user name
    private String username;

    / / password
    private String password;

    / / age
    private Integer age;

    / / address
    privateString address; . }Copy the code

Start the program, visit http://localhost:8888/swagger-ui.html can see interface document page.

Swagger

The default generated interface document has no interface description and no field description, so this is not enough. We need to use the following annotations to enrich our interface document:

  • @api, modifies the interface class
  • @apiOperation, decorates the interface function
  • @apiModel, decorates the model class
  • @apiModelProperty, decorates the field

Now change the UserController code to:

package com.cc.controller;

import com.cc.model.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@API (tags = "User module ")
@RestController
public class UserController {
    @apiOperation (value = "register ")
    @postmapping ("/register", notes = "Here is the description of the interface ")
    public String register(@RequestBody User user) {
        return "Registration successful";
    }

    @apiOperation (value = "login ", notes =" here is the description of the interface ")
    @PostMapping("/login")
    public String login(@RequestBody User user) {
        return "Login successful"; }}Copy the code

User model class changed to:

package com.cc.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@apiModel (value = "user object ")
public class User {
    @apiModelProperty (value = "username ")
    private String username;

    @apiModelProperty (value = "password ")
    private String password;

    @apiModelProperty (value = "age ")
    private Integer age;

    @apiModelProperty (value = "address ")
    privateString address; . }Copy the code

Visit http://localhost:8888/swagger-ui.html you can see the effect again.

Knife4j library UI enhancements and extensions

Native Swagger interface doesn’t accord with our habits, have a UI enhancement library knife4j to Swagger interface is reformed, and strengthen the function of many development.

Let’s change the dependence of Swagger in poM to:

<!--Swagger UI API文档-->
<! -- http://localhost:8888/swagger-ui.html -->
<! --<dependency>-->
<! -- <groupId>io.springfox</groupId>-->
<! -- <artifactId>springfox-swagger2</artifactId>-->
<! - < version > 2.9.2 < / version > -- >
<! --</dependency>-->
<! --<dependency>-->
<! -- <groupId>io.springfox</groupId>-->
<! -- <artifactId>springfox-swagger-ui</artifactId>-->
<! - < version > 2.9.2 < / version > -- >
<! --</dependency>-->

<! --Swagger Knife Enhanced UI API
<! -- http://localhost:8080/doc.html -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.9</version>
</dependency>
Copy the code

Knife4j has Swagger built in, so there is no dependency on native Sawgger.

Using Knife4J, our configuration class will also change to:

Swagger2Config:

package com.cc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/** * Swagger configuration class *@author cc
 * @dateThe 2021-07-09 men that * /
@Configuration
@EnableSwagger2WebMvc
public class Swagger2Config {

    @Bean
    public Docket createRestApi(a) {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // Generate API documentation for the controller under the current package
                .apis(RequestHandlerSelectors.basePackage("com.cc"))
                // Generate Api documentation for controller annotated with @API
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                // Generate API documentation for methods annotated @apiOperation
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo(a) {
        Contact contact = new Contact("chen"."http://www.dchen.cc"."[email protected]");
        return new ApiInfoBuilder()
                .title("mall-business")
                .description("Mymall interface document")
                .version("v1")
                .contact(contact)
                // (not visible) clause address, not required for internal use
                .termsOfServiceUrl("http://www.dchen.cc") .build(); }}Copy the code

After using knife4j, access interface of the document link becomes: http://localhost:8888/doc.html

The UI enhancements are, in my opinion, much better looking.

Knife4j extension features

Knife4j has a number of extensions that are not detailed here

We only use two extended annotations: @apisupPort and @apiOperationSupport

In UserController, change it to:

package com.cc.controller;

import com.cc.model.User;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@ApiSupport(author = "cc", order = 1)
@API (tags = "User module ")
@RestController
public class UserController {
    private static final int register = 1;
    private static final int login = 2;

    @ApiOperationSupport(order = register)
    @apiOperation (value = "register ", notes =" here is the description of the interface ")
    @PostMapping("/register")
    public String register(@RequestBody User user) {
        return "Registration successful";
    }

    @ApiOperationSupport(order = login)
    @apiOperation (value = "login ", notes =" here is the description of the interface ")
    @PostMapping("/login")
    public String login(@RequestBody User user) {
        return "Login successful"; }}Copy the code
  • @apisupport (author = “cc”, order = 1) indicates that the author of this interface class is CC, and the list on the left side of the interface document is 1. If there is another interface class whose order is 2, it will be sorted in ascending order.
  • @APIOperationSupport (order = register), indicating the display order of this interface, also in ascending order

The reason for using these two annotations is that the interface documentation is out of order, so we put the interface modules and interfaces in the order we want them to be, so that front-end developers can look at them better.

Global parameters for online debugging

We can already call interfaces from the interface documentation page, but usually we need to call other interfaces with tokens returned by successful login, so we can do this:

  1. Left of the interface document page
  2. Document management
  3. Global parameter Setting
  4. Add parameters

This will be carried automatically when debugging the interface.

The same class is compatible with mandatory properties of different interface parameters

[username, password, age, address] [username, password, age, address] [username, password, age, address] [username, password, age, address] [username, password], we must do a good specification in this place, otherwise it will definitely cause trouble to the front end.

The expected results are:

  • Request parameters for the registered interface:

    The parameter name Parameters that Request type Whether must
    username The user name string true
    password password string true
    age age integer false
    address address string false
  • Request parameters for the login interface:

    The parameter name Parameters that Request type Whether must
    username The user name string true
    password password string true

There are two options. One is to create a separate object class for each interface, like this:

package com.cc.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@apiModel (value = "user registration request parameter ")
public class RegisterUserVo {
    @APIModelProperty (value = "username ", Required = true)
    private String username;

    @APIModelProperty (value = "password ", Required = true)
    private String password;

    @apiModelProperty (value = "age ")
    private Integer age;

    @apiModelProperty (value = "address ")
    privateString address; . }Copy the code

This is not impossible, but it can be ridiculous when there are too many interfaces.

The scheme I’m using now is to use custom annotations in the interface to indicate which fields are required and which are not.

Without further ado, create three new notes:

  • @apiigp, which fields do not need
  • @apineed, what fields do you want
  • @apiRequired, which fields are required

@ ApiIgp:

package com.cc.config;

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

/** * Custom AOP, Swagger document field ignore field *@author cc
 * @dateThe 2021-09-08 * / departed
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIgp {
    String[] value();   // Fields to ignore
}
Copy the code

@ ApiNeed:

package com.cc.config;

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

/** * Custom AOP, Swagger document field required field, but does not represent mandatory, to specify mandatory need to use@ApiRequiredNote *@author cc
 * @dateThe 2021-09-08 1 * /
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNeed {
    String[] value();   // The required field
}
Copy the code

@ ApiRequired:

package com.cc.config;

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

/** * Custom AOP, swagger document field required *@author cc
 * @dateThe 2021-09-08 1 * /
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRequired {
    String[] value();   // Mandatory field
}
Copy the code

Then comes the most critical MyParameterBuilderPlugin class:

package com.cc.config;

import com.fasterxml.classmate.TypeResolver;
import io.swagger.annotations.ApiModelProperty;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;

/** * Swagger2 /** * Swagger2 https://www.jianshu.com/p/09a4619fb0f7 https://www.jianshu.com/p/540016c84635 * *@author cc
 * @dateThe 2021-09-08 15:01 * /
@Component
@Order
public class MyParameterBuilderPlugin implements ParameterBuilderPlugin {

    @Autowired
    private TypeResolver typeResolver;

    @Override
    public void apply(ParameterContext context) {

        ResolvedMethodParameter methodParameter = context.resolvedMethodParameter();

        // Custom annotationsOptional<ApiIgp> apiIgp = methodParameter.findAnnotation(ApiIgp.class); Optional<ApiNeed> apiNeed = methodParameter.findAnnotation(ApiNeed.class); Optional<ApiRequired> apiRequired = methodParameter.findAnnotation(ApiRequired.class); Class<? > originClass = context.resolvedMethodParameter().getParameterType().getErasedType(); String[] requireds =null;
        if (apiRequired.isPresent()) {
            requireds = apiRequired.get().value();
        }

        if (apiIgp.isPresent() || apiNeed.isPresent()) {
            Random random = new Random();
            // Add a random number to make it a specific model, otherwise it will be overridden by the native model
            String modelName = originClass.getSimpleName() + random.nextInt(1000);
            String[] properties;

            if (apiIgp.isPresent()) {
                properties = apiIgp.get().value();
                context.getDocumentationContext()
                        .getAdditionalModels()
                        // Add our newly generated Class to the Models for the documentContext
                        .add(typeResolver.resolve(createRefModelIgp(properties, originClass.getPackage() + "." + modelName, originClass, requireds)));
            }
            // Need (whitelist)
            if (apiNeed.isPresent()) {
                properties = apiNeed.get().value();
                context.getDocumentationContext()
                        .getAdditionalModels()
                        // Add our newly generated Class to the Models for the documentContext
                        .add(typeResolver.resolve(createRefModelNeed(properties, originClass.getPackage() + "." + modelName, originClass, requireds)));
            }

            // Change the Map parameter ModelRef to our dynamically generated class
            context.parameterBuilder()
                    .parameterType("body")
                    .modelRef(newModelRef(modelName)) .name(modelName); }}/** * Create custom mode for swagger2@paramProperties Parameter to exclude *@paramName Model Name *@param origin     originClass
     * @return r
     */
    privateClass<? > createRefModelIgp(String[] properties, String name, Class<? > origin, String[] requireds) { ClassPool pool = ClassPool.getDefault();// Create a class dynamically
        CtClass ctClass = pool.makeClass(name);
        try {
            Field[] fields = origin.getDeclaredFields();
            List<Field> fieldList = Arrays.asList(fields);
            List<String> ignoreProperties = Arrays.asList(properties);
            // Filter out the properties parametersList<Field> dealFields = fieldList.stream().filter(s -> ! ignoreProperties.contains(s.getName())).collect(Collectors.toList()); addField2CtClass(dealFields, origin, ctClass, requireds);return ctClass.toClass();
        } catch (Exception e) {
// log.error(" Swagger section error ", e);
            e.printStackTrace();
            return null; }}/** * Create custom mode for Swagger2@paramProperties Required parameter *@paramName Model Name *@param origin     originClass
     * @return r
     */
    privateClass<? > createRefModelNeed(String[] properties, String name, Class<? > origin, String[] requireds) { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass(name);try {
            Field[] fields = origin.getDeclaredFields();
            List<Field> fieldList = Arrays.asList(fields);
            List<String> ignoreProperties = Arrays.asList(properties);
            // Filter out non-properties parameters
            List<Field> dealFields = fieldList.stream().filter(s -> ignoreProperties.contains(s.getName())).collect(Collectors.toList());
            addField2CtClass(dealFields, origin, ctClass, requireds);
            return ctClass.toClass();
        } catch (Exception e) {
// log.error(" Swagger section error ", e);
            e.printStackTrace();
            return null; }}private void addField2CtClass(List
       
         dealFields, Class
         origin, CtClass ctClass, String[] requireds)
        throws NoSuchFieldException, NotFoundException, CannotCompileException {
        // reverse order traversal
        for (int i = dealFields.size() - 1; i >= 0; i--) {
            Field field = dealFields.get(i);
            CtField ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
            ctField.setModifiers(Modifier.PUBLIC);
            ApiModelProperty ampAnno = origin.getDeclaredField(field.getName()).getAnnotation(ApiModelProperty.class);
            String attributes = Optional.ofNullable(ampAnno).map(ApiModelProperty::value).orElse("");
            // Add the model attribute description
            if(! StringUtils.isEmpty(attributes)) { 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(attributes, constPool));
                // When mandatory parameters are specified
                if(requireds ! =null && Arrays.asList(requireds).contains(field.getName())) {
                    ann.addMemberValue("required".new BooleanMemberValue(true, constPool)); } attr.addAnnotation(ann); ctField.getFieldInfo().addAttribute(attr); } ctClass.addField(ctField); }}@Override
    public boolean supports(DocumentationType delimiter) {
        return true; }}Copy the code

Finally, modify the interface as follows:

package com.cc.controller;

import com.cc.config.ApiNeed;
import com.cc.config.ApiRequired;
import com.cc.model.User;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@ApiSupport(author = "cc", order = 1)
@API (tags = "User module ")
@RestController
public class UserController {
    private static final int register = 1;
    private static final int login = 2;

    @ApiOperationSupport(order = register)
    @apiOperation (value = "register ", notes =" here is the description of the interface ")
    @PostMapping("/register")
    public String register(@ApiRequired ({"username"."password"})
                               @RequestBody User user) {
        return "Registration successful";
    }
    @ApiOperationSupport(order = login)
    @apiOperation (value = "login ", notes =" here is the description of the interface ")
    @PostMapping("/login")
    public String login(@ApiNeed ({"username"."password"})
                            @ApiRequired ({"username"."password"})
                            @RequestBody User user) {
        return "Login successful"; }}Copy the code

Now you’re done, the front now you can see the enhanced interface document page, you can online debugging, is also treated with friendly interface display order, and each interface has the author, interface description, parameter descriptions and parameters of the necessary information, believe that as long as the developer’s interface description is clear, and the needs of the project clear enough, You can reduce a lot of communication success at the front and back end.

Finally, if the front end person is a girl, I don’t recommend reading this article, native Swagger is enough # doghead