Spring - Transaction - JDBC

原文地址:https://www.journaldev.com/2603/spring-transaction-management-jdbc-example

Spring事务管理是Spring框架中使用最广泛,最重要的功能之一。在任何企业应用程序中,事务管理都是一项琐碎的任务。我们已经学习了如何使用JDBC API进行事务管理。Spring为事务管理提供了广泛的支持,并帮助开发人员将更多的精力放在业务逻辑上,而不用担心任何系统故障时数据的完整性。

1. Spring事务管理

使用Spring事务管理的一些好处是:

  • 支持声明式事务管理。在此模型中,Spring在事务方法上使用AOP来提供数据完整性。这是首选方法,并且在大多数情况下都有效。
  • 支持大多数事务API,例如JDBC,Hibernate,JPA,JDO,JTA等。我们需要做的就是使用适当的事务管理器实现类。例如,如果我们使用Hibernate作为ORM工具,则用于JDBC事务管理的org.springframework.jdbc.datasource.DriverManagerDataSource和org.springframework.orm.hibernate3.HibernateTransactionManager。
  • 通过使用TransactionTemplate或PlatformTransactionManager实现,支持编程式事务管理。

声明式事务管理支持我们在事务管理器中需要的大多数功能,因此我们将在示例项目中使用这种方法。

2. Spring事务管理JDBC示例

我们将创建一个简单的Spring JDBC项目,在其中我们将在单个事务中更新多个表。仅当所有JDBC语句成功执行时,事务才应提交,否则应回滚以避免数据不一致。

如果您了解JDBC事务管理,您可能会说,通过将连接的auto-commit设置为false,并根据所有语句的结果,提交或回滚事务,我们可以很容易地做到这一点。显然,我们可以这样做,但是那样会产生大量用于事务管理的样板代码。同样的代码也会出现在我们寻找事务管理的所有地方,从而导致紧密耦合和不可维护的代码。

Spring声明式事务管理通过使用面向切面的编程来实现这些耦合,从而实现松散耦合并避免在我们的应用程序中使用样板代码。让我们用一个简单的例子来看看Spring是如何做到的。

在进入Spring项目之前,我们先进行一些数据库设置以供使用。

Spring事务管理 - 数据库建立

我们将创建两个供我们使用的表,并在单个事务中更新它们。

CREATE TABLE `Customer` (
  `id` int(11) unsigned NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `Address` (
  `id` int(11) unsigned NOT NULL,
  `address` varchar(20) DEFAULT NULL,
  `country` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们可以在此处从Address id列到Customer id列定义外键关系,但是为简单起见,我在这里没有定义任何约束。

我们的数据库设置已准备就绪,可以用于Spring事务管理项目,可以在Spring Tool Suite中创建一个简单的Spring Maven项目。我们的最终项目结构将如下图所示。

Spring-Transaction-Management-Example.png

让我们逐一研究各个部分,它们一起将提供一个简单的带有JDBC的spring事务管理示例。

Spring事务管理 - Maven依赖

由于我们使用的是JDBC API,因此我们必须在应用程序中包含spring-jdbc依赖项。我们还需要MySQL数据库驱动程序来连接到mysql数据库,因此我们还将包括mysql-connector-java依赖项。

spring-tx工件提供了事务管理依赖关系,通常它会被STS自动包含,但是如果没有,那么您也需要包含它。您可能会看到其他一些用于日志记录和单元测试的依赖项,但是我们将不再使用它们。我们最终的pom.xml文件看起来像下面的代码。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.springframework.samples</groupId>
    <artifactId>SpringJDBCTransactionManagement</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>

        <!-- Generic properties -->
        <java.version>1.7</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <!-- Spring -->
        <spring-framework.version>4.0.2.RELEASE</spring-framework.version>

        <!-- Logging -->
        <logback.version>1.0.13</logback.version>
        <slf4j.version>1.7.5</slf4j.version>

        <!-- Test -->
        <junit.version>4.11</junit.version>

    </properties>

    <dependencies>
        <!-- Spring and Transactions -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <!-- Spring JDBC and MySQL Driver -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.0.5</version>
        </dependency>

        <!-- Logging with SLF4J & LogBack -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Test Artifacts -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring-framework.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

到目前为止,我已经将Spring版本更新为最新版本。确保MySQL数据库驱动程序与您的mysql安装兼容。

Spring事务管理 - 模型类

我们将创建两个Java Bean,分别是Customer和Address,它们将映射到我们的表。

package com.journaldev.spring.jdbc.model;

public class Address {

    private int id;
    private String address;
    private String country;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getCountry() {
        return country;
    }
    public void setCountry(String country) {
        this.country = country;
    }
    
}
package com.journaldev.spring.jdbc.model;

public class Customer {

    private int id;
    private String name;
    private Address address;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address address) {
        this.address = address;
    }
    
}

请注意,Customer bean的一个变量是Address。当我们为客户实现DAO时,我们将获得客户和地址表的数据,并且我们将对这些表执行两个单独的插入查询,这就是为什么我们需要事务管理来避免数据不一致。

Spring事务管理 - DAO实现

让我们为Customer bean实现DAO,为了简单起见,我们将只有一个方法在Customer和address表中插入记录。

package com.journaldev.spring.jdbc.dao;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerDAO {

    public void create(Customer customer);
}
package com.journaldev.spring.jdbc.dao;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import com.journaldev.spring.jdbc.model.Customer;

public class CustomerDAOImpl implements CustomerDAO {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void create(Customer customer) {
        String queryCustomer = "insert into Customer (id, name) values (?,?)";
        String queryAddress = "insert into Address (id, address,country) values (?,?,?)";

        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        jdbcTemplate.update(queryCustomer, new Object[] { customer.getId(),
                customer.getName() });
        System.out.println("Inserted into Customer Table Successfully");
        jdbcTemplate.update(queryAddress, new Object[] { customer.getId(),
                customer.getAddress().getAddress(),
                customer.getAddress().getCountry() });
        System.out.println("Inserted into Address Table Successfully");
    }

}

注意,CustomerDAO实现并不负责事务管理。通过这种方式,我们可以实现关注点分离,因为有时候我们从第三方获得DAO实现,而我们无法控制这些类。

Spring声明式事务管理 - Service

让我们创建一个客户服务,它将使用CustomerDAO实现,并在将记录插入客户和地址表时提供事务管理。

package com.journaldev.spring.jdbc.service;

import com.journaldev.spring.jdbc.model.Customer;

public interface CustomerManager {

    public void createCustomer(Customer cust);
}
package com.journaldev.spring.jdbc.service;

import org.springframework.transaction.annotation.Transactional;

import com.journaldev.spring.jdbc.dao.CustomerDAO;
import com.journaldev.spring.jdbc.model.Customer;

public class CustomerManagerImpl implements CustomerManager {

    private CustomerDAO customerDAO;

    public void setCustomerDAO(CustomerDAO customerDAO) {
        this.customerDAO = customerDAO;
    }

    @Override
    @Transactional
    public void createCustomer(Customer cust) {
        customerDAO.create(cust);
    }

}

如果您注意到CustomerManager实现,它只是使用CustomerDAO实现来创建客户,但是通过使用@Transactional注释注释createCustomer()方法来提供声明性事务管理。这就是我们在代码中需要做的,以获得Spring事务管理的好处。

@Transactional注释可以应用于方法,也可以应用于整个类。如果希望所有方法都具有事务管理特性,则应该使用该注释注释类。在Java注释教程中阅读更多关于注释的内容。

剩下的惟一部分是连接spring bean以使spring事务管理示例工作。

Spring事务管理 - Bean配置

创建一个名为“Spring .xml”的Spring Bean配置文件。我们将在测试程序中使用它来连接spring bean并执行JDBC程序来测试事务管理。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <!-- Enable Annotation based Declarative Transaction Management -->
    <tx:annotation-driven proxy-target-class="true"
        transaction-manager="transactionManager" />

    <!-- Creating TransactionManager Bean, since JDBC we are creating of type 
        DataSourceTransactionManager -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
    <!-- MySQL DB DataSource -->
    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">

        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
        <property name="username" value="pankaj" />
        <property name="password" value="pankaj123" />
    </bean>

    <bean id="customerDAO" class="com.journaldev.spring.jdbc.dao.CustomerDAOImpl">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <bean id="customerManager" class="com.journaldev.spring.jdbc.service.CustomerManagerImpl">
        <property name="customerDAO" ref="customerDAO"></property>
    </bean>

</beans>

在spring bean配置文件中需要注意的点是:

  • x:annotation-driven元素用于告诉Spring上下文我们正在使用基于注解的事务管理配置。transaction-manager用于提供事务管理器bean名称。transaction-manager的默认值是transactionManager,但是为了避免混淆,我仍然使用它。proxy-target-class属性用于告诉Spring context使用基于类的代理,如果没有它,你会得到运行时异常,比如Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.journaldev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
  • 因为我们使用的是JDBC,所以我们创建了org.springframework.jdbc.datasource.DataSourceTransactionManager类型的transactionManager bean。这非常重要,我们应该根据事务API的使用使用合适的事务管理器实现类。
  • dataSource bean用于创建dataSource对象,我们需要提供数据库配置属性,如driverClassName、url、username和password。根据本地设置更改这些值。
  • 我们将数据源注入到customerDAO bean中。类似地,我们将customerDAO bean注入到customerManager bean定义中。

我们的设置已经就绪,让我们创建一个简单的测试类来测试事务管理实现。

package com.journaldev.spring.jdbc.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.journaldev.spring.jdbc.model.Address;
import com.journaldev.spring.jdbc.model.Customer;
import com.journaldev.spring.jdbc.service.CustomerManager;
import com.journaldev.spring.jdbc.service.CustomerManagerImpl;

public class TransactionManagerMain {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                "spring.xml");

        CustomerManager customerManager = ctx.getBean("customerManager",
                CustomerManagerImpl.class);

        Customer cust = createDummyCustomer();
        customerManager.createCustomer(cust);

        ctx.close();
    }

    private static Customer createDummyCustomer() {
        Customer customer = new Customer();
        customer.setId(2);
        customer.setName("Pankaj");
        Address address = new Address();
        address.setId(2);
        address.setCountry("India");
        // setting value more than 20 chars, so that SQLException occurs
        address.setAddress("Albany Dr, San Jose, CA 95129");
        customer.setAddress(address);
        return customer;
    }

}

请注意,我显式地设置地址列值太长,以便在将数据插入地址表时获得异常。

现在,当我们运行测试程序时,我们得到以下输出。

Mar 29, 2014 7:59:32 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3fa99295: startup date [Sat Mar 29 19:59:32 PDT 2014]; root of context hierarchy
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Inserted into Customer Table Successfully
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.support.SQLErrorCodesFactory <init>
INFO: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into Address (id, address,country) values (?,?,?)]; Data truncation: Data too long for column 'address' at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
    at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:100)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:907)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:968)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:978)
    at com.journaldev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
    at com.journaldev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
    at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$$84f71441.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
    at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
    at com.journaldev.spring.jdbc.main.TransactionManagerMain.main(TransactionManagerMain.java:20)
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:914)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:907)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642)
    ... 16 more

请注意,该日志消息表明已成功将数据插入到客户表中,但MySQL数据库驱动程序引发的异常清楚地表明,该值对于地址列而言太长。
现在,如果您要检查“Customer”表,那么您将在该表中找不到任何行,这意味着事务已完全回滚。

如果您想知道事务管理的魔力在哪里发生,请仔细查看日志,并注意Spring framework创建的AOP和代理类。Spring框架使用Around通知为CustomerManagerImpl生成代理类,并且只有在方法成功返回时才提交事务。如果有任何异常,它只是回滚整个事务。我建议你阅读Spring AOP的例子来学习更多面向方面的编程模型。

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

推荐阅读更多精彩内容