Mybatis插件扩展以及与Spring整合原理

1. Interceptor核心实现原理

熟悉Mybatis配置的都知道,在xml配置中我们可以配置如下节点:

  <plugins>

    <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">

      <property name="pluginProperty" value="100"/>

    </plugin>

  </plugins>

这个就是插件的配置,那么自然而然的这个节点就会在解析xml的时候进行解析,并将其添加到Configuration中。细心的读者应该还记得下面这段代码,在XMLConfigBuilderl类中:

  private void parseConfiguration(XNode root) {

    try {

      //issue #117 read properties first

    //解析<properties>节点

      propertiesElement(root.evalNode("properties"));

      //解析<settings>节点

      Properties settings = settingsAsProperties(root.evalNode("settings"));

      loadCustomVfs(settings);

      //解析<typeAliases>节点

      typeAliasesElement(root.evalNode("typeAliases"));

      //解析<plugins>节点

      pluginElement(root.evalNode("plugins"));

      //解析<objectFactory>节点

      objectFactoryElement(root.evalNode("objectFactory"));

      //解析<objectWrapperFactory>节点

      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

      //解析<reflectorFactory>节点

      reflectorFactoryElement(root.evalNode("reflectorFactory"));

      settingsElement(settings);//将settings填充到configuration

      // read it after objectFactory and objectWrapperFactory issue #631

      //解析<environments>节点

      environmentsElement(root.evalNode("environments"));

      //解析<databaseIdProvider>节点

      databaseIdProviderElement(root.evalNode("databaseIdProvider"));

      //解析<typeHandlers>节点

      typeHandlerElement(root.evalNode("typeHandlers"));

      //解析<mappers>节点

      mapperElement(root.evalNode("mappers"));

    } catch (Exception e) {

      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

    }

  }

其中pluginElement就是解析插件节点的:

  private void pluginElement(XNode parent) throws Exception {

    if (parent != null) {

      //遍历所有的插件配置

      for (XNode child : parent.getChildren()) {

    //获取插件的类名

        String interceptor = child.getStringAttribute("interceptor");

        //获取插件的配置

        Properties properties = child.getChildrenAsProperties();

        //实例化插件对象

        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();

        //设置插件属性

        interceptorInstance.setProperties(properties);

        //将插件添加到configuration对象,底层使用list保存所有的插件并记录顺序

        configuration.addInterceptor(interceptorInstance);

      }

    }

  }

从上面可以看到,就是根据配置实例化为Interceptor对象,并添加到InterceptorChain中,该类的对象被Configuration持有。Interceptor包含三个方法:

  //执行拦截逻辑的方法

  Object intercept(Invocation invocation) throws Throwable;

  //target是被拦截的对象,它的作用就是给被拦截的对象生成一个代理对象

  Object plugin(Object target);

  //读取在plugin中设置的参数

  void setProperties(Properties properties);

而InterceptorChain只是保存了所有的Interceptor,并提供方法给客户端调用,使得所有的Interceptor生成代理对象:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {

    for (Interceptor interceptor : interceptors) {

      target = interceptor.plugin(target);

    }

    return target;

  }

  public void addInterceptor(Interceptor interceptor) {

    interceptors.add(interceptor);

  }


  public List<Interceptor> getInterceptors() {

    return Collections.unmodifiableList(interceptors);

  }

}

可以看到pluginAll就是循环去调用了Interceptor的plugin方法,而该方法的实现一般是通过Plugin.wrap去生成代理对象:

  public static Object wrap(Object target, Interceptor interceptor) {

//解析Interceptor上@Intercepts注解得到的signature信息

    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

    Class<?> type = target.getClass();//获取目标对象的类型

    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);//获取目标对象实现的接口

    if (interfaces.length > 0) {

      //使用jdk的方式创建动态代理

      return Proxy.newProxyInstance(

          type.getClassLoader(),

          interfaces,

          new Plugin(target, interceptor, signatureMap));

    }

    return target;

  }

其中getSignatureMap就是将@Intercepts注解中的value值解析并缓存起来,该注解的值是@Signature类型的数组,而这个注解可以定义class类型、方法、参数,即拦截器的定位。而getAllInterfaces就是获取要被代理的接口,然后通过JDK动态代理创建代理对象,可以看到InvocationHandler就是Plugin类,所以直接看invoke方法,最终就是调用interceptor.intercept方法:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    try {

      //获取当前接口可以被拦截的方法

      Set<Method> methods = signatureMap.get(method.getDeclaringClass());

      if (methods != null && methods.contains(method)) {//如果当前方法需要被拦截,则调用interceptor.intercept方法进行拦截处理

        return interceptor.intercept(new Invocation(target, method, args));

      }

      //如果当前方法不需要被拦截,则调用对象自身的方法

      return method.invoke(target, args);

    } catch (Exception e) {

      throw ExceptionUtil.unwrapThrowable(e);

    }

  }

这里的插件实现思路是通用的,即这个interceptor我们可以用来扩展任何对象的任何方法,比如对Map的get进行拦截,可像下面这样实现:

  @Intercepts({

      @Signature(type = Map.class, method = "get", args = {Object.class})})

  public static class AlwaysMapPlugin implements Interceptor {

    @Override

    public Object intercept(Invocation invocation) throws Throwable {

      return "Always";

    }

    @Override

    public Object plugin(Object target) {

      return Plugin.wrap(target, this);

    }

    @Override

    public void setProperties(Properties properties) {

    }

  }

然后在使用Map时先用插件对其包装,这样拿到的就是Map的代理对象。

    Map map = new HashMap();

    map = (Map) new AlwaysMapPlugin().plugin(map);

2. Mybatis的拦截增强

因为我们可以对Mybatis扩展任意多个的插件,所以它使用InterceptorChain对象来保存所有的插件,这是责任链模式的实现。那么Mybatis到底会拦截哪些对象和哪些方法呢?回忆上篇文章我们就可以发现Mybatis只会对以下4个对象进行拦截:

Executor:

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

......省略

    //通过interceptorChain遍历所有的插件为executor增强,添加插件的功能

    executor = (Executor) interceptorChain.pluginAll(executor);

    return executor;

  }

StatementHandler

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

//创建RoutingStatementHandler对象,实际由statmentType来指定真实的StatementHandler来实现

StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);

    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

    return statementHandler;

  }

ParameterHandler

  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {

    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);

    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);

    return parameterHandler;

  }

ResultSetHandler

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,

      ResultHandler resultHandler, BoundSql boundSql) {

    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);

    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

    return resultSetHandler;

  }

而具体要拦截哪些对象和哪些方法则是由@Intercepts和@Signature指定的。

以上就是Mybatis扩展插件的实现机制,读者可据此自行分析下PageHelper的实现原理。另外需要注意,我们在进行自定义插件开发时,尤其要谨慎。因为直接关系到操作数据库,如果对插件的实现原理不透彻,很有可能引发难以估量的后果。

Mybatis与Spring整合原理

前面的示例都是单独使用Mybatis,可以看到需要创建SqlSessionFactory和SqlSession对象,然后通过SqlSession去创建Mapper接口的代理对象,所以在与Spring整合时,显而易见的,我们就需要考虑以下几点:

什么时候创建以及怎么创建SqlSessionFactory和SqlSession?

什么时候创建以及怎么创建代理对象?

如何将Mybatis的代理对象注入到IOC容器中?

Mybatis怎么保证和Spring在同一个事务中并且使用的是同一个连接?

那么如何实现以上几点呢?下文基于mybatis-spring-1.3.3版本分析。

1. SqlSessionFactory的创建

熟悉Spring源码的(如果不熟悉,可以阅读我之前的Spring系列源码)都知道Spring最重要的那些扩展点:

BeanDefinitionRegistryPostProcessor:Bean实例化前调用

BeanFactoryPostProcessor:Bean实例化前调用

InitializingBean:Bean实例化后调用

FactoryBean:实现该接口代替Spring管理一些特殊的Bean

其它还有很多,以上列举出来的就是Mybatis集成Spring所用到的扩展点。首先我们需要实例化SqlSessionFactory,而实例化该对象在Mybatis里实际上就是去解析一大堆配置并封装到该对象中,所以我们不能简单的使用<bean>标签来配置,为此Mybatis实现了一个类SqlSessionFactoryBean(这个类我们在以前使用整合包时都会配置),之前XML中的配置都以属性的方式放入到了该类中:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

<property name="typeAliasesPackage" value="com.enjoylearning.mybatis.entity" />

<property name="mapperLocations" value="classpath:sqlmapper/*.xml" />

</bean>

进入这个类,我们可以看到它实现了InitializingBean和FactoryBean接口,实现第一个接口的作用就是在该类实例化后立即去执行配置解析的阶段:

  public void afterPropertiesSet() throws Exception {

    notNull(dataSource, "Property 'dataSource' is required");

    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");

    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),

              "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();

  }

具体的解析就在buildSqlSessionFactory方法中,这个方法比较长,但不复杂,这里就不贴代码了。而实现第二接口的作用就在于Spring获取该类实例时实际上会通过getObject方法返回SqlSessionFactory的实例,通过这两个接口就完成了SqlSessionFactory的实例化。

2. 扫描Mapper并创建代理对象

在整合之后我们除了要配置SqlSessionFactoryBean外,还要配置一个类:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="com.enjoylearning.mybatis.mapper" />

</bean>

这个类的作用就是用来扫描Mapper接口的,并且这个类实现了BeanDefinitionRegistryPostProcessor和InitializingBean,这里实现第二个接口的作用主要是校验有没有配置待扫描包的路径:

  public void afterPropertiesSet() throws Exception {

    notNull(this.basePackage, "Property 'basePackage' is required");

  }

主要看到postProcessBeanDefinitionRegistry方法:

  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

    if (this.processPropertyPlaceHolders) {

      processPropertyPlaceHolders();

    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    scanner.setAddToConfig(this.addToConfig);

    scanner.setAnnotationClass(this.annotationClass);

    scanner.setMarkerInterface(this.markerInterface);

    scanner.setSqlSessionFactory(this.sqlSessionFactory);

    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);

    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);

    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);

    scanner.setResourceLoader(this.applicationContext);

    scanner.setBeanNameGenerator(this.nameGenerator);

    scanner.registerFilters();

    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));

  }

这里创建了一个扫描类,而这个扫描类是继承自Spring的ClassPathBeanDefinitionScanner,也就是会将扫描到的类封装为BeanDefinition注册到IOC容器中去:

public int scan(String... basePackages) {

int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

doScan(basePackages);

// Register annotation config processors, if necessary.

if (this.includeAnnotationConfig) {

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

}

return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);

}

  public Set<BeanDefinitionHolder> doScan(String... basePackages) {

    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {

      logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");

    } else {

      processBeanDefinitions(beanDefinitions);

    }

    return beanDefinitions;

  }

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {

    GenericBeanDefinition definition;

    for (BeanDefinitionHolder holder : beanDefinitions) {

      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {

        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()

          + "' and '" + definition.getBeanClassName() + "' mapperInterface");

      }

      // the mapper interface is the original class of the bean

      // but, the actual class of the bean is MapperFactoryBean

      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59

      definition.setBeanClass(this.mapperFactoryBean.getClass());

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;

      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {

        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));

        explicitFactoryUsed = true;

      } else if (this.sqlSessionFactory != null) {

        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);

        explicitFactoryUsed = true;

      }

      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {

        if (explicitFactoryUsed) {

          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");

        }

        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));

        explicitFactoryUsed = true;

      } else if (this.sqlSessionTemplate != null) {

        if (explicitFactoryUsed) {

          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");

        }

        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);

        explicitFactoryUsed = true;

      }

      if (!explicitFactoryUsed) {

        if (logger.isDebugEnabled()) {

          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");

        }

        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

      }

    }

  }

你可能会好奇,在哪里生成的代理对象?只是将Mapper接口注入到IOC有什么用呢?其实关键代码就在definition.setBeanClass(this.mapperFactoryBean.getClass()),这句代码的作用就是将每一个Mapper接口都转为MapperFactoryBean类型。

为什么要这么转呢?进入这个类你会发现它也是实现了FactoryBean接口的,所以自然而然的又是利用它来创建代理实现类对象:

  public T getObject() throws Exception {

    return getSqlSession().getMapper(this.mapperInterface);

  }

3. 如何整合Spring事务

Mybatis作为一个ORM框架,它是有自己的数据源和事务控制的,而Spring同样也会配置这两个,那么怎么将它们整合到一起呢?而不是在Service类调用Mapper接口时就切换了数据源和连接,那样肯定是不行的。

在使用Mybatis时,我们可以在xml中配置TransactionFactory事务工厂类,不过一般都会使用默认的JdbcTransactionFactory,而当与Spring整合后,默认的事务工厂类改为了SpringManagedTransactionFactory。回到SqlSessionFactoryBean读取配置的方法,在该方法中有下面这样一段代码:

    if (this.transactionFactory == null) {

      this.transactionFactory = new SpringManagedTransactionFactory();

    }

configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

上面默认创建了SpringManagedTransactionFactory,同时还将我们xml中ref属性引用的dataSource添加到了Configuration中,这个工厂会创建下面这个事务控制对象:

  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {

    return new SpringManagedTransaction(dataSource);

  }

而这个方法是在DefaultSqlSessionFactory获取SqlSession时会调用:

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

    Transaction tx = null;

    try {

      final Environment environment = configuration.getEnvironment();

      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

      final Executor executor = configuration.newExecutor(tx, execType);

      return new DefaultSqlSession(configuration, executor, autoCommit);

    } catch (Exception e) {

      closeTransaction(tx); // may have fetched a connection so lets call close()

      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);

    } finally {

      ErrorContext.instance().reset();

    }

  }

这就保证使用的是同一个数据源对象,但是怎么保证拿到的是同一个连接和事务呢?关键就在于SpringManagedTransaction获取连接是怎么实现的:

  public Connection getConnection() throws SQLException {

    if (this.connection == null) {

      openConnection();

    }

    return this.connection;

  }

  private void openConnection() throws SQLException {

    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");

    }

  }

这里委托给了DataSourceUtils获取连接:

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {

try {

return doGetConnection(dataSource);

}

catch (SQLException ex) {

throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);

}

}

public static Connection doGetConnection(DataSource dataSource) throws SQLException {

Assert.notNull(dataSource, "No DataSource specified");

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {

conHolder.requested();

if (!conHolder.hasConnection()) {

logger.debug("Fetching resumed JDBC Connection from DataSource");

conHolder.setConnection(dataSource.getConnection());

}

return conHolder.getConnection();

}

// Else we either got no holder or an empty thread-bound holder here.

logger.debug("Fetching JDBC Connection from DataSource");

Connection con = dataSource.getConnection();

if (TransactionSynchronizationManager.isSynchronizationActive()) {

logger.debug("Registering transaction synchronization for JDBC Connection");

// Use same Connection for further JDBC actions within the transaction.

// Thread-bound object will get removed by synchronization at transaction completion.

ConnectionHolder holderToUse = conHolder;

if (holderToUse == null) {

holderToUse = new ConnectionHolder(con);

}

else {

holderToUse.setConnection(con);

}

holderToUse.requested();

TransactionSynchronizationManager.registerSynchronization(

new ConnectionSynchronization(holderToUse, dataSource));

holderToUse.setSynchronizedWithTransaction(true);

if (holderToUse != conHolder) {

TransactionSynchronizationManager.bindResource(dataSource, holderToUse);

}

}

return con;

}

亚马逊测评 www.yisuping.com

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