原文网址:swagger--注解/Map问题/Content Type/导出为MarkDown_IT利刃出鞘的博客-CSDN博客
简介
说明
本文介绍Swagger的作用、为什么要使用Swagger、官网、注解、Map问题、导出为MarkDown等。
swagger的作用
1.接口的文档在线自动生成。
2.功能测试。
为什么要用swagger?
为了减少与其他团队平时开发期间的频繁沟通成本,传统做法我们会创建一份RESTful API文档来记录所有接口细节,然而这样的做法有以下几个问题:
- 由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下游的抱怨声不绝于耳。
- 随着时间推移,不断修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,除非有严格的管理机制,不然很容易导致不一致现象。
官网
REST API Documentation Tool | Swagger UI
github
swagger-springmvc: https://github.com/martypitt/swagger-springmvc
swagger-ui: https://github.com/swagger-api/swagger-ui
swagger-core: https://github.com/swagger-api/swagger-core
swagger-spec:https://github.com/swagger-api/swagger-spec
常用注解
注解 | 作用 | 示例 |
@Api | 用在Controller类上 | @Api(value = "用户管理类", description = "Operations about user") |
@ApiIgnore | 用在Controller类上。表示不为此Controller生成swagger接口 | @ApiIgnore |
@ApiOperation | 用在Controller方法上 | @ApiOperation( value = "Find purchase order by ID", notes = "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", response = Order, tags = {"Pet Store"}) |
@ApiImplicitParam | 用在Controller方法上或者@ApiImplicitParams里。给方法入参增加说明 | @ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User") |
@ApiImplicitParams | 用在Controller方法上。给方法入参增加说明 | @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long"), @ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User") }) |
@ApiParam | 可用在Controller方法、参数、属性上。 | public ResponseEntity<User> createUser(@RequestBody @ApiParam(value = "Created user object", required = true) User user) |
@ApiResponse | 用在controller的方法上或者@ApiResponses里 | @ApiResponse(code = 400, message = "Invalid user supplied") |
@ApiResponses | 用在controller的方法上 | @ApiResponses({ @ApiResponse(code = CommonStatus.OK, message = "操作成功"), @ApiResponse(code = CommonStatus.EXCEPTION, message = "服务器内部异常"), @ApiResponse(code = CommonStatus.FORBIDDEN, message = "权限不足") }) |
@ResponseHeader | 用在controller的方法上 | @ResponseHeader(name="head1",description="response head conf") |
@ApiModel | 用在返回对象类上 | @ApiModel |
@ApiModelProperty | 用在返回对象类的属性 | @ApiModelProperty(notes = "错误消息") |
@ApiImplicitParam
属性 |
取值 |
作用 |
paramType |
查询参数类型。此参数和@RequestBody冲突,最好不用 |
|
path |
以地址的形式提交数据 |
|
query |
直接跟参数完成自动映射赋值 |
|
body |
以流的形式提交 仅支持POST |
|
header |
参数在request headers 里边提交 |
|
form |
以form表单的形式提交。仅支持POST |
|
dataType |
参数的数据类型 只作为标志说明,并没有实际验证 |
|
Long |
||
String |
||
name |
接收参数名 |
|
value |
接收参数的意义描述 |
|
required |
参数是否必填 |
|
true |
必填 |
|
false |
非必填 |
|
defaultValue |
默认值 |
Content Type
其他网址
HTTP系列--Content type_feiying0canglang的博客-CSDN博客
Swagger之http content-type 实践 – 想你所想
Swagger2企业实战 - 简书
swagger-ui使用问题记录_进步源于总结-CSDN博客_swagger content-type
Map问题
其他网址
Swagger2 关于Map参数在API文档中展示详细参数以及参数说明_hellopeng1的博客-CSDN博客
问题描述
Swagger2 (SpringFox)关于Map参数生成的API文档中没有详细Json结构说明,问题如下图所示:
此种方式生成的Api文档中的请求参数如下:
如果是这样的参数类型的会让查看API的人员无法清晰的知道如何请求API文档。
解决方案
@ApiOperation(value = "not use")
@ApiImplicitParam(name = "params" , paramType = "body",examples = @Example({
@ExampleProperty(value = "{'user':'id'}", mediaType = "application/json")
}))
@PostMapping("/xxx")
public void test(Map<String,String> params){}
2.8.0至2.9.0之间解决方案
上边这种写法在SpringFox版本2.8.0至2.9.0之间好像没有实现@ApiImplicitParam的examples的用法,还是属于issue的状态,下面是关于这两个issue的说明:
Springfox Reference Documentation
spring boot - How can I manually describe an example input for a java @RequestBody Map<String, String>? - Stack Overflow
SpringFox 提供给我们了一个ParameterBuilderPlugin接口,通过这个接口我们可以在SpringFox构造Map参数映射的ModelRef时使用javassist动态的生成类,并把这个map参数的modelRef对象指向我们动态生成的具体Class对象(通过自定义注解在Map参数上生成可表示JSON结构的类),具体实现如下(求方便的同学可以把下面3个类直接Copy到自己的代码中即可):
package com.telepay.service.controller.agent;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import com.telepay.service.controller.agent.annotation.ApiJsonObject;
import com.telepay.service.controller.agent.annotation.ApiJsonProperty;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.IntegerMemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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.util.Map;
@Component
@Order //plugin加载顺序,默认是最后加载
public class MapApiReader implements ParameterBuilderPlugin {
@Autowired
private TypeResolver typeResolver;
@Override
public void apply(ParameterContext parameterContext) {
ResolvedMethodParameter methodParameter = parameterContext.resolvedMethodParameter();
if (methodParameter.getParameterType().canCreateSubtype(Map.class) || methodParameter.getParameterType().canCreateSubtype(String.class)) { //判断是否需要修改对象ModelRef,这里我判断的是Map类型和String类型需要重新修改ModelRef对象
Optional<ApiJsonObject> optional = methodParameter.findAnnotation(ApiJsonObject.class); //根据参数上的ApiJsonObject注解中的参数动态生成Class
if (optional.isPresent()) {
String name = optional.get().name(); //model 名称
ApiJsonProperty[] properties = optional.get().value();
parameterContext.getDocumentationContext().getAdditionalModels().add(typeResolver.resolve(createRefModel(properties, name))); //像documentContext的Models中添加我们新生成的Class
parameterContext.parameterBuilder() //修改Map参数的ModelRef为我们动态生成的class
.parameterType("body")
.modelRef(new ModelRef(name))
.name(name);
}
}
}
private final static String basePackage = "com.xx.xxx.in.swagger.model."; //动态生成的Class名
/**
* 根据propertys中的值动态生成含有Swagger注解的javaBeen
*/
private Class createRefModel(ApiJsonProperty[] propertys, String name) {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(basePackage + name);
try {
for (ApiJsonProperty property : propertys) {
ctClass.addField(createField(property, ctClass));
}
return ctClass.toClass();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据property的值生成含有swagger apiModelProperty注解的属性
*/
private CtField createField(ApiJsonProperty property, CtClass ctClass) throws NotFoundException, CannotCompileException {
CtField ctField = new CtField(getFieldType(property.type()), property.key(), ctClass);
ctField.setModifiers(Modifier.PUBLIC);
ConstPool constPool = ctClass.getClassFile().getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation ann = new Annotation("io.swagger.annotations.ApiModelProperty", constPool);
ann.addMemberValue("value", new StringMemberValue(property.description(), constPool));
if (ctField.getType().subclassOf(ClassPool.getDefault().get(String.class.getName())))
ann.addMemberValue("example", new StringMemberValue(property.example(), constPool));
if (ctField.getType().subclassOf(ClassPool.getDefault().get(Integer.class.getName())))
ann.addMemberValue("example", new IntegerMemberValue(Integer.parseInt(property.example()), constPool));
attr.addAnnotation(ann);
ctField.getFieldInfo().addAttribute(attr);
return ctField;
}
private CtClass getFieldType(String type) throws NotFoundException {
CtClass fileType = null;
switch (type) {
case "string":
fileType = ClassPool.getDefault().get(String.class.getName());
break;
case "int":
fileType = ClassPool.getDefault().get(Integer.class.getName());
break;
}
return fileType;
}
@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
ApiJsonObject注解和ApiJsonProperty注解的实现:
package com.telepay.service.controller.agent.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 ApiJsonObject {
ApiJsonProperty[] value(); //对象属性值
String name(); //对象名称
}
package com.telepay.service.controller.agent.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiJsonProperty {
String key(); //key
String example() default "";
String type() default "string"; //支持string 和 int
String description() default "";
}
需要特殊说明一下,我们每一个ApiOperation都是按一个RequestMapping来加载的每一个RequestMapping在加载的时候都会经过许多不同类型的Plugin的处理,而负责管理全局的ModelRef的Plugin是OperationModelsProviderPlugin这个处理RequestMapping时会检测有没有还没有被放到全局的ModelRef对象(而我们放到DocumentContext的对象就是此时被加载的),但是OperationModelsProviderPlugin类型的执行顺序是优先于ParameterBuilderPlugin类型的 ,所以这里就有了一个小问题,如果我们新建的ModelRef是最后一个被处理的RequestMapping那我们新建的ModelRef就没有机会被OperationModelsProviderPlugin放到全局的ModelRef中了,所以解决方法就是在这个Controller中添加一个无用的方法但是这个方法名要足够的长(这个Document范围内即可)保证这个方法才是被SpringFox最后解析的,让我们每个ModelRef都能被OperationModelsProviderPlugin装载进来,如果想看SpringFox这部分具体实现的可以关注下DocumentationPluginsManager这个类,打个断点(断点在OperationModelsProviderPlugin和ParameterBuilderPlugin这两个plugin的调用地方)应该就能理解了:
Ok做完准备工作,来看下我们在controller层如何使用我们新开发的功能:
@ApiOperation(value = "Login", tags = "login")
@PutMapping
public void auth(@ApiJsonObject(name = "login_model", value = {
@ApiJsonProperty(key = "mobile", example = "18614242538", description = "user mobile"),
@ApiJsonProperty(key = "password", example = "123456", description = "user password")
})
@RequestBody Map<String, String> params) {
xxxxxxxxxxxxxx
}
@ApiOperation(value = "none")
@GetMapping
public void authaaaa(){
}
效果图:
注意
这个解决方法是比较繁琐的,但是也实现了在Api文档中展示Map参数应要接收的详细对象。如果你并没有很多Map参数需要表明结构,建议你新建个Class做ModelRef就可以了,或者新建个ModelRequestVo也是好的。最后如果同学们发现有更好的解决方法请告知,以免误导其他人,谢谢~
补充:这个只是个DEMO并没有经过完善的测试,不建议生产使用,个人建议还是新建个对象来做参数接收,代码可读性也要高些,好维护,也好进行参数校验等。
swagger导出markdown
其他网址
将Swagger2文档导出为HTML或markdown等格式离线阅读_个人文章 - SegmentFault 思否
简介
我们日常使用swagger接口文档的时候,有的时候需要接口文档离线访问,如将文档导出为html、markdown格式。又或者我们不希望应用系统与swagger接口文档使用同一个服务,而是导出HTML之后单独部署,这样做保证了对接口文档的访问不影响业务系统,也一定程度提高了接口文档的安全性。核心的实现过程就是:
- 在swagger2接口文档所在的应用内,利用swagger2markup将接口文档导出为adoc文件,也可以导出markdown文件。
- 然后将adoc文件转换为静态的html格式,可以将html发布到nginx或者其他的web应用容器,提供访问(本文不会讲html静态部署,只讲HTML导出)。
注意:adoc是一种文件格式,不是我的笔误。不是doc文件也不是docx文件。
法1:swagger2markup依赖+代码配置
1.引入依赖
在已经集成了swagger2的应用内,通过maven坐标引入相关依赖类库,pom.xml代码如下:
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup</artifactId>
<version>1.3.3</version>
</dependency>
swagger2markup用于将swagger2在线接口文档导出为html,markdown,adoc等格式文档,用于静态部署或离线阅读。
2.生成adoc或者markdown
下边这两种生成的代码都是参考的 swagger2markup源码,见最后。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class DemoApplicationTests {
// 输出Ascii格式到单文件
@Test
public void generateAsciiDocs() throws Exception {
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.ASCIIDOC) //设置生成格式
.withOutputLanguage(Language.ZH) //设置语言中文还是其他语言
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8888/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("src/main/resources/docs/asciidoc"));
}
// 输出Markdown到单文件
@Test
public void generateMarkdownDocsToFile() throws Exception {
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.MARKDOWN)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8888/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("src/main/resources/docs/markdown"));
}
}
- 使用RunWith注解和SpringBootTest注解,启动应用服务容器。 SpringBootTest.WebEnvironment.DEFINED_PORT表示使用application.yml定义的端口,而不是随机使用一个端口进行测试,这很重要。
- Swagger2MarkupConfig 是输出文件的配置,如文件的格式和文件中的自然语言等
- Swagger2MarkupConverter的from表示哪一个HTTP服务作为资源导出的源头(JSON格式),可以自己访问试一下这个链接。8888是我的服务端口,需要根据你自己的应用配置修改。
- toFile表示将导出文件存放的位置,不用加后缀名。也可以使用toFolder表示文件导出存放的路径。二者区别在于使用toFolder导出为文件目录下按标签TAGS分类的多个文件,使用toFile是导出一个文件(toFolder多个文件的合集)。
问题解决
问题
原因
产生异常的原因已经有人在github的issues上给出解释了:当你使用swagger-core版本大于等于1.5.11,并且swagger-models版本小于1.5.11就会有异常发生。所以我们显式的引入这两个jar,替换掉swagger2默认引入的这两个jar。
解决方法
引入依赖
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-core</artifactId>
<version>1.5.16</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.16</version>
</dependency>
源码
生成文档的代码可以直接参考swagger2markup-xxx.jar的代码:
package io.github.swagger2markup.main;
import io.github.swagger2markup.GroupBy;
import io.github.swagger2markup.Language;
import io.github.swagger2markup.Swagger2MarkupConfig;
import io.github.swagger2markup.Swagger2MarkupConverter;
import io.github.swagger2markup.builder.Swagger2MarkupConfigBuilder;
import io.github.swagger2markup.markup.builder.MarkupLanguage;
import java.net.URL;
import java.nio.file.Paths;
/**
* @author :cyf
* @date :Created in 2019/7/4 16:15
* @description:
* @modified By:
*/
public class MakeUp {
public static void main(String[] args) {
MakeUp m = new MakeUp();
try {
m.generateMarkdownDocs("http://localhost:8610/v2/api-docs?group=all","d:/doc/md/api");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成AsciiDocs格式文档
* @throws Exception
*/
public void generateAsciiDocs() throws Exception {
// 输出Ascii格式
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.ASCIIDOC)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8061/v2/api-docs"))
.withConfig(config)
.build()
.toFolder(Paths.get("./docs/asciidoc/generated"));
}
/**
* 生成Confluence格式文档
* @throws Exception
*/
public void generateConfluenceDocs() throws Exception {
// 输出Confluence使用的格式
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.CONFLUENCE_MARKUP)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8061/v2/api-docs"))
.withConfig(config)
.build()
.toFolder(Paths.get("./docs/confluence/generated"));
}
/**
* 生成AsciiDocs格式文档,并汇总成一个文件
* @throws Exception
*/
public void generateAsciiDocsToFile() throws Exception {
// 输出Ascii到单文件
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.ASCIIDOC)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8082/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("./docs/asciidoc/generated/all"));
}
/**
* 生成Markdown格式文档,并汇总成一个文件
* @throws Exception
*/
public void generateMarkdownDocsToFile() throws Exception{
// 输出Markdown到单文件
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.MARKDOWN)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8061/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("./docs/markdown/generated/all"));
}
/**
* 生成Markdown格式文档
* @throws Exception
*/
public void generateMarkdownDocs() throws Exception {
// 输出Markdown格式
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.MARKDOWN)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL("http://localhost:8061/v2/api-docs"))
.withConfig(config)
.build()
.toFolder(Paths.get("./docs/markdown/generated"));
}
public static void generateMarkdownDocs(String swaggerJsonUrl,String filePath) throws Exception {
// 输出Markdown格式
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.MARKDOWN)
.withOutputLanguage(Language.EN)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();
Swagger2MarkupConverter.from(new URL(swaggerJsonUrl))
.withConfig(config)
.build()
.toFile(Paths.get(filePath));
}
}
法2:swagger2markup-maven-plugin
<plugin>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>1.3.1</version>
<configuration>
<swaggerInput>http://localhost:8888/v2/api-docs</swaggerInput><!---swagger-api-json路径-->
<outputDir>src/main/resources/docs/asciidoc</outputDir><!---生成路径-->
<config>
<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage><!--生成格式-->
</config>
</configuration>
</plugin>
然后运行插件就可以了,如下图:
法3:maven插件生成HTML文档
有了HTML接口文档你想转成其他各种格式的文档就太方便了,有很多工具可以使用。
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.6</version>
<configuration>
<!--asciidoc文件目录-->
<sourceDirectory>src/main/resources/docs</sourceDirectory>
<!---生成html的路径-->
<outputDirectory>src/main/resources/html</outputDirectory>
<backend>html</backend>
<sourceHighlighter>coderay</sourceHighlighter>
<attributes>
<!--导航栏在左-->
<toc>left</toc>
<!--显示层级数-->
<!--<toclevels>3</toclevels>-->
<!--自动打数字序号-->
<sectnums>true</sectnums>
</attributes>
</configuration>
</plugin>
adoc的sourceDirectory路径必须和第三小节中生成的adoc文件路径一致。然后按照下图方式运行插件。
HTMl接口文档显示的效果如下