Mybatis查詢語句sql拼裝與Ognl源碼解析

@[toc]

## Mybatis查詢語句sql拼裝源碼解析

### 帶著問題學習源碼(從加載mapper到sql拼裝)

#### 問題現(xiàn)象

**后端用Integer接收0傳入param.pushStatus,為什么param.pushStatus !=''判斷為false**

后端使用Integer接收

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kgpQR0NO-1650516052826)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/d896a8c5d9e669c7b1c36a1e4a1610d7)\]](https://img-blog.csdnimg.cn/b4a279b8e4ce4c61a62bde1c1ebc7ff8.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_12,color_FFFFFF,t_70,g_se,x_16)![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yhqAzZE8-1650516052827)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/9602944814709df16db8d90d17c9a5d3)\]](https://img-blog.csdnimg.cn/8c8537f9ef384c7f898cb99774756a25.png)

#### 原因

mapper接口傳入的參數(shù)類型為Integer值為0時棘钞,mybaits 在進行 **param.pushStatus !=''**的時候會默認<font color='red'>""和0 都轉(zhuǎn)換成double進行比較 都是0.0 </font> 乔夯,結果不是重點端蛆,重點在于下面過程砚哗。

#### 源碼解析(Mybatis-plus)

Mybatis-plus很多類重寫了Mybatis,此處以Mybatis-plus源碼出發(fā)

##### 1脯倒、加載SqlSessionFactory

項目啟動會通過springboot的自動裝配原理加載MybatisPlusAutoConfiguration從而加載SqlSessionFactory(加載mapper到MybatisSqlSessionFactoryBean)

```java

public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

? ? ? ? MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();

? ? ? ? factory.setDataSource(dataSource);

? ? ? ? ...

? ? ? ? if (StringUtils.hasText(this.properties.getConfigLocation())) {

//下面的ConfigLocation為:classpath:mybatis/mybatis-config.xml

factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));

? ? ? ? }

? ? ? ? ...

//這里是裝載插件(之后會加入責任鏈中)適用于慢sql查詢攔截器等

? ? ? ? if (!ObjectUtils.isEmpty(this.interceptors)) {

? ? ? ? ? ? factory.setPlugins(this.interceptors);

? ? ? ? }

...

//...注入很多屬性比如:自定義枚舉包办悟、注入主鍵生成器躬厌、注入sql注入器、注入ID生成器等

factory.getObject();

}

//factory.getObject()--會進行后置屬性設置(MybatisSqlSessionFactoryBean)

```

```java

//MybatisSqlSessionFactoryBean

//這里關注mapper的設置

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

? ? ? ? MybatisXMLConfigBuilder xmlConfigBuilder = null;

? ? ? ? if (this.configuration != null) {

? ? ? ? ? ? ...

? ? ? ? } else if (this.configLocation != null) {

? ? ? ? ? ? // this.configLocation 里面包括mybatis/mybatis-config.xml

//如果在配置文件有<mapper>標簽解析

? ? ? ? ? ? xmlConfigBuilder = new MybatisXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);

? ? ? ? ? ? targetConfiguration = xmlConfigBuilder.getConfiguration();

? ? ? ? } else {

? ? ? ? ? ? ...

? ? ? ? }

? ? ......

? ? ? ? if (xmlConfigBuilder != null) {

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? //第一種--解析配置文件mybatis-config.xml信息

? ? ? ? ? ? ? ? xmlConfigBuilder.parse();

? ? ? ? }

? ? ......

//mapperLocations是提前掃描自定義的classpath:mapper/*.xml文件

? ? ? ? //例如:file [E:\code\trade\trade\target\classes\mapper\SellReconciliationMapper.xml]

if (this.mapperLocations != null) {

? ? ? ? ? ? if (this.mapperLocations.length == 0) {

? ? ? ? ? ? ? ...

? ? ? ? ? ? } else {

//循環(huán)遍歷所有的xxxx.xml

? ? ? ? ? ? ? ? for (Resource mapperLocation : this.mapperLocations) {

? ? ? ? ? ? ? ? ? ? if (mapperLocation == null) {

? ? ? ? ? ? ? ? ? ? ? ? continue;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? try {

? ? ? ? ? ? ? ? ? ? ? ? XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),

? ? ? ? ? ? ? ? ? ? ? ? ? ? targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());

? ? ? ? ? ? ? ? ? ? ? ? //這里解析mapper

? ? ? ? ? ? ? ? ? ? ? ? xmlMapperBuilder.parse();

? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

}

```

###### xmlConfigBuilder.parse();

```java

public Configuration parse() {

? ? ? ? if (parsed) {

? ? ? ? ? ? throw new BuilderException("Each XMLConfigBuilder can only be used once.");

? ? ? ? }

? ? ? ? parsed = true;

? ? ? ? parseConfiguration(parser.evalNode("/configuration"));

? ? ? ? return configuration;

? ? }

? ? private void parseConfiguration(XNode root) {

? ? ? ? try {

//將mybatis-config.xml通過XPath解析成XNode再進行解析

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

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

? ? ? ? ? ? loadCustomVfs(settings);

? ? ? ? ? ? loadCustomLogImpl(settings);

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

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

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

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

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

? ? ? ? ? ? settingsElement(settings);

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

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

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

//這里可以配置mapper

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

? ? ? ? } catch (Exception e) {

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

? ? ? ? }

? ? }

```

###### xmlMapperBuilder.parse();

```java

public void parse() {

? ? if (!configuration.isResourceLoaded(resource)) {

//前面已經(jīng)獲取了SellReconciliationMapper.xml狠裹,通過命名空間解析mapper

? ? ? configurationElement(parser.evalNode("/mapper"));

? ? ? configuration.addLoadedResource(resource);

? ? ? //再通過命名空間綁定mapper

? ? ? bindMapperForNamespace();

? ? }

//解析ResultMap等屬性

? ? parsePendingResultMaps();

? ? parsePendingCacheRefs();

? ? parsePendingStatements();

? }

private void bindMapperForNamespace() {

? ? //com.jdh.trade.mapper.SellReconciliationMapper

? ? String namespace = builderAssistant.getCurrentNamespace();

? ? if (namespace != null) {

? ? ? Class<?> boundType = null;

? ? ? try {

? ? ? ? //獲取命名空間獲取mapper接口

? ? ? ? boundType = Resources.classForName(namespace);

? ? ? }

? ? ? if (boundType != null && !configuration.hasMapper(boundType)) {

? ? ? ? //Spring可能不知道真正的資源名虽界,所以我們設置了一個標志

//防止從映射器接口再次加載這個資源

? ? ? ? configuration.addLoadedResource("namespace:" + namespace);

? ? ? ? //關鍵這里將mapper增加到map中

? ? ? ? configuration.addMapper(boundType);

? ? ? }

? ? }

? }

//key mapper接口 value mapper代理工廠

//private final Map<Class<?>, MybatisMapperProxyFactory<?>> knownMappers = new HashMap<>();

@Override

? ? public <T> void addMapper(Class<T> type) {

? ? ? ? if (type.isInterface()) {

? ? ? ? ? ? ...

? ? ? ? ? ? boolean loadCompleted = false;

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? //!!!!保存到map!!!!

? ? ? ? ? ? ? ? knownMappers.put(type, new MybatisMapperProxyFactory<>(type));

? ? ? ? ? ? ? ? //在運行解析器之前添加類型是很重要的否則,將自動嘗試綁定映射器解析器涛菠。如果類型已經(jīng)知道莉御,則不會嘗試.

? ? ? ? ? ? ? ? MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);

? ? ? ? ? ? ? ? parser.parse();

? ? ? ? ? ? ? ? loadCompleted = true;

? ? ? ? ? ? }...

? ? }

```

##### 2、mapper接口生成代理對象

@service 加載bean之后會通過

doGetObjectFromFactoryBean方法中執(zhí)行factory.getObject()獲取到bean實例MybatisMapperProxy俗冻。

```java

//MapperFactoryBean

//對于mybatis相關的mapper

? @Override

? public T getObject() throws Exception {

? ? ? //this.mapperInterface 相當于接口com.jdh.trade.mapper.xxxMapper

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

? }

//SqlSessionTemplate

? @Override

? public <T> T getMapper(Class<T> type) {

? ? return getConfiguration().getMapper(type, this);

? }

//MybatisMapperRegistry

@Override

? ? public <T> T getMapper(Class<T> type, SqlSession sqlSession) {

? ? ? ? //knownMappers 這個map是上面保存的

? ? ? ? final MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);

...

? ? ? ? try {

? ? ? ? ? ? //生成代理對象

? ? ? ? ? ? return mapperProxyFactory.newInstance(sqlSession);

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? throw new BindingException("Error getting mapper instance. Cause: " + e, e);

? ? ? ? }

? ? }

//MybatisMapperProxyFactory

//mapperProxyFactory.newInstance(sqlSession);

protected T newInstance(MybatisMapperProxy<T> mapperProxy) {

? ? //Proxy 生成動態(tài)代理實例

? ? ? ? return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);

? ? }

? ? public T newInstance(SqlSession sqlSession) {

? ? ? ? final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);

? ? ? ? return newInstance(mapperProxy);

? ? }

```

##### 3礁叔、調(diào)用查詢方法

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-t4H8ej0Q-1650516052829)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/3f07629179b2a67f00a75a1c969ca3ad)\]](https://img-blog.csdnimg.cn/46df55196bb443e1915cd7f1609898fd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

執(zhí)行mapper的查詢等語句就會進入代理對象的invoke方法

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-z0AArxR6-1650516052829)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/4639d1b1bcbc912c3f7c5a2353ad3da6)\]](https://img-blog.csdnimg.cn/9d4560591b2e4285836cff47aa7a9e2b.png)

InvocationHandler接口是proxy代理實例的調(diào)用處理程序?qū)崿F(xiàn)的一個接口,每一個proxy代理實例都有一個關聯(lián)的調(diào)用處理程序迄薄;在代理實例調(diào)用方法時琅关,方法調(diào)用被編碼分派到調(diào)用處理程序的invoke方法。

MybatisMapperProxy

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-43rqFH0W-1650516052830)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/50c83add18d67470cdd256f3a1fe4546)\]](https://img-blog.csdnimg.cn/ba4cb1f79fab492b88d5ce22574bc813.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

從緩存獲取MybatisMapperMethod-屬性如下

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YaYw3jeo-1650516052830)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/d4fe14619bcf0e7cfef5d3c509f65069)\]](https://img-blog.csdnimg.cn/2a3241c6c4644e1daf7f0cd53c300c61.png)

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fDPYeN03-1650516052831)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/3e0d6569d357998f88a74fbe6521245a)\]](https://img-blog.csdnimg.cn/00ac9576c2a24ada9bdff1302839a088.png)

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yE1T20pP-1650516052831)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/13a45f906187eca9674e49b990f29602)\]](https://img-blog.csdnimg.cn/194138e438d84d5d85e940b99ad33b60.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

最終執(zhí)行sql走的是MybatisMapperMethod.execute(sqlSession, args)

```java

public Object execute(SqlSession sqlSession, Object[] args) {

? ? ? ? Object result;

//根據(jù)是查詢還是更新進入不同分支

? ? ? ? switch (command.getType()) {

? ? ? ? ? ? case INSERT: {

? ? ? ? ? ? ? ? Object param = method.convertArgsToSqlCommandParam(args);

? ? ? ? ? ? ? ? result = rowCountResult(sqlSession.insert(command.getName(), param));

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? ? ? case UPDATE: {

? ? ? ? ? ? ? ? Object param = method.convertArgsToSqlCommandParam(args);

? ? ? ? ? ? ? ? result = rowCountResult(sqlSession.update(command.getName(), param));

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? ? ? case DELETE: {

? ? ? ? ? ? ? ? Object param = method.convertArgsToSqlCommandParam(args);

? ? ? ? ? ? ? ? result = rowCountResult(sqlSession.delete(command.getName(), param));

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? ? ? case SELECT:

? ? ? ? ? ? ? ? if (method.returnsVoid() && method.hasResultHandler()) {

? ? ? ? ? ? ? ? ? ? executeWithResultHandler(sqlSession, args);

? ? ? ? ? ? ? ? ? ? result = null;

? ? ? ? ? ? ? ? } else if (method.returnsMany()) {

? ? ? ? ? ? ? ? ? ? //比如 selectList 就會走這里

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

? ? ? ? ? ? ? ? ? ? // 分頁查詢

? ? ? ? ? ? ? ? ? ? if (IPage.class.isAssignableFrom(method.getReturnType())) {

? ? ? ? ? ? ? ? ? ? ? ? ...

? ? ? ? ? ? ? ? ? ? ? ? //關注這里 執(zhí)行分頁 默認也是執(zhí)行selectList

? ? ? ? ? ? ? ? ? ? ? ? result = executeForIPage(sqlSession, args);

? ? ? ? ? ? ? ? ? ? ? ? ...

? ? ? ? ? ? ? ? }

}

/**

? ? * TODO IPage 專用

? ? */

? ? private <E> List<E> executeForIPage(SqlSession sqlSession, Object[] args) {

? ? ? ? Object param = method.convertArgsToSqlCommandParam(args);

? ? ? ? //執(zhí)行SqlSessionTemplate的selectList

? ? ? ? return sqlSession.selectList(command.getName(), param);

? ? }

? @Override

? public <E> List<E> selectList(String statement, Object parameter) {

? //DefaultSqlSession

? ? return this.sqlSessionProxy.selectList(statement, parameter);

? }

```

###### DefaultSqlSession代理對象獲取sqlSession

```java

private class SqlSessionInterceptor implements InvocationHandler {

? ? @Override

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

? ? //加載sqlSession-getSqlSession-通過sessionFactory.openSession(executorType);

? ? //從中會獲取執(zhí)行器和加載插件

? ? ? SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,

? ? ? ? ? SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

? ? ? try {

//拿到sqlSession才能去執(zhí)行查詢

? ? ? ? Object result = method.invoke(sqlSession, args);

? ? ? ? ....

? ? }

```

```java

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,

? ? ? PersistenceExceptionTranslator exceptionTranslator) {

...

? ? //DefaultSqlSessionFactory.openSession

? ? session = sessionFactory.openSession(executorType);

...

? ? return session;

? }

@Override

? public SqlSession openSession(ExecutorType execType) {

? ? return openSessionFromDataSource(execType, null, false);

? }

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

? ? ? ? //創(chuàng)建執(zhí)行器 MybatisConfiguration.newExecutor

//(默認是simple執(zhí)行器)

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

? ? ? ? //最后返回默認DefaultSqlSession

? ? ? 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();

? ? }

? }

```

###### 裝飾者模式創(chuàng)建executor和責任鏈模式interceptorChain加載插件

```java

@Override

? ? 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 MybatisBatchExecutor(this, transaction);

? ? ? ? } else if (ExecutorType.REUSE == executorType) {

? ? ? ? ? ? executor = new MybatisReuseExecutor(this, transaction);

? ? ? ? } else {

? ? ? ? ? ? executor = new MybatisSimpleExecutor(this, transaction);

? ? ? ? }

? ? ? ? if (cacheEnabled) {

? ? ? ? ? ? //裝飾者模式 裝飾了簡單執(zhí)行器

? ? ? ? ? ? executor = new MybatisCachingExecutor(executor);

? ? ? ? }

? ? ? ? //責任鏈 進行增加所有執(zhí)行器 并執(zhí)行plugin

? ? ? ? //通過判斷插件讥蔽,new Plugin(target, interceptor, signatureMap))生成MybatisCachingExecutor代理對象!!!

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

? ? ? ? return executor;

? ? }

```

##### 繼續(xù)執(zhí)行查詢

DefaultSqlSession.selectList

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Rqw7uuW7-1650516052832)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/7e7eada5ee523b9358e3746bb94237eb)\]](https://img-blog.csdnimg.cn/6711ad8cde8f4c7db48da47dac668b2d.png)

```java

//DefaultSqlSession

@Override

? public <E> List<E> selectList(String statement, Object parameter) {

? ? return this.selectList(statement, parameter, RowBounds.DEFAULT);

? }

? @Override

//rowBounds 是用來邏輯分頁(按照條件將數(shù)據(jù)從數(shù)據(jù)庫查詢到內(nèi)存中涣易,在內(nèi)存中進行分頁)

//wrapCollection(parameter)是用來裝飾集合或者數(shù)組參數(shù) 里面有查詢條件

? public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {

? ? try {

? ? //statement:

? ? ? //名字com.jdh.trade.mapper.SellReconciliationMapper.selectSellReconciliationByPage

? ? ? //獲取MappedStatement對象,通過配置信息從StrictMap緩存中獲取

? ? ? MappedStatement ms = configuration.getMappedStatement(statement);

? ? ? ? //執(zhí)行executor對象里面的query方法

? ? ? ? //這里的executor是在DefaultSqlSessionFactory中勤篮,

? ? ? ? //mybatis 通過Configuration對象創(chuàng)建的 對應CachingExecutor?

? ? ? ? //mybatis-plus 通過MybatisConfiguration 創(chuàng)建MybatisCachingExecutor

? ? ? ? //根據(jù)不同的配置都毒,會有不同的Executor 無論那個執(zhí)行器查詢最終都會到下面的查詢

? ? ? return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

? ? }

```

這一次獲得MappedStatement ms 如下

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cVkJL02K-1650516052833)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/bbd0c079418e440b6bef01551fadbd4f)\]](https://img-blog.csdnimg.cn/19932814ebb54cdcb317dfb280ff29b0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

###### 關鍵查詢

```java

MybatisCachingExecutor

@Override

? ? public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

? ? ? ? //組裝sql--我們主要研究這里的組裝

? ? ? ? BoundSql boundSql = ms.getBoundSql(parameterObject);

//獲取一級緩存key

? ? ? ? CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

? ? ? ? //從緩存查詢還是直接查詢數(shù)據(jù)庫等

? ? ? ? return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

? ? }

```

```java

MappedStatement

public BoundSql getBoundSql(Object parameterObject) {

? ? //sqlSource 為 DynamicSqlSource(動態(tài)sql拼接)見上圖

//SqlSouce里面已經(jīng)解析mapper對應的sql

//已經(jīng)被解析為MixedSqlNode

? ? BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

? ? ....

? ? return boundSql;

? }

```

```xml

<select id="selectSellReconciliationByPage"

? ? ? ? ? ? resultType="com.jdh.trade.model.bankcontribution.resp.QuerySellReconciliationResp">

? ? ? ? SELECT

? ? ? ? ...

? ? ? ? WHERE sr.root_code = po.root_code

? ? ? ? ...

? ? ? ? <if test="param.pushStatus != null and param.pushStatus 色罚!='' ">

? ? ? ? ? ? and sr.push_status = #{param.pushStatus}

</select>

```

###### 解析sql過程

```java

@Override

? public BoundSql getBoundSql(Object parameterObject) {

? ? //下面有實體屬性

? ? DynamicContext context = new DynamicContext(configuration, parameterObject);

? ? //處理一個個的sqlNode 編譯出一個完整的xml的sql

? ? //rootSqlNode是 MixedSqlNode 合并sql節(jié)點

? ? //${} 已經(jīng)在靜態(tài)節(jié)點賦值了 因為是直接靜態(tài)判斷${ 然后替換 會有sql注入風險

? ? rootSqlNode.apply(context);

? ? //創(chuàng)建sql信息解析器

? ? SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);

? ? //獲取入?yún)㈩愋?/p>

? ? Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

? ? //執(zhí)行解析:將帶有#{}和${}的sql語句進行解析碰缔,然后封裝到StaticSqlSource中

? ? ? //比如and sr.push_status = #{param.pushStatus} 解析成? and sr.push_status = ?

? ? SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

? ? //將解析后的sql語句還有入?yún)⒔壎ǖ揭黄穑ǚ庋b到一個對象中,此時還沒有將參數(shù)替換到SQL占位符戳护?)

? ? ? //此時變?yōu)殪o態(tài)綁定 StaticSqlSource

? ? BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

? ? context.getBindings().forEach(boundSql::setAdditionalParameter);

? ? return boundSql;

? }

```

###### sql解析為MixedSqlNode節(jié)點

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NjkqYKO0-1650516052833)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/6f3a80fa6d0587f8844927a0a4e36141)\]](https://img-blog.csdnimg.cn/90b0406c995743c588dc9c6120647ce7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WYiKs5H5-1650516052834)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/fd0edc96898cccdbdfdebc55531df78f)\]](https://img-blog.csdnimg.cn/1f3e827aeccd4f549e764c7258637311.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

**將每一個節(jié)點進行各自的解析 然后拼裝xml的sql 到 sqlBuilder (其實是裝入string屬性)**

我們來看一下 rootSqlNode.apply(context);

######? rootSqlNode.apply(context)

每個節(jié)點根據(jù)自己的規(guī)則判斷是否組裝到sqlBuilder

MixedSqlNode.apply

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wl8g1ejY-1650516052834)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/0fc7f8cea1ab54cc583068836c25b9c5)\]](https://img-blog.csdnimg.cn/6aacecbe823c437396fa0d59f2ecc7b3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

這里我們注意一下為什么IfSqlNode節(jié)點會失效呢

<font color='red'>注意:為什么param.pushStatus != '' 進入這個方法會返回false</font>

```java

@Override

? public boolean apply(DynamicContext context) {

? ? ? //test = param.pushStatus != null and param.pushStatus != ''

? ? ? //context.getBindings() 有前端傳的參數(shù)條件

? ? ? //判斷符合條件就加入

? ? if (evaluator.evaluateBoolean(test, context.getBindings())) {

? ? ? contents.apply(context);

? ? ? return true;

? ? }

? ? return false;

? }

```

調(diào)用ExpressionEvaluator.evaluateBoolean 進行判斷

```java

public boolean evaluateBoolean(String expression, Object parameterObject) {

? ? //問題出現(xiàn)在這里 value 返回了false

? ? Object value = OgnlCache.getValue(expression, parameterObject);

? ? if (value instanceof Boolean) {

? ? ? //從這里返回boolean值 false

? ? ? return (Boolean) value;

? ? }

? ? if (value instanceof Number) {

? ? ? return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;

? ? }

? ? return value != null;

? }

```

再調(diào)用OgnlCache.getValue方法 獲取校驗結果

```java

//表達式expression = param.pushStatus != null and param.pushStatus != ''

//root 入?yún)?/p>

public static Object getValue(String expression, Object root) {

? ? try {

? ? ? //Mybatis底層校驗使用Ognl語法

? ? ? Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);

? ? ? ? //這里調(diào)用Ognl.getValue進行比較

? ? ? return Ognl.getValue(parseExpression(expression), context, root);

? ? } catch (OgnlException e) {

? ? ? throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);

? ? }

? }

```

parseExpression(expression) 會從緩存中讀取Node 如果沒有 去解析成特定類型的Node對象

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jtSyNZzf-1650516052834)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/7f5a45ede1b4c446ca6c010177e944aa)\]](https://img-blog.csdnimg.cn/bd19badffcab45b2922ab70e97345819.png)

```java

//OgnlCache

private static Object parseExpression(String expression) throws OgnlException {

? ? Object node = expressionCache.get(expression);

? ? if (node == null) {

? ? ? node = Ognl.parseExpression(expression);

? ? ? expressionCache.put(expression, node);

? ? }

? ? return node;

? }

```

比如這里轉(zhuǎn)化成 ASTAND

以下節(jié)點都是繼承SimpleNode

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0pTTkrP2-1650516052835)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/79511546ca850b7612af95c39ef14591)\]](https://img-blog.csdnimg.cn/94023c27a12d4fb4a47f8a5bd263ca8a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

Ognl.getValue(parseExpression(expression), context, root)獲取最終結果

```java

public static Object getValue(Object tree, Map context, Object root)

? ? ? ? ? ? throws OgnlException

? ? {

? ? ? ? return getValue(tree, context, root, null);

? ? }

/**

? ? * 計算給定的OGNL表達式樹金抡,從給定的根對象中提取值。通過addDefaultContext()為給定的上下文和根設置默認上下文腌且。

? ? *

? ? * @param tree

? ? *? ? ? ? ? ? 要計算的OGNL表達式樹梗肝,由parseExpression()返回

? ? 如:(param.pushStatus != null) && (param.pushStatus != "")

? ? * @param context

? ? *? ? ? ? ? ? 求值的命名上下文

? ? * @param root

? ? *? ? ? ? ? ? OGNL表達式的根對象


? ? * @return ? 返回計算表達式的結果

? ? */

public static Object getValue(Object tree, Map context, Object root, Class resultType) throws OgnlException {

? ? ? ? OgnlContext ognlContext = (OgnlContext)addDefaultContext(root, context);

? ? ? ? //根據(jù)解析表達式獲取的節(jié)點 該例子為 ASTAND

? ? Node node = (Node)tree;

? ? ? ? Object result;

? ? ? ? if (node.getAccessor() != null) {

? ? ? ? ? ? result = node.getAccessor().get(ognlContext, root);

? ? ? ? } else {

? ? ? ? ? ? //調(diào)用父類SimpleNode的getValue方法

//該節(jié)點為 ASTAND

? ? ? ? ? ? result = node.getValue(ognlContext, root);

? ? ? ? }

...

? ? ? ? return result;

? ? }

```

###### ASTAND節(jié)點樹進行解析

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Uv5gijjR-1650516052836)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/4553a1d57700a78d5d412db1f5a5a389)\]](https://img-blog.csdnimg.cn/162fa2950cd84f239eb568b39cc4e968.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

調(diào)用父類 SimpleNode. getValue

以下兩個方法方法為父類方法

```java

public final Object getValue(OgnlContext context, Object source) throws OgnlException {

? ? ? ? Object result = null;

? ? ? ? if (context.getTraceEvaluations()) {

? ? ? ? ? ? ...

? ? ? ? } else {

? ? ? ? ? ? //獲取常量值

? ? ? ? ? ? result = this.evaluateGetValueBody(context, source);

? ? ? ? }

? ? ? ? return result;

? ? }

```

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gCaqx2zA-1650516052836)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/b89e9fd510e869daf43bccaf08a6ac61)\]](https://img-blog.csdnimg.cn/9b1c7262557a4211a5774818005bc628.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

調(diào)用子類自己的判斷實現(xiàn)getValueBody? 從節(jié)點樹葉子節(jié)點ASTConst一層一層根據(jù)規(guī)則返回結果給上一層

###### ASTAnd

```java

protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {

? ? ? ? Object result = null;

? ? ? ? int last = this._children.length - 1;

? ? ? ? for(int i = 0; i <= last; ++i) {

? ? ? ? ? ? result = this._children[i].getValue(context, source);

? ? ? ? ? ? if (i != last && !OgnlOps.booleanValue(result)) {

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return result;

? ? }

```

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-s4tnU2yD-1650516052837)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/eb2e2e018bc33c8ee5b76a11252e2fb1)\]](https://img-blog.csdnimg.cn/38a998291734448e90c9cbcd2ee4c9aa.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

###### ASTNotEq

```java

protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {

? ? ? ? Object v1 = this._children[0].getValue(context, source);

? ? ? ? Object v2 = this._children[1].getValue(context, source);

? ? ? ? return OgnlOps.equal(v1, v2) ? Boolean.FALSE : Boolean.TRUE;

? ? }

```

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HyFxeqay-1650516052837)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/6ada714321b6458785e31fabe2887a86)\]](https://img-blog.csdnimg.cn/49b92c25ffa1462f8ce2b5ab4a353aef.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWlNaW5n5LuU,size_20,color_FFFFFF,t_70,g_se,x_16)

<font color='red'>這里出現(xiàn)了問題為什么 0 和 '' equal會相等</font>

```java

public static boolean equal(Object v1, Object v2) {

? ? ? ? if (v1 == null) {

? ? ? ? ? ? return v2 == null;

? ? ? ? ? ? //!isEqual 判斷出問題

? ? ? ? } else if (v1 != v2 && !isEqual(v1, v2)) {

? ? ? ? ? ? if (v1 instanceof Number && v2 instanceof Number) {

? ? ? ? ? ? ? ? return ((Number)v1).doubleValue() == ((Number)v2).doubleValue();

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? return false;

? ? ? ? ? ? }

? ? ? ? } else {

? ? ? ? ? ? return true;

? ? ? ? }

? ? }

```

<font color='red'>!isEqual(v1, v2)? 判斷成了相等返回true</font>

```java

public static boolean isEqual(Object object1, Object object2) {

? ? ? ? ...

{

? ? ? ? ? ? ? ? ? ? //前面根據(jù)Ognl的算法

? ? ? ? ? ? ? ? ? ? result = compareWithConversion(object1, object2) == 0;

? ? ? ? ? ? ? ? }

? ? ? ? return result;

? ? }

public static int compareWithConversion(Object v1, Object v2) {

...

? ? ? ? double dv1 = doubleValue(v1);

? ? ? ? double dv2 = doubleValue(v2);

? ? ? ? return dv1 == dv2 ? 0 : (dv1 < dv2 ? -1 : 1);

? ? }

```

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tP6PMJ3z-1650516052838)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/d51079b0f256d5537fae3039142a2542)\]](https://img-blog.csdnimg.cn/0ccec46131624ee39dd901e7a849baa1.png)

###### 4、問題根源

```java

public static double doubleValue(Object value) throws NumberFormatException {

? ? ? ? if (value == null) {

? ? ? ? ? ? return 0.0D;

? ? ? ? } else {

? ? ? ? ? ? Class c = value.getClass();

? ? ? ? ? ? if (c.getSuperclass() == Number.class) {

? ? ? ? ? ? ? ? return ((Number)value).doubleValue();

? ? ? ? ? ? } else if (c == Boolean.class) {

? ? ? ? ? ? ? ? return (Boolean)value ? 1.0D : 0.0D;

? ? ? ? ? ? } else if (c == Character.class) {

? ? ? ? ? ? ? ? return (double)(Character)value;

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? //這里將''改成了0.0D

? ? ? ? ? ? ? ? String s = stringValue(value, true);

? ? ? ? ? ? ? ? return s.length() == 0 ? 0.0D : Double.parseDouble(s);

? ? ? ? ? ? }

? ? ? ? }

? ? }

```

最后是 "" 轉(zhuǎn)換成了0.0 去比較了

###### ASTChain

```java

protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {

? ? ? ? Object result = source;

? ? ? ? int i = 0;

? ? ? ? for(int ilast = this._children.length - 1; i <= ilast; ++i) {

? ? ? ? ? ? boolean handled = false;

? ? ? ? ? ? if (i < ilast && this._children[i] instanceof ASTProperty) {

? ? ? ? ? ? ? ? ASTProperty propertyNode = (ASTProperty)this._children[i];

? ? ? ? ? ? ? ? int indexType = propertyNode.getIndexedPropertyType(context, result);

...

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? if (!handled) {

//將最后的鏈值返回

? ? ? ? ? ? ? ? result = this._children[i].getValue(context, result);

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return result;

? ? }

```

###### ASTProperty

```java

protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {

? ? ? ? //通過ASTConst 直接獲取返回值 如 param 或 pushStatus 再讀取入?yún)闹?/p>

? Object property = this.getProperty(context, source);

? ? //根據(jù) param 或 pushStatus 返回對應的值

? ? ? ? Object result = OgnlRuntime.getProperty(context, source, property);

? ? ? ? if (result == null) {

? ? ? ? ? ? result = OgnlRuntime.getNullHandler(OgnlRuntime.getTargetClass(source)).nullPropertyValue(context, source, property);

? ? ? ? }

? ? ? ? return result;

? ? }

```

###### ASTConst

```java

protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {

? ? ? ? return this.value;

? ? }

```

#### 解決

(1) 不用Integer接收铺董,使用String類型接收

(2)去掉【參數(shù)巫击!=’‘】 的非空判斷

![\[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Sex5LBqb-1650516052838)(http://10.0.17.20/server/index.php?s=/api/attachment/visitFile/sign/e872d96804f4039fa5878d896d0f970f)\]](https://img-blog.csdnimg.cn/cf172267c941471a8ea0bf6402ca0f08.png)

> 番外:如果String類型需要判斷!=0精续,則需要寫成 xxx != '0'.toString()

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坝锰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子重付,更是在濱河造成了極大的恐慌顷级,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件确垫,死亡現(xiàn)場離奇詭異弓颈,居然都是意外死亡帽芽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門翔冀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來导街,“玉大人,你說我怎么就攤上這事纤子【漳洌” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵计福,是天一觀的道長跌捆。 經(jīng)常有香客問我,道長象颖,這世上最難降的妖魔是什么佩厚? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮说订,結果婚禮上抄瓦,老公的妹妹穿的比我還像新娘。我一直安慰自己陶冷,他們只是感情好钙姊,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著埂伦,像睡著了一般煞额。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沾谜,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天膊毁,我揣著相機與錄音,去河邊找鬼基跑。 笑死婚温,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的媳否。 我是一名探鬼主播栅螟,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼篱竭!你這毒婦竟也來了力图?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤室抽,失蹤者是張志新(化名)和其女友劉穎搪哪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡晓折,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年惑朦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漓概。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡漾月,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胃珍,到底是詐尸還是另有隱情梁肿,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布觅彰,位于F島的核電站吩蔑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏填抬。R本人自食惡果不足惜烛芬,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望飒责。 院中可真熱鬧赘娄,春花似錦、人聲如沸宏蛉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拾并。三九已至揍堰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辟灰,已是汗流浹背个榕。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芥喇,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓凰萨,卻偏偏與公主長得像继控,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胖眷,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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