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…