C/C++教程

swagger--注解/Map问题/Content Type/导出为MarkDown

本文主要是介绍swagger--注解/Map问题/Content Type/导出为MarkDown,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

原文网址: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
? - Stack Overflow" href="https://stackoverflow.com/questions/41861164/how-can-i-manually-describe-an-example-input-for-a-java-requestbody-mapstring" rel="external nofollow" target="_blank">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 思否

github代码
github文档

简介

        我们日常使用swagger接口文档的时候,有的时候需要接口文档离线访问,如将文档导出为html、markdown格式。又或者我们不希望应用系统与swagger接口文档使用同一个服务,而是导出HTML之后单独部署,这样做保证了对接口文档的访问不影响业务系统,也一定程度提高了接口文档的安全性。核心的实现过程就是:

  1. 在swagger2接口文档所在的应用内,利用swagger2markup将接口文档导出为adoc文件,也可以导出markdown文件。
  2. 然后将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接口文档显示的效果如下

这篇关于swagger--注解/Map问题/Content Type/导出为MarkDown的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!