2018-05-26

spring源码分析(六)



目录

五、spring源码分析
--5.7、Spring JDBC 设计原理及二次开发
----1. 异常处理
----2.config 模块
----3. core 模块
------3.1 NativeJdbcExtractor
------3.2 RowMapper
------3.3 元数据 metaData 模块
------3.4 使用 SqlParameterSource 提供参数值
------3.5 simple 实现
----4.DataSource
----5. object 模块
----6.JdbcTemplate
----7.NamedParameterJdbcTemplate



五、spring源码分析

5.7、Spring JDBC 设计原理及二次开发

使用Spring进行基本的JDBC访问数据库有多种选择。 Spring至少提供了三种不同的工作模式: JdbcTemplate, 一个在Spring2.5 中新提供的 SimpleJdbc 类能够更好的处理数据库元数据; 还有一种称之为 RDBMSObject 的风格的面向对象封装方式, 有点 类似于 JDO 的查询设计。我们在这里简要列举你采取某一种工作方式的主要理由. 不过请注意, 即使你选择了其中的一种工 作模式, 你依然可以在你的代码中混用其他任何一种模式以获取其带来的好处和优势。所有的工作模式都必须要求JDBC2.0 以上的数据库驱动的支持, 其中一些高级的功能可能需要 JDBC3.0 以上的数据库驱动支持。

JdbcTemplate- 这是经典的也是最常用的 Spring 对于 JDBC 访问的方案。这也是最低级别的封装, 其他的工作模式事实上在 底层使用了 JdbcTemplate 作为其底层的实现基础。JdbcTemplate 在 JDK1.4 以上的环境上工作得很好。

NamedParameterJdbcTemplate- 对 JdbcTemplate 做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的 JDBC 所使用的“?”作为参数的占位符。这种方式在你需要为某个 SQL 指定许多个参数时,显得更加直观而易用。该特性必须工作 在 JDK1.4 以上。

SimpleJdbcTemplate- 这个类结合了JdbcTemplate和NamedParameterJdbcTemplate的最常用的功能, 同时它也利用了一些Java 5 的特性所带来的优势,例如泛型、varargs 和 autoboxing 等,从而提供了更加简便的 API 访问方式。需要工作在 Java5 以上 的环境中。

SimpleJdbcInsert 和 SimpleJdbcCall- 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程, 你可以仅仅提供数据库表名或者存储过程的名称以及一个 Map 作为参数。其中 Map 的 key 需要与数据库表中的字段保持一 致。这两个类通常和 SimpleJdbcTemplate 配合使用。这两个类需要工作在 JDK5 以上,同时数据库需要提供足够的元数据信 息。

RDBMS 对象包括 MappingSqlQuery, SqlUpdateand StoredProcedure- 这种方式允许你在初始化你的数据访问层时创建可重 用并且线程安全的对象。该对象在你定义了你的查询语句,声明查询参数并编译相应的 Query 之后被模型化。一旦模型化完 成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在 JDK1.4 以上。

1. 异常处理

SQLExceptionTranslator 是一个接口,如果你需要在 SQLException 和 org.springframework.dao.DataAccessException 之间作转 换,那么必须实现该接口。 转换器类的实现可以采用一般通用的做法(比如使用 JDBC 的 SQLStatecode),如果为了使转换 更准确,也可以进行定制(比如使用 Oracle 的 errorcode)。

SQLErrorCodeSQLExceptionTranslator 是 SQLExceptionTranslator 的默认实现。 该实现使用指定数据库厂商的 errorcode,比 采用 SQLState 更精确。转换过程基于一个 JavaBean(类型为 SQLErrorCodes)中的 errorcode。 这个 JavaBean 由 SQLErrorCodesFactory 工厂类创建,其中的内容来自于 “sql-error-codes.xml”配置文件。该文件中的数据库厂商代码基于 DatabaseMetaData 信息中的 DatabaseProductName,从而配合当前数据库的使用。

SQLErrorCodeSQLExceptionTranslator 使用以下的匹配规则:

1、检查是否存在完成定制转换的子类实现。通常 SQLErrorCodeSQLExceptionTranslator 这个类可以作为一个具体类使用, 不需要进行定制,那么这个规则将不适用。

2、将SQLException的errorcode与错误代码集中的errorcode进行匹配。默认情况下错误代码集将从SQLErrorCodesFactory 取得。 错误代码集来自 classpath 下的 sql-error-codes.xml 文件,它们将与数据库 metadata 信息中的 databasename 进行映射。 使用 fallback 翻译器。SQLStateSQLExceptionTranslator 类是缺省的 fallback 翻译器。

2.config 模块

NamespaceHandler接口, DefaultBeanDefinitionDocumentReader使用该接口来处理在springxml配置文件中自定义的命名空间。


NamespaceHandler接口.PNG

在 jdbc 模块,我们使用 JdbcNamespaceHandler 来处理 jdbc 配置的命名空间,其代码如下:

public class JdbcNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
            registerBeanDefinitionParser("embedded-database", new EmbeddedDatabaseBeanDefinitionParser());
            registerBeanDefinitionParser("initialize-database", new InitializeDatabaseBeanDefinitionParser());
    }
    ...
}

其中,EmbeddedDatabaseBeanDefinitionParser 继承了 AbstractBeanDefinitionParser,解析<embedded-database>元素,并使用EmbeddedDatabaseFactoryBean 创建一个 BeanDefinition。顺便介绍一下用到的软件包 org.w3c.dom。

软件包 org.w3c.dom:为文档对象模型 (DOM) 提供接口,该模型是 Java API for XMLProcessing 的组件 API。该 DocumentObject Model Level 2 Core API 允许程序动态访问和更新文档的内容和结构。

Attr:Attr 接口表示 Element 对象中的属性。

CDATASection: CDATA 节用于转义文本块,该文本块包含的字符如果不转义则会被视为标记。

CharacterData: CharacterData 接口使用属性集合和用于访问 DOM 中字符数据的方法扩展节点。

Comment: 此接口继承自 CharacterData 表示注释的内容,即起始 '' 之间的所有字符。

Document: Document 接口表示整个 HTML 或 XML 文档。

DocumentFragment: DocumentFragment 是“轻量级”或“最小”Document 对象。

DocumentType: 每个 Document 都有 doctype 属性,该属性的值可以为 null,也可以为 DocumentType 对象。

DOMConfiguration: 该 DOMConfiguration 接口表示文档的配置,并维护一个可识别的参数表。

DOMError: DOMError 是一个描述错误的接口。

DOMErrorHandler: DOMErrorHandler 是在报告处理 XML 数据时发生的错误或在进行某些其他处理(如验证文档)时DOM 实现可以调用的回调接口。

DOMImplementation: DOMImplementation 接口为执行独立于文档对象模型的任何特定实例的操作提供了许多方法。

DOMImplementationList: DOMImplementationList 接口提供对 DOM 实现的有序集合的抽象,没有定义或约束如何实现此集合。

DOMImplementationSource: 此接口允许 DOM 实现程序根据请求的功能和版本提供一个或多个实现,如下所述。

DOMLocator: DOMLocator 是一个描述位置(如发生错误的位置)的接口。

DOMStringList: DOMStringList 接口提供对 DOMString 值的有序集合的抽象,没有定义或约束此集合是如何实现的。

Element: Element 接口表示 HTML 或 XML 文档中的一个元素。

Entity: 此接口表示在 XML 文档中解析和未解析的已知实体。

EntityReference: EntityReference 节点可以用来在树中表示实体引用。

NamedNodeMap: 实现 NamedNodeMap 接口的对象用于表示可以通过名称访问的节点的集合。

NameList NameList 接口提供对并行的名称和名称空间值对(可以为 null 值)的有序集合的抽象,无需定义或约束如何实现此集合。

Node: 该 Node 接口是整个文档对象模型的主要数据类型。

NodeList: NodeList 接口提供对节点的有序集合的抽象,没有定义或约束如何实现此集合。

Notation: 此接口表示在 DTD 中声明的表示法。

ProcessingInstruction: ProcessingInstruction 接口表示“处理指令”,该指令作为一种在文档的文本中保持特定于处理器的信息的方法在 XML 中使用。

Text: 该 Text 接口继承自 CharacterData,并且表示 Element 或 Attr 的文本内容(在 XML 中称为 字符数据)。

TypeInfo: TypeInfo 接口表示从 Element 或 Attr 节点引用的类型,用与文档相关的模式指定。

UserDataHandler: 当使用 Node.setUserData() 将一个对象与节点上的键相关联时,当克隆、导入或重命名该对象关联的节点时应用程序可以提供调用的处理程序。

3. core 模块

3.1 NativeJdbcExtractor
从线程池中的封装的对象中提取出本地的 jdbc 对象,其结构如下:

NativeJdbcExtractor.PNG

其实现原理如下(以 c3po 为例):

/**
* Retrieve the Connection via C3P0's {@code rawConnectionOperation} API,
* using the {@code getRawConnection} as callback to get access to the 
* raw Connection (which is otherwise not directly supported by C3P0). 
* @see #getRawConnection
*/
@Override
protected Connection doGetNativeConnection(Connection con) throws SQLException {
    if (con instanceof C3P0ProxyConnection) {
        C3P0ProxyConnection cpCon = (C3P0ProxyConnection) con;
        try {
            return (Connection) cpCon.rawConnectionOperation(
                this.getRawConnectionMethod, null, new Object[]
                {C3P0ProxyConnection.RAW_CONNECTION});
        }
        catch (SQLException ex) {
            throw ex;
        }
        catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
        }
    }
    return con;
}

上述代码通过调用 C3P0 的 rawConnectionOperationapi 来获取 Connection,使用 getRawConnection 的回调方法来获取原生的 Connection(C3P0 不直接支持原生的 Connection)。NativeJdbcExtractorAdapter 是 NativeJdbcExtractor 的一个简单实现,它的 getNativeConnection()方法检查 ConnectionProxy 链,并且代理 doGetNativeConnection 方法。Spring 的 TransactionAwareDataSourceProxy 和 LazyConnectionDataSourceProxy 使用 ConnectionProxy。目标 Connection 置于本地连接池 中,被子类实现的 doGetNativeConnection 的方法去掉封装获取到原生的 Connection。其实现如下:

@Override
public Connection getNativeConnection(Connection con) throws SQLException {
    if (con == null) {
        return null;
    }
    Connection targetCon = DataSourceUtils.getTargetConnection(con);
    Connection nativeCon = doGetNativeConnection(targetCon);
    if (nativeCon == targetCon) {
        // We haven't received a different Connection, so we'll assume that there's
        // some additional proxying going on. Let's check whether we get something
        // different back from the DatabaseMetaData.getConnection() call.
        DatabaseMetaData metaData = targetCon.getMetaData();
        // The following check is only really there for mock Connections
        // which might not carry a DatabaseMetaData instance. 
        if (metaData != null) {
            Connection metaCon = metaData.getConnection();
            if (metaCon != null && metaCon != targetCon) {
                // We've received a different Connection there:
                // Let's retry the native extraction process with it. 
                nativeCon = doGetNativeConnection(metaCon);
            }
        }
    }
    return nativeCon;
}

3.2 RowMapper

RowMapper.PNG

3.3 元数据 metaData 模块

本节中 spring 应用到工厂模式,结合代码可以更具体了解


CallMetaDataProvider.PNG
    /** List of supported database products for procedure calls */
    public static final List<String> supportedDatabaseProductsForProcedures = Arrays.asList(        "Apache Derby", 
         "DB2", 
         "MySQL", 
         "Microsoft SQL Server",
         "Oracle", 
         "PostgreSQL", 
         "Sybase"
    );
    /** List of supported database products for function calls */
    public static final List<String> supportedDatabaseProductsForFunctions = Arrays.asList(         "MySQL", 
         "Microsoft SQL Server", 
         "Oracle", 
         "PostgreSQL"
    );
    /**
    * Create a CallMetaDataProvider based on the database metadata
    * @param dataSource used to retrieve metadata
    * @param context the class that holds configuration and metadata
    * @return instance of the CallMetaDataProvider implementation to be used
    */
    static public CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final
    CallMetaDataContext context) {
        try {
            return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, new
            DatabaseMetaDataCallback() {
                @Override
                public Object processMetaData(DatabaseMetaData databaseMetaData) 
                throws SQLException, MetaDataAccessException {
                    String databaseProductName =
                        JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
                    boolean accessProcedureColumnMetaData =         
                        context.isAccessCallParameterMetaData();
                    if (context.isFunction()) {
                        if 
                        (!supportedDatabaseProductsForFunctions.contains(databaseProductName)) {
                            if (logger.isWarnEnabled()) {
                                logger.warn(databaseProductName + " is not one of the databases                                 fully supported for function calls " +
                                    "-- supported are: " +                                                                  supportedDatabaseProductsForFunctions);
                            }
                            if (accessProcedureColumnMetaData) {
                                logger.warn("Metadata processing disabled - you must specify all                                parametersexplicitly");
                                accessProcedureColumnMetaData = false;
                            }
                        }
                    }else {
                        if 
                       (!supportedDatabaseProductsForProcedures.contains(databaseProductName)) {
                            if (logger.isWarnEnabled()) {
                                logger.warn(databaseProductName + " is not one of the databases                                 fullysupported for procedure calls " +
                                   "-- supported are: " +           
                                   supportedDatabaseProductsForProcedures);
                            }
                            if (accessProcedureColumnMetaData) {
                                logger.warn("Metadata processing disabled - you must specify all                                parameters explicitly");
                                accessProcedureColumnMetaData = false;
                            }
                        }
                    }
                    CallMetaDataProvider provider;
                    if ("Oracle".equals(databaseProductName)) {
                        provider = new OracleCallMetaDataProvider(databaseMetaData);
                    }else if ("DB2".equals(databaseProductName)) {
                        provider = new Db2CallMetaDataProvider((databaseMetaData));
                    }else if ("Apache Derby".equals(databaseProductName)) {
                        provider = new DerbyCallMetaDataProvider((databaseMetaData));
                    }else if ("PostgreSQL".equals(databaseProductName)) {
                        provider = new PostgresCallMetaDataProvider((databaseMetaData));
                    }else if ("Sybase".equals(databaseProductName)) {
                        provider = new SybaseCallMetaDataProvider((databaseMetaData));
                    }else if ("Microsoft SQL Server".equals(databaseProductName)) {
                        provider = new SqlServerCallMetaDataProvider((databaseMetaData));
                    }else {
                        provider = new GenericCallMetaDataProvider(databaseMetaData);
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Using " + provider.getClass().getName());
                    }
                    provider.initializeWithMetaData(databaseMetaData);
                    if (accessProcedureColumnMetaData) {
                        provider.initializeWithProcedureColumnMetaData(
                        databaseMetaData, context.getCatalogName(), context.getSchemaName(), 
                            context.getProcedureName());
                    }
                    return provider;
                }
            });
        }catch (MetaDataAccessException ex) {
            throw new DataAccessResourceFailureException("Error retreiving database metadata", 
                       ex);
        }
    }

TableMetaDataProviderFactory 创建 TableMetaDataProvider 工厂类,其创建过程如下:

/**
* Create a TableMetaDataProvider based on the database metedata
* @param dataSource used to retrieve metedata
* @param context the class that holds configuration and metedata
* @param nativeJdbcExtractor the NativeJdbcExtractor to be used
* @return instance of the TableMetaDataProvider implementation to be used
*/
public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource, final TableMetaDataContext context, final NativeJdbcExtractor nativeJdbcExtractor) {
    try {
        return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource,
            new DatabaseMetaDataCallback() {
            @Override
            public Object processMetaData(DatabaseMetaData databaseMetaData) throws
            SQLException {
                String databaseProductName =
                  JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
                boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData();
                TableMetaDataProvider provider;
                if ("Oracle".equals(databaseProductName)) {
                    provider = new OracleTableMetaDataProvider(databaseMetaData, 
                        context.isOverrideIncludeSynonymsDefault());
                }
                else if ("HSQL Database Engine".equals(databaseProductName)) {
                    provider = new HsqlTableMetaDataProvider(databaseMetaData);
                }
                else if ("PostgreSQL".equals(databaseProductName)) {
                    provider = new PostgresTableMetaDataProvider(databaseMetaData);
                }
                else if ("Apache Derby".equals(databaseProductName)) {
                    provider = new DerbyTableMetaDataProvider(databaseMetaData);
                }
                else {
                    provider = new GenericTableMetaDataProvider(databaseMetaData);
                }
                if (nativeJdbcExtractor != null) {
                    provider.setNativeJdbcExtractor(nativeJdbcExtractor);
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Using " + provider.getClass().getSimpleName());
                }
                provider.initializeWithMetaData(databaseMetaData);
                if (accessTableColumnMetaData) {
                    provider.initializeWithTableColumnMetaData(databaseMetaData, 
                        context.getCatalogName(), context.getSchemaName(), 
                        context.getTableName());
                }
                return provider;
            }
        });
    }catch (MetaDataAccessException ex) {
        throw new DataAccessResourceFailureException("Error retrieving database metadata", ex);
    }
}

3.4 使用 SqlParameterSource 提供参数值

使用 Map 来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring 提供了一些其他的 SqlParameterSource实现类来指定参数值。 我们首先可以看看 BeanPropertySqlParameterSource 类,这是一个非常简便的指定参数的实现类,只要你有一个符合 JavaBean 规范的类就行了。它将使用其中的 getter 方法来获取参数值。

SqlParameter 封装了定义 sql 参数的对象。CallableStateMentCallback,PrePareStateMentCallback,StateMentCallback,ConnectionCallback 回调类分别对应 JdbcTemplate 中的不同处理方法。


SqlParameter.PNG

3.5 simple 实现

simple 实现.PNG

4.DataSource

spring 通过 DataSource 获取数据库的连接。Datasource 是 jdbc 规范的一部分,它通过 ConnectionFactory 获取。一个容 器和框架可以在应用代码层中隐藏连接池和事务管理。

当使用 spring 的 jdbc 层,你可以通过 JNDI 来获取 DataSource,也可以通过你自己配置的第三方连接池实现来获取。流 行的第三方实现由 apacheJakartaCommonsdbcp 和 c3p0.


DataSource.PNG

TransactionAwareDataSourceProxy 作为目标 DataSource 的一个代理, 在对目标 DataSource 包装的同时,还增加了 Spring 的事务管理能力, 在这一点上,这个类的功能非常像 J2EE 服务器所提供的事务化的 JNDI DataSource。

Note

该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource 接口实现作为参数。 这种情况下,这个类可以使现有代码参与 Spring 的事务管理。通常最好的做法是使用更高层的抽象 来对数据源进行管理,比如JdbcTemplate 和 DataSourceUtils 等等。

注意:DriverManagerDataSource 仅限于测试使用,因为它没有提供池的功能,这会导致在多个请求获取连接时性能很差。

5. object 模块
object 模块.PNG
6.JdbcTemplate

它是是 core 包的核心类。

它替我们完成了资源的创建以及释放工作,从而简化了我们对 JDBC 的使用。 它还可以帮助我们避免一些常见的错误,比 如忘记关闭数据库连接。 JdbcTemplate 将完成 JDBC 核心处理流程,比如 SQL 语句的创建、执行,而把 SQL 语句的生成以 及查询结果的提取工作留给我们的应用代码。 它可以完成 SQL 查询、更新以及调用存储过程,可以对 ResultSet 进行遍历并 加以提取。 它还可以捕获 JDBC 异常并将其转换成 org.springframework.dao 包中定义的,通用的,信息更丰富的异常。

使用 JdbcTemplate 进行编码只需要根据明确定义的一组契约来实现回调接口。 PreparedStatementCreator 回调接口通过给定 的 Connection 创建一个 PreparedStatement,包含 SQL 和任何相关的参数。 CallableStatementCreateor 实现同样的处理,只不 过它创建的是 CallableStatement。 RowCallbackHandler 接口则从数据集的每一行中提取值。

我们可以在 DAO 实现类中通过传递一个 DataSource 引用来完成 JdbcTemplate 的实例化,也可以在 Spring 的 IoC 容器中配置一个 JdbcTemplate 的 bean 并赋予 DAO 实现类作为一个实例。 需要注意的是 DataSource 在 Spring 的 IoC 容器中总是配制成 一个 bean,第一种情况下,DataSourcebean 将传递给 service,第二种情况下 DataSourcebean 传递给 JdbcTemplatebean。

7.NamedParameterJdbcTemplate

这个类为 JDBC 操作增加了命名参数的特性支持,而不是传统的使用('?')作为参数的占位符。 NamedParameterJdbcTemplate 类对 JdbcTemplate 类进行了封装, 在底层,JdbcTemplate 完成了多数的工作。

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