Mybatis源码分析——Mybatis事务如何被Spring管理

前言

不知道一些同学有没有这种疑问,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?那么Mybatis和Spring事务中用的Connection是同一个吗?我们常用配置如下

<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--使用注释事务 -->
<tx:annotation-driven  transaction-manager="transactionManager" />

看到没,sqlSessionFactory中配置了dataSource,transactionManager也配置了dataSource,我们来回忆一下SqlSessionFactoryBean这个类

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    // 配置类
    Configuration configuration;
    // 解析mybatis-Config.xml文件,
    // 将相关配置信息保存到configuration
    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
      
    //资源文件不为空  
    } else if (this.configLocation != null) {
      //根据configLocation创建xmlConfigBuilder,XMLConfigBuilder构造器中会创建Configuration对象
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      
      //将XMLConfigBuilder构造器中创建的Configuration对象直接赋值给configuration属性
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }

    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
        }
      }
    }

    if (!isEmpty(this.typeAliases)) {
      for (Class<?> typeAlias : this.typeAliases) {
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type alias: '" + typeAlias + "'");
        }
      }
    }

    if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }

    if (hasLength(this.typeHandlersPackage)) {
      String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeHandlersPackageArray) {
        configuration.getTypeHandlerRegistry().register(packageToScan);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
        }
      }
    }

    if (!isEmpty(this.typeHandlers)) {
      for (TypeHandler<?> typeHandler : this.typeHandlers) {
        configuration.getTypeHandlerRegistry().register(typeHandler);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type handler: '" + typeHandler + "'");
        }
      }
    }

    if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
      try {
        configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

    if (this.cache != null) {
      configuration.addCache(this.cache);
    }

    if (xmlConfigBuilder != null) {
      try {
        //解析mybatis-Config.xml文件,并将相关配置信息保存到configuration
        xmlConfigBuilder.parse();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    if (this.transactionFactory == null) {
      //事务默认采用SpringManagedTransaction,这一块非常重要
      this.transactionFactory = new SpringManagedTransactionFactory();
    }
    // 为sqlSessionFactory绑定事务管理器和数据源
    // 这样sqlSessionFactory在创建sqlSession的时候可以通过该事务管理器获取jdbc连接,从而执行SQL
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    
    // 解析mapper.xml
    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          // 解析mapper.xml文件,并注册到configuration对象的mapperRegistry
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

    // 将Configuration对象实例作为参数,
    // 调用sqlSessionFactoryBuilder创建sqlSessionFactory对象实例
    return this.sqlSessionFactoryBuilder.build(configuration);
  }
}

我们看this.transactionFactory = new SpringManagedTransactionFactory(),Mybatis集成Spring后,默认使用的transactionFactory是SpringManagedTransactionFactory,那我们就来看看其获取Transaction的方法

public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won't support transactions
        autoCommit = true;
      }      
      //从configuration中取出environment对象
      final Environment environment = configuration.getEnvironment();
      //从environment中取出TransactionFactory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //创建Transaction
      final Transaction tx = transactionFactory.newTransaction(connection);
      //创建包含事务操作的执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      //构建包含执行器的SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  
  private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    //这里返回SpringManagedTransactionFactory
    return environment.getTransactionFactory();
  }  
}


public interface TransactionFactory {

  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

}  

public class SpringManagedTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    //创建SpringManagedTransaction
    return new SpringManagedTransaction(dataSource);
  }
}

SpringManagedTransaction

也就是说mybatis的执行事务的事务管理器就切换成了SpringManagedTransaction,下面我们再去看看SpringManagedTransactionFactory类的源码:

public class SpringManagedTransaction implements Transaction {

  private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);

  private final DataSource dataSource;

  private Connection connection;

  private boolean isConnectionTransactional;

  private boolean autoCommit;

  public SpringManagedTransaction(DataSource dataSource) {
    notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
  }
  
  @Override
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }
  
  private void openConnection() throws SQLException {
    //通过DataSourceUtils获取connection,这里和JdbcTransaction不一样
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }
  
  @Override
  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
      }
      //通过connection提交,这里和JdbcTransaction一样
      this.connection.commit();
    }
  }
  
  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
      }
      //通过connection回滚,这里和JdbcTransaction一样
      this.connection.rollback();
    }
  }
  
  @Override
  public void close() throws SQLException {
    DataSourceUtils.releaseConnection(this.connection, this.dataSource);
  }
  
  @Override
  public Integer getTimeout() throws SQLException {
    ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    if (holder != null && holder.hasTimeout()) {
      return holder.getTimeToLiveInSeconds();
    } 
    return null;
  }

}
org.springframework.jdbc.datasource.DataSourceUtils#getConnection
public abstract class DataSourceUtils {

    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            return doGetConnection(dataSource);
        } catch (SQLException var2) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
        } catch (IllegalStateException var3) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
        }
    }
    
    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
        //TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
        //还记得我们前面Spring事务源码的分析吗?@Transaction会创建Connection,并放入ThreadLocal中
        //这里从ThreadLocal中获取ConnectionHolder
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
            logger.debug("Fetching JDBC Connection from DataSource");
            //如果没有使用@Transaction,那调用Mapper接口方法时,也是通过Spring的方法获取Connection
            Connection con = fetchConnection(dataSource);
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                try {
                    ConnectionHolder holderToUse = conHolder;
                    if (conHolder == null) {
                        holderToUse = new ConnectionHolder(con);
                    } else {
                        conHolder.setConnection(con);
                    }

                    holderToUse.requested();
                    TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                    holderToUse.setSynchronizedWithTransaction(true);
                    if (holderToUse != conHolder) {
                        //将获取到的ConnectionHolder放入ThreadLocal中,那么当前线程调用下一个接口,下一个接口使用了Spring事务,那Spring事务也可以直接取到Mybatis创建的Connection
                        //通过ThreadLocal保证了同一线程中Spring事务使用的Connection和Mapper代理类使用的Connection是同一个
                        TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                    }
                } catch (RuntimeException var4) {
                    releaseConnection(con, dataSource);
                    throw var4;
                }
            }

            return con;
        } else {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            //所以如果我们业务代码使用了@Transaction注解,在Spring中就已经通过dataSource创建了一个Connection并放入ThreadLocal中
            //那么当Mapper代理对象调用方法时,通过SqlSession的SpringManagedTransaction获取连接时,就直接获取到了当前线程中Spring事务创建的Connection并返回    
            return conHolder.getConnection();
        }
    }
}

想看怎么获取connHolder

org.springframework.transaction.support.TransactionSynchronizationManager#getResource
public abstract class TransactionSynchronizationManager {
    
    //保存数据库连接的ThreadLocal
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");

    @Nullable
    public static Object getResource(Object key) {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        //获取ConnectionHolder
        Object value = doGetResource(actualKey);
        if (value != null && logger.isTraceEnabled()) {
            logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
        }

        return value;
    }
    
    @Nullable
    private static Object doGetResource(Object actualKey) {
        /**
         * 从threadlocal <Map<Object, Object>>中取出来当前线程绑定的map
         * map里面存的是<dataSource,ConnectionHolder>
         */ 
        Map<Object, Object> map = (Map)resources.get();
        if (map == null) {
            return null;
        } else {
            //map中取出来对应dataSource的ConnectionHolder
            Object value = map.get(actualKey);
            if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
                map.remove(actualKey);
                if (map.isEmpty()) {
                    resources.remove();
                }

                value = null;
            }

            return value;
        }
    }
}   

我们看到直接从ThreadLocal中取出来的conn,而spring自己的事务也是操作的这个ThreadLocal中的conn来进行事务的开启和回滚,由此我们知道了在同一线程中Spring事务中的Connection和Mybaits中Mapper代理对象中操作数据库的Connection是同一个,当取出来的conn为空时候,调用org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取,然后把从数据源取出来的连接返回

public abstract class DataSourceUtils {

    private static Connection fetchConnection(DataSource dataSource) throws SQLException {
        //从数据源取出来conn
        Connection con = dataSource.getConnection();
        if (con == null) {
            throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
        } else {
            return con;
        }
    }
}

我们再来回顾一下上篇文章中的SqlSessionInterceptor

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  private final SqlSessionFactory sqlSessionFactory;

  private final ExecutorType executorType;

  private final SqlSession sqlSessionProxy;

  private final PersistenceExceptionTranslator exceptionTranslator;

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 获取一个sqlSession来执行proxy的method对应的SQL,
      // 每次调用都获取创建一个sqlSession线程局部变量,故不同线程相互不影响,在这里实现了SqlSessionTemplate的线程安全性  
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        //直接通过新创建的SqlSession反射调用method
        //这也就解释了为什么不需要目标类属性了,这里每次都会创建一个
        Object result = method.invoke(sqlSession, args);
        
        // 如果当前操作没有在一个Spring事务中,则手动commit一下
        // 如果当前业务没有使用@Transation,那么每次执行了Mapper接口的方法直接commit
        // 还记得我们前面讲的Mybatis的一级缓存吗,这里一级缓存不能起作用了,因为每执行一个Mapper的方法,sqlSession都提交了
        // sqlSession提交,会清空一级缓存
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
}

if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory))sqlSession.commit(true)行,如果我们没有使用@Transation,Mapper方法执行完后,sqlSession将会提交,也就是说通过org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取到的Connection将会commit,相当于Connection是自动提交的,也就是说如果不使用@Transation,Mybatis将没有事务可言。

Mybatis和Spring整合后SpringManagedTransaction和Spring的Transaction的关系:

  • 如果开启Spring事务,则先有Spring的Transaction,然后mybatis创建sqlSession时,会创建SpringManagedTransaction并加入sqlSession中,SpringManagedTransaction中的connection会从Spring的Transaction创建的Connection并放入ThreadLocal中获取。

  • 如果没有开启Spring事务或者第一个方法没有事务后面的方法有事务,则SpringManagedTransaction创建Connection并放入ThreadLocal中。

spring结合mybatis后mybaits一级缓存失效分为两种情况:

  • 如果没有开启事务,每一次sql都是用的新的SqlSession,这时mybatis的一级缓存是失效的。
  • 如果有事务,同一个事务中相同的查询使用的相同的SqlSessioon,此时一级缓存是生效的。

如果使用了@Transation呢?那在调用Mapper代理类的方法之前就已经通过Spring的事务生成了Connection并放入ThreadLocal,并且设置事务不自动提交,当前线程多个Mapper代理对象调用数据库操作方法时,将从ThreadLocal获取Spring创建的connection,在所有的Mapper方法调用完后,Spring事务提交或者回滚,到此mybatis的事务是怎么被spring管理的就显而易见了。

还有文章开头的问题,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?

因为Spring事务在没调用Mapper方法之前就需要开一个Connection,并设置事务不自动提交,那么transactionManager中自然要配置dataSource。那如果我们的Service没有用到Spring事务呢,难道就不需要获取数据库连接了吗?当然不是,此时通过SpringManagedTransaction调用org.springframework.jdbc.datasource.DataSourceUtils#getConnection#fetchConnection方法获取,并将dataSource作为参数传进去,实际上获取的Connection都是通过dataSource来获取的。

参考:
https://www.cnblogs.com/java-chen-hao/p/11839993.html

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

推荐阅读更多精彩内容