MyBatis詳解6.MyBatis技術(shù)內(nèi)幕

字節(jié)跳動飛書內(nèi)推!
北京他炊、杭州争剿、武漢、廣州痊末、深圳蚕苇、上海,六大城市等你來投凿叠。
感興趣的朋友可以私我咨詢&內(nèi)推涩笤,也可以通過鏈接直接投遞
海量HC,極速響應(yīng)辆它,快來和我成為同事吧。
今日頭條履恩、抖音锰茉、Tik Tok也可以內(nèi)推~

點(diǎn)擊進(jìn)入我的博客

MyBatis詳解1.概述
MyBatis詳解2.MyBatis使用入門
MyBatis詳解3.MyBatis配置詳解
MyBatis詳解4.映射器Mapper
MyBatis詳解5.動態(tài)SQL
MyBatis詳解6.MyBatis技術(shù)內(nèi)幕
MyBatis詳解7.插件
MyBatis詳解8.集成Spring

1 構(gòu)建SqlSessionFactory的過程

通過SqlSessionFactoryBuilder構(gòu)建SqlSessionFactory共有兩步:

  1. 通過XMLConfigBuilder解析MyBatis的配置文件,并讀取到Confinguration對象中切心。
  2. 使用Confinguration對象去創(chuàng)建SqlSessionFactory飒筑,由于SqlSessionFactory是一個接口,最終構(gòu)建出的其實是DefaultSqlSessionFactory的對象绽昏。
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 通過XMLConfigBuilder讀入配置
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      // ......
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    // 返回的是DefaultSqlSessionFactory
    return new DefaultSqlSessionFactory(config);
  }
Configuration的主要作用如下:
  • 讀入配置文件协屡,包括基礎(chǔ)配置的XML文件和映射器的XML文件。
  • 初始化基礎(chǔ)配置全谤,比如MyBatis的別名等肤晓,一些重要的類對象如,插件认然、映射器补憾、 ObjectFactory和typeHandler對象。
  • 提供單例卷员,為后續(xù)創(chuàng)建SessionFactory服務(wù)并提供配置的參數(shù)盈匾。
  • 執(zhí)行一些重要的對象方法,初始化配置信息毕骡。

2 映射器的內(nèi)部構(gòu)成

一個映射器是由3個部分組成
  1. MappedStatement削饵,它保存映射器的一個節(jié)點(diǎn)(select、insert未巫、delete窿撬、update)。包括許多我們配置的SQL橱赠、SQL的id尤仍、緩存信息、resultMap狭姨、parameterType宰啦、resultType、languageDriver等重要配置內(nèi)容饼拍。
  2. SqlSource赡模,是一個接口,它的主要作用是根據(jù)參數(shù)和其他的規(guī)則組裝SQL师抄,它是提供BoundSql對象的地方漓柑,它是MappedStatement的一個屬性。
  3. BoundSql,它是建立SQL和參數(shù)的地方辆布。它有3個常用的屬性:SQL瞬矩、parameterObject、parameterMappings锋玲。
映射器的內(nèi)部構(gòu)成
BoundSql的主要屬性
  • BoundSql會提供3個主要的屬性:parameterObject景用、parameterMappings和sql
  • parameterObject為參數(shù)本身,可以傳遞簡單對象惭蹂、POJO伞插、Map或者@Param注解的參數(shù)。
  • parameterMappings盾碗,它是一個List媚污,每一個元素都是ParameterMapping的對象。這個對象會描述我們的參數(shù)廷雅,包括屬性耗美、名稱、表達(dá)式榜轿、 javaType幽歼、 jdbcType、typeHandler等重要信息谬盐。通過它可以實現(xiàn)參數(shù)和SQL的結(jié)合甸私,以便PreparedStatement能夠通過它找到parameterObject對象的屬性并設(shè)置參數(shù),使得程序準(zhǔn)確運(yùn)行飞傀。
  • sql屬性就是我們書寫在映射器里面的一條SQL皇型,在插件的情況下,我們可以根據(jù)需要進(jìn)行改寫砸烦。
parameterObject的細(xì)節(jié)
  • 傳遞簡單對象(如基本數(shù)據(jù)類型)時弃鸦,例如當(dāng)我們傳遞int類型時,MyBatis會把參數(shù)變?yōu)镮nteger對象傳遞幢痘。如果我們傳遞的是POJO或者M(jìn)ap唬格,那么這個parameterObject就是你傳入的POJO或者M(jìn)ap不變。
  • 當(dāng)然我們也可以傳遞多個參數(shù)颜说,如果沒有@Param注解购岗,那么MyBatis就會把parameterObject變?yōu)橐粋€Map<String, Object>對象,其鍵值的關(guān)系是按順序來規(guī)劃的门粪,類似于這樣的形式:{"1":p1; "2":p2, "3":p3,...,"param1":pl, "param2":p2, "param3":p3}喊积,可以使用#{param1}或者#{1}去引用第1個參數(shù)。
  • 如果我們使用@Param注解,那么MyBatis就會把parameterObject會變?yōu)橐粋€Map<String, Object>對象玄妈,鍵為@Param注解的鍵乾吻。

3 SqlSession的運(yùn)行過程

3.1 Mapper的動態(tài)代理

我們自定義的Mapper接口想要發(fā)揮功能髓梅,必須有具體的實現(xiàn)類,在MyBatis中是通過為Mapper每個接口提供一個動態(tài)代理類來實現(xiàn)的绎签。整個過程主要有三個類:MapperProxyFactory枯饿、MapperProxy、MapperMethod诡必。

  • MapperProxyFactory就是MapperProxy的工廠類鸭你,主要方法就是包裝了Java動態(tài)代理的Proxy.newProxyInstance()方法。
  • MapperProxy就是一個動態(tài)代理類擒权,它實現(xiàn)了InvocationHandler接口。對于代理對象的調(diào)用都會被代理到InvocationHandler#invoke方法上阁谆。
  • MapperMethod包含了具體增刪改查方法的實現(xiàn)邏輯碳抄。
public class MapperProxyFactory<T> {
   
  // 這里可以看到是通過Java的動態(tài)代理來實現(xiàn)的,具體代理的方法被放到來MapperProxy中
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}
// 實現(xiàn)了InvocationHandler接口
public class MapperProxy<T> implements InvocationHandler, Serializable {
  
  // 對代理類的所有方法的執(zhí)行场绿,都會進(jìn)入到invoke方法中
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 此處判斷是否是Object類的方法剖效,如toString()、clone()焰盗,如果是則直接執(zhí)行不進(jìn)行代理
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    // 如果不是Object類的方法璧尸,則初始化一個MapperMethod并放入緩存中
    // 或者從緩存中取出之前的MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 調(diào)用MapperMethod執(zhí)行對應(yīng)
    return mapperMethod.execute(sqlSession, args);
  }
}
public class MapperMethod {
  // MapperMethod采用命令模式運(yùn)行,根據(jù)上下文跳轉(zhuǎn)熬拒,它可能跳轉(zhuǎn)到許多方法中
  // 實際上它最后就是通過SqlSession對象去運(yùn)行對象的SQL爷光。
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: { //...
      }
      case UPDATE: { //...
      }
      case DELETE: { //...
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        //...
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }

    return result;
  }  
}

3.2 SqlSession中的對象

Mapper執(zhí)行的過程是通過Executor、StatementHandler澎粟、ParameterHandler和ResultHandler來完成數(shù)據(jù)庫操作和結(jié)果返回的:

  • Executor代表執(zhí)行器蛀序,由它來調(diào)度StatementHandler、ParameterHandler活烙、ResultHandler等來執(zhí)行對應(yīng)的SQL徐裸。
  • StatementHandler的作用是使用數(shù)據(jù)庫的Statement(PreparedStatement)執(zhí)行操作,起到承上啟下的作用啸盏。
  • ParameterHandler用于SQL對參數(shù)的處理重贺。
  • ResultHandler是進(jìn)行最后數(shù)據(jù)集(ResultSet)的封裝返回處理的。

3.3 執(zhí)行器Executor

執(zhí)行器是一個真正執(zhí)行Java和數(shù)據(jù)庫交互的類回懦,一共有三種執(zhí)行器气笙,我們可以在MyBatis的配置文件中設(shè)置defaultExecutorType屬性進(jìn)行選擇。

  • SIMPLE(org.apache.ibatis.executor.SimpleExecutor)粉怕,簡易執(zhí)行器健民,默認(rèn)執(zhí)行器。
  • REUSE(org.apache.ibatis.executor.ReuseExecutor)贫贝,是一種執(zhí)行器重用預(yù)處理語句秉犹。
  • BATCH(org.apache.ibatis.executor.BatchExecutor)蛉谜,執(zhí)行器重用語句和批量更新,它是針對批量專用的執(zhí)行器崇堵。
// Configure類中創(chuàng)建Executor的具體邏輯
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 在executor完成創(chuàng)建之后型诚,會通過interceptorChain來添加插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

創(chuàng)建Executor的具體邏輯在Configure類中,可以看到鸳劳,在Executor創(chuàng)建完成之后狰贯,會通過interceptorChain來添加插件,通過代理到方式赏廓,在調(diào)度真實的Executor方法之前執(zhí)行插件代碼來完成功能涵紊。

Executor的具體執(zhí)行邏輯

我們通過SimpleExecutor來看一下Executor的具體執(zhí)行邏輯:

  1. 根據(jù)Configuration來構(gòu)建StatementHandler
  2. 然后使用prepareStatement方法,對SQL編譯并對參數(shù)進(jìn)行初始化
  3. 在prepareStatement方法中幔摸,調(diào)用了StatementHandler的prepared進(jìn)行了預(yù)編譯和基礎(chǔ)設(shè)置摸柄,然后通過StatementHandler的parameterize來設(shè)置參數(shù)并執(zhí)行。
  4. 包裝好的Statement通過StatementHandler來執(zhí)行既忆,并把結(jié)果傳遞給resultHandler驱负。
public class SimpleExecutor extends BaseExecutor {

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // (1)根據(jù) Configuration來構(gòu)建Statementhandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
}

3.4 數(shù)據(jù)庫會話器StatementHandler

StatementHandler就是專門處理數(shù)據(jù)庫會話的,創(chuàng)建StatementHandler的過程在Configuration中患雇。

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
      return delegate.prepare(connection, transactionTimeout);
    }

    @Override
    public void parameterize(Statement statement) throws SQLException {
      delegate.parameterize(statement);
    }
  }

很顯然創(chuàng)建的真實對象是一個RoutingStatementHandler對象跃脊,它實現(xiàn)了接口StatementHandler。從RoutingStatementHandler的構(gòu)造方法來看苛吱,它其實是使用來委派模式來把具體的StatementHandler類型隱藏起來酪术,通過RoutingStatementHandler來統(tǒng)一管理。一共用三種具體的StatementHandler類型:SimpleHandler翠储、PreparedStatementHandler拼缝、CallableStatementHandler。

通過StatementHandler看執(zhí)行細(xì)節(jié)

在Executor的具體執(zhí)行邏輯中彰亥,我們主要關(guān)注StatementHandler的prepared咧七、parameterize兩個方法。

public abstract class BaseStatementHandler implements StatementHandler {
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      // instantiateStatement對SQL進(jìn)行了預(yù)編譯
      statement = instantiateStatement(connection);
      // 設(shè)置超時時間
      setStatementTimeout(statement, transactionTimeout);
      // 設(shè)置獲取最大的行數(shù)
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }
}
public class PreparedStatementHandler extends BaseStatementHandler {
  // 調(diào)用parameterize去設(shè)置參數(shù)任斋,可以發(fā)現(xiàn)是通過parameterHandler來具體執(zhí)行的
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }
}
public class PreparedStatementHandler extends BaseStatementHandler {
  // 具體的查詢就是通過PreparedStatement#execute來執(zhí)行的
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }
}

3.5 參數(shù)處理器ParameterHandler

MyBatis是通過ParameterHandler對預(yù)編譯的語句進(jìn)行參數(shù)設(shè)置的继阻。

public interface ParameterHandler {
  // 返回參數(shù)對象
  Object getParameterObject();
  // 設(shè)置預(yù)編譯的SQL語句的參數(shù)
  void setParameters(PreparedStatement ps) throws SQLException;
}
public class DefaultParameterHandler implements ParameterHandler {

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }
}

MyBatis為ParameterHandler提供了一個實現(xiàn)類DefaultParameterHandler,具體執(zhí)行過程還是從 parameterObject對象中取參數(shù)然后使用typeHandler進(jìn)行參數(shù)處理废酷,而typeHandler也是在My Batis初始化的時候瘟檩,注冊在Configuration里面的,我們需要的時候可以直接拿來用澈蟆。

3.6 ResultSetHandler

public interface ResultSetHandler {
  // 包裝結(jié)果集的
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;
  // 處理存儲過程輸出參數(shù)的
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
  void handleOutputParameters(CallableStatement cs) throws SQLException;
}

MyBatis為我們提供了一個DefaultResultSetHandler類墨辛,在默認(rèn)的情況下都是通過這個類進(jìn)行處理的。這個類JAVASSIST或者CGLIB作為延遲加載趴俘,然后通過typeHandler和ObjectFactory進(jìn)行組裝結(jié)果再返回睹簇。

3.7 總結(jié)

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奏赘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子太惠,更是在濱河造成了極大的恐慌磨淌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凿渊,死亡現(xiàn)場離奇詭異梁只,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)埃脏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門搪锣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人彩掐,你說我怎么就攤上這事淤翔。” “怎么了佩谷?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長监嗜。 經(jīng)常有香客問我谐檀,道長,這世上最難降的妖魔是什么裁奇? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任桐猬,我火速辦了婚禮,結(jié)果婚禮上刽肠,老公的妹妹穿的比我還像新娘溃肪。我一直安慰自己,他們只是感情好音五,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布惫撰。 她就那樣靜靜地躺著,像睡著了一般躺涝。 火紅的嫁衣襯著肌膚如雪厨钻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天坚嗜,我揣著相機(jī)與錄音夯膀,去河邊找鬼。 笑死苍蔬,一個胖子當(dāng)著我的面吹牛诱建,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碟绑,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼俺猿,長吁一口氣:“原來是場噩夢啊……” “哼茎匠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辜荠,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤汽抚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伯病,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體造烁,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年午笛,在試婚紗的時候發(fā)現(xiàn)自己被綠了惭蟋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡药磺,死狀恐怖告组,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情癌佩,我是刑警寧澤木缝,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站围辙,受9級特大地震影響我碟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姚建,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一矫俺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掸冤,春花似錦厘托、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饺藤,卻和暖如春伊群,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背策精。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工舰始, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咽袜。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓丸卷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親询刹。 傳聞我的和親對象是個殘疾皇子谜嫉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容