基于Spring的Restful接口生成工具

场景

有时候需要为前端开发者提供Restful Api说明文档,通过word文档创建和修改非常耗时,希望有一种比较便捷的第三方库可以减少生成Api说明文档的工作量

基于Spring的Restful Api生成工具

术语解析

  • Springfox-swagger2
    Swagger是一个可以生成基于多种语言编写的Restful Api的文档生成工具,详见这里
    查看Springfox-swagger2注解文档,请点击这里
  • Swagger2markup
    Swagger2markup是一个使用java编写的将Swagger语义转换成markdown、asciidoc文本格式的开源项目
    Swagger2Markerup的详细说明请见这里
  • Asciidoc
    AsciiDoc是一种MarkDown的扩展文本格式,AsciiDoc相比MarkDown更适合编写类似API文档,学术文档这样的文档。
    详见这里
  • Asciidoctor
    asciidoctor是一个由ruby编写的可以将 asciidoc转换成html、pdf的开源项目,这个项目有java版本和maven插件,详见这里

接口生成原理

  • generate an up-to-date Swagger JSON file during an unit or integration test
    使用Springfox-swagger2生成swagger json文件
  • convert the Swagger JSON file into AsciiDoc
    使用Swagger2markup将swagger json文件转换成asciidoc文档片段
  • add hand-written AsciiDoc documentation
    编写asciidoc的文档(主要是组装步骤2中生成的asciidoc文档片段)
  • convert AsciiDoc into HTML and PDF
    使用Asciidoctor将asciidoc转换成HTML 或pdf

Swagger部署说明

  1. 在pom引入下面依赖:
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.4.0</version>
            <scope>test</scope>
        </dependency>
  1. 配置一个SwaggerConfig
@EnableSwagger2
@Configuration
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {

    @Bean
    public Docket restApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .securitySchemes(asList(
                        new OAuth(
                            "petstore_auth",
                            asList(new AuthorizationScope("write_pets", "modify pets in your account"),
                                    new AuthorizationScope("read_pets", "read your pets")),
                                Arrays.<GrantType>asList(new ImplicitGrant(new LoginEndpoint("http://petstore.swagger.io/api/oauth/dialog"), "tokenName"))
                        ),
                        new ApiKey("api_key", "api_key", "header")
                ))
                .select()
                .paths(Predicates.and(ant("/**"), Predicates.not(ant("/error")), Predicates.not(ant("/management/**")), Predicates.not(ant("/management*"))))
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger Petstore")
                .description("Petstore API Description")
                .contact(new Contact("TestName", "http:/test-url.com", "test@test.de"))
                .license("Apache 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .version("1.0.0")
                .build();
    }
}
  1. 运行Spring项目
    运行项目后会发布Spring的路由器多了若干个对外的接口,其中一个是/v2/api-docs/
    通过这个接口可以获取到由Swagger生成的所有API接口的元数据。
  2. Api界面部署
    呈现由Swagger生成的API大概有两种方法(目前只找到两种)
  • Swagger自带有Swagger-UI可以直观显示API接口说明并可以在线调试
  • 使用Swagger2Markerup插件和AsciiDoc插件可以将Swagger生成的JSON元数据转换成HTML、PDF

注意:springfox-swagger2是依赖与Spring MVC框架的!!

Swagger-UI部署

  1. 引入Swagger-UI依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.5.0</version>
</dependency>
  1. 运行Spring项目,并访问htttp://[host]:[ip]/swagger-ui.html
Paste_Image.png
Paste_Image.png

注意:必须首先部署Swagger

Swagger2Markerup与AsciiDoc插件部署

  1. maven插件部署
<!-- First, use the swagger2markup plugin to generate asciidoc -->
            <plugin>
                <groupId>io.github.swagger2markup</groupId>
                <artifactId>swagger2markup-maven-plugin</artifactId>
                <version>${swagger2markup.version}</version>
                <dependencies>
                    <dependency>
                        <groupId>io.github.swagger2markup</groupId>
                        <artifactId>swagger2markup-import-files-ext</artifactId>
                        <version>${swagger2markup.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>io.github.swagger2markup</groupId>
                        <artifactId>swagger2markup-spring-restdocs-ext</artifactId>
                        <version>${swagger2markup.version}</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <swaggerInput>${swagger.input}</swaggerInput>
                    <outputDir>${generated.asciidoc.directory}</outputDir>
                    <config>
                        <swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
                        <swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>      <swagger2markup.extensions.dynamicOverview.contentPath>${project.basedir}/src/docs/asciidoc/extensions/overview</swagger2markup.extensions.dynamicOverview.contentPath>
                        <swagger2markup.extensions.dynamicDefinitions.contentPath>${project.basedir}/src/docs/asciidoc/extensions/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
                        <swagger2markup.extensions.dynamicPaths.contentPath>${project.basedir}/src/docs/asciidoc/extensions/paths</swagger2markup.extensions.dynamicPaths.contentPath>
                        <swagger2markup.extensions.dynamicSecurity.contentPath>${project.basedir}src/docs/asciidoc/extensions/security/</swagger2markup.extensions.dynamicSecurity.contentPath>

                        <swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
                        <swagger2markup.extensions.springRestDocs.defaultSnippets>false</swagger2markup.extensions.springRestDocs.defaultSnippets>
                    </config>
                </configuration>
                <executions>
                    <execution>
                        <phase>test</phase>
                        <goals>
                            <goal>convertSwagger2markup</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- Run the generated asciidoc through Asciidoctor to generate
                 other documentation types, such as PDFs or HTML5 -->
            <plugin>
                <groupId>org.asciidoctor</groupId>
                <artifactId>asciidoctor-maven-plugin</artifactId>
                <version>1.5.3</version>
                <!-- Include Asciidoctor PDF for pdf generation -->
                <dependencies>
                    <dependency>
                        <groupId>org.asciidoctor</groupId>
                        <artifactId>asciidoctorj-pdf</artifactId>
                        <version>1.5.0-alpha.10.1</version>
                    </dependency>
                </dependencies>
                <!-- Configure generic document generation settings -->
                <configuration>
                    <sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
                    <sourceDocumentName>index.adoc</sourceDocumentName>
                    <attributes>
                        <doctype>book</doctype>
                        <toc>left</toc>
                        <toclevels>3</toclevels>
                        <numbered></numbered>
                        <hardbreaks></hardbreaks>
                        <sectlinks></sectlinks>
                        <sectanchors></sectanchors>
                        <generated>${generated.asciidoc.directory}</generated>
                    </attributes>
                </configuration>
                <!-- Since each execution can only handle one backend, run
                     separate executions for each desired output type -->
                <executions>
                    <execution>
                        <id>output-html</id>
                        <phase>test</phase>
                        <goals>
                            <goal>process-asciidoc</goal>
                        </goals>
                        <configuration>
                            <backend>html5</backend>
                            <outputDirectory>${asciidoctor.html.output.directory}</outputDirectory>
                        </configuration>
                    </execution>
                       <!--  
                    <execution>
                        <id>output-pdf</id>
                        <phase>test</phase>
                        <goals>
                            <goal>process-asciidoc</goal>
                        </goals>
                        <configuration>
                            <backend>pdf</backend>
                            <outputDirectory>${asciidoctor.pdf.output.directory}</outputDirectory>
                        </configuration>
                    </execution>
                   -->
                </executions>
            </plugin>
  1. 编写文档生成脚本
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@AutoConfigureRestDocs(outputDir = "build/asciidoc/snippets")
@SpringBootTest(classes = {Application.class, SwaggerConfig.class})
@AutoConfigureMockMvc
public class Swagger2MarkupTest {
    private static final Logger LOG = LoggerFactory.getLogger(Swagger2MarkupTest.class);
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void createSpringfoxSwaggerJson() throws Exception {
        //String designFirstSwaggerLocation = Swagger2MarkupTest.class.getResource("/swagger.yaml").getPath();

        String outputDir = System.getProperty("io.springfox.staticdocs.outputDir");
        MvcResult mvcResult = this.mockMvc.perform(get("/v2/api-docs")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        MockHttpServletResponse response = mvcResult.getResponse();
        String swaggerJson = response.getContentAsString();
        Files.createDirectories(Paths.get(outputDir));
        try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputDir, "swagger.json"), StandardCharsets.UTF_8)){
            writer.write(swaggerJson);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

注意上述的代码来源于swagger2markup官方说明一个demo,代码基于Spring Boot 1.4.0.release

Swagger2Markerup与AsciiDoc无插件调用(推荐)

有时候没有必须在maven的生命周期中远行单元测试生成文档,可以直接代码块生成文档

  1. maven依赖
<!-- swagger -->
 <dependency>
         <groupId>io.springfox</groupId>
         <artifactId>springfox-swagger2</artifactId>
         <version>2.5.0</version>
         <scope>test</scope>
</dependency>  
<dependency>
         <groupId>io.github.swagger2markup</groupId>
             <artifactId>swagger2markup-maven-plugin</artifactId>
              <version>1.0.1</version>
         <scope>test</scope>
         </dependency>
<dependency>
 <groupId>org.asciidoctor</groupId>
     <artifactId>asciidoctorj</artifactId>
     <version>1.5.4.1</version>
     <scope>test</scope>
</dependency>
  1. 编写文档生成脚本
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {BIMobileMasterApplication.class, SwaggerConfig.class})
public class Swagger2MarkupTest {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }

    @Test
    public void convertSwaggerToAsciiDoc() throws Exception {
        MvcResult mvcResult = this.mockMvc.perform(get("/v2/api-docs")
                .accept("application/json;charset=utf-8"))
                .andExpect(status().isOk())
                .andReturn();

        //文档输出目录 
        String outputDirectory = "docs/restful/generated";
        Path outputDirectoryPath = Paths.get(outputDirectory); 
        MockHttpServletResponse response = mvcResult.getResponse();
        String swaggerJson = response.getContentAsString();
        swaggerJson = swaggerJson.replace("{\"status\":200,\"message\":\"\",\"data\":", "");
        swaggerJson = swaggerJson.substring(0,swaggerJson.length()-1);
        Swagger2MarkupConverter.from(swaggerJson)
        .build()
        .toFolder(outputDirectoryPath);
        
        Asciidoctor asciidoctor = Asciidoctor.Factory.create();
        Attributes attributes = new Attributes();
        attributes.setCopyCss(true);
        attributes.setLinkCss(false);
        attributes.setSectNumLevels(3);
        attributes.setAnchors(true);
        attributes.setSectionNumbers(true);
        attributes.setHardbreaks(true);
        attributes.setTableOfContents(Placement.LEFT);
        attributes.setAttribute("generated", "generated");
        OptionsBuilder optionsBuilder = OptionsBuilder.options()
                .backend("html5")
                .docType("book")
                .eruby("")
                .inPlace(true)
                .safe(SafeMode.UNSAFE)
                .attributes(attributes);
        String asciiInputFile = "docs/restful/index.adoc";
        asciidoctor.convertFile(
                new File(asciiInputFile),
                optionsBuilder.get());
        
    }

Swagger2Markup插件的说明及使用

swagger2markup-import-files-ext

插件说明

    有时在写接口注释时,可能需要对接口附加一些特别说明,这种情况下Swagger2的Java注释感觉有点鸡肋。 这时可以使用swagger2markup-import-files-ext动态向adoc文档添加额外内容。

插件使用

  1. maven插件部署
    swagger2markup-import-files-ext是可以与swagger2markup-maven-plugin插件一起使用的
<plugin>
                <groupId>io.github.swagger2markup</groupId>
                <artifactId>swagger2markup-maven-plugin</artifactId>
                <version>${swagger2markup.version}</version>
                <dependencies> 
                    <dependency>
                        <groupId>io.github.swagger2markup</groupId>
                        <artifactId>swagger2markup-import-files-ext</artifactId>
                        <version>${swagger2markup.version}</version>
                    </dependency>
                    
                    <dependency>
                        <groupId>io.github.swagger2markup</groupId>
                        <artifactId>swagger2markup-spring-restdocs-ext</artifactId>
                        <version>${swagger2markup.version}</version>
                    </dependency>
      
                </dependencies>
                <configuration>
                    <swaggerInput>${swagger.input}</swaggerInput>
                    <outputDir>${generated.asciidoc.directory}</outputDir>
                    <config>
                        <swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
                        <swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>
                        <swagger2markup.extensions.dynamicOverview.contentPath>${project.basedir}/src/docs/asciidoc/extensions/overview</swagger2markup.extensions.dynamicOverview.contentPath>
                        <swagger2markup.extensions.dynamicDefinitions.contentPath>${project.basedir}/src/docs/asciidoc/extensions/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
                        <swagger2markup.extensions.dynamicPaths.contentPath>${project.basedir}/src/docs/asciidoc/extensions/paths</swagger2markup.extensions.dynamicPaths.contentPath>
                        <swagger2markup.extensions.dynamicSecurity.contentPath>${project.basedir}src/docs/asciidoc/extensions/security/</swagger2markup.extensions.dynamicSecurity.contentPath>
                        <swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
                        <swagger2markup.extensions.springRestDocs.defaultSnippets>false</swagger2markup.extensions.springRestDocs.defaultSnippets>
                    </config>
                </configuration>
                <executions>
                    <execution>
                        <phase>test</phase>
                        <goals>
                            <goal>convertSwagger2markup</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

swagger2markup.extensions.dynamicOverview.contentPath
swagger2markup.extensions.dynamicDefinitions.contentPath
swagger2markup.extensions.dynamicPaths.contentPath
swagger2markup.extensions.dynamicSecurity.contentPath
四个参数是必须要指定的,意思是swagger2markup-maven-plugin启动时,会在上述四个目录中分别查找adoc,并把adoc内容分别插入到原adoc中。

  1. 创建自定义的adc
    完成步骤1部署后,我们需要定义每一个adoc的内容及其插入的位置。Swagger2Markup文档中定义下面规则用于将自定义的adoc插入到接口文档指定位置中:

All extensions, relatively to each extension contentPath :
DOCUMENT_BEFORE : document-before-.<markup.ext>
DOCUMENT_BEGIN : document-begin-
.<markup.ext>
DOCUMENT_END : document-end-.<markup.ext>
DOCUMENT_AFTER : document-after-
.<markup.ext>
Paths extensions, relatively to each extension contentPath :
OPERATION_BEFORE : <operationId>/operation-before-.<markup.ext>
OPERATION_BEGIN : <operationId>/operation-begin-
.<markup.ext>
OPERATION_END : <operationId>/operation-end-.<markup.ext>
OPERATION_AFTER : <operationId>/operation-after-
.<markup.ext>
OPERATION_DESCRIPTION_BEFORE: <operationId>/operation-description-before-.<markup.ext>
OPERATION_DESCRIPTION_BEGIN: <operationId>/operation-description-begin-
.<markup.ext>
OPERATION_DESCRIPTION_END: <operationId>/operation-description-end-.<markup.ext>
OPERATION_DESCRIPTION_AFTER: <operationId>/operation-description-after-
.<markup.ext>
OPERATION_PARAMETERS_BEFORE: <operationId>/operation-parameters-before-.<markup.ext>
OPERATION_PARAMETERS_BEGIN: <operationId>/operation-parameters-begin-
.<markup.ext>
OPERATION_PARAMETERS_END: <operationId>/operation-parameters-end-.<markup.ext>
OPERATION_PARAMETERS_AFTER: <operationId>/operation-parameters-after-
.<markup.ext>
OPERATION_RESPONSES_BEFORE: <operationId>/operation-responses-before-.<markup.ext>
OPERATION_RESPONSES_BEGIN: <operationId>/operation-responses-begin-
.<markup.ext>
OPERATION_RESPONSES_END: <operationId>/operation-responses-end-.<markup.ext>
OPERATION_RESPONSES_AFTER: <operationId>/operation-responses-after-
.<markup.ext>
OPERATION_SECURITY_BEFORE: <operationId>/operation-security-before-.<markup.ext>
OPERATION_SECURITY_BEGIN: <operationId>/operation-security-begin
.<markup.ext>
OPERATION_SECURITY_END: <operationId>/operation-security-end-.<markup.ext>
OPERATION_SECURITY_AFTER: <operationId>/operation-security-after-
.<markup.ext>
Definitions extensions, relatively to each extension contentPath :
DEFINITION_BEFORE : <definitionName>/definition-before-.<markup.ext>
DEFINITION_BEGIN : <definitionName>/definition-begin-
.<markup.ext>
DEFINITION_END : <definitionName>/definition-end-.<markup.ext>
DEFINITION_AFTER : <definitionName>/definition-after-
.<markup.ext>
Security extensions, relatively to each extension contentPath :
SECURITY_SCHEME_BEFORE : <securitySchemeName>/security-scheme-before-.<markup.ext>
SECURITY_SCHEME_BEGIN : <securitySchemeName>/security-scheme-begin-
.<markup.ext>
SECURITY_SCHEME_END : <securitySchemeName>/security-scheme-end-.<markup.ext>
SECURITY_SCHEME_AFTER : <securitySchemeName>/security-scheme-after-
.<markup.ext>

注意:operationId相当于@ApiOperation注释中的nickname属性

假设现在要将一段接口说明插入到operationId为findPet的接口中,我们需要做如下步骤:

  • 创建一个新目录src/docs/asciidoc/extensions/paths/
  • 在上述目录下创建一个findPet的子目录
  • 创建一个operation-before-test.adoc文件,内容为:
这是一个插件入adoc

可以看到生成文章后在在findPet的接口前面增加了一段描述


截图.png

其它的插入位置同理

swagger2markup-spring-restdocs-ext

插件说明

此插件可以自动将Spring rest doc生成的adoc片段添加到Paths说明部分的最后。
Spring Rest doc生成的adoc版片段:

  • curl-request.adoc
  • http-request.adoc
  • http-response.adoc
  • httpie-request.adoc

插件默认扫描前3个adoc,当然也可以通过withExplicitSnippets自定义扫描的文件名。

插件使用

  1. maven插件部署
    这里我们使用与上一个述件不同的部署方式,直接到通过代码配置插件
        <dependency>
            <groupId>io.github.swagger2markup</groupId>
            <artifactId>swagger2markup-spring-restdocs-ext</artifactId>
            <version>1.0.0</version>
            <scope>compile</scope>
        </dependency>
  1. 配置
        Map<String, String> configMap = new HashMap<>(); 
        configMap.put("swagger2markup.extensions.springRestDocs.snippetBaseUri", "docs/restful/snippets");
        configMap.put("swagger2markup.extensions.springRestDocs.defaultSnippets", "true");
        configMap.put(Swagger2MarkupProperties.PATHS_GROUPED_BY, GroupBy.TAGS.name());
        Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder(configMap)

其中snippetBaseUri是Spring Rest Doc生成文档的位置;defaultSnippets指明是否使用默认的文件名扫描,如果设置为false,需要手工创建插件并通过withExplicitSnippets自行设置扫描的文件名。

其他参考资料

Spring Boot Test

http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容